From bee01afafb02a2f31660b805990dd966e9300d58 Mon Sep 17 00:00:00 2001 From: Tad Decker Date: Mon, 20 Nov 2023 11:52:44 -0700 Subject: [PATCH] authorization --- app.js | 7 ++-- package.json | 4 ++- src/config/config.js | 4 ++- src/controllers/noteController.js | 20 +++++++++++ src/controllers/userController.js | 45 +++++++++++++++++++----- src/middlewares/authMiddleware.js | 20 +++++++++++ src/models/userModel.js | 57 ++++++++++++++++++++++++++++--- src/routes/notesRouter.js | 9 ++--- src/utils/apiUtils.js | 4 +++ 9 files changed, 148 insertions(+), 22 deletions(-) create mode 100644 src/middlewares/authMiddleware.js diff --git a/app.js b/app.js index 6d7134e..7af8d97 100644 --- a/app.js +++ b/app.js @@ -3,16 +3,15 @@ import dotenv from 'dotenv' import notesRouter from './src/routes/notesRouter.js' import usersRouter from './src/routes/usersRouter.js' import cors from 'cors' +import config from './src/config/config.js' dotenv.config() -const port = process.env.PORT ?? 80 - -// configureAzure(config.database) +const port = config.port ?? 80 // Default to 80 const app = express() const corsOptions = { - origin: [process.env.DEV_URL], + origin: [config.devUrl], methods: 'GET, PUT, POST, DELETE, HEAD, PATCH, OPTIONS', credentials: true, allowedHeaders: 'Content-Type, Authorization' diff --git a/package.json b/package.json index 6775758..b9b951f 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,10 @@ "dependencies": { "@azure/cosmos": "^4.0.0", "@azure/identity": "^3.3.2", + "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.3.1", - "express": "^4.18.2" + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2" } } diff --git a/src/config/config.js b/src/config/config.js index f4d6961..8ada05b 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -3,8 +3,10 @@ dotenv.config() export default { endpoint: process.env.COSMOS_ENDPOINT, + port: process.env.PORT, key: process.env.PRIMARY_KEY, databaseName: process.env.DATABASE_NAME, notesContainerName: process.env.NOTES_CONTAINER_NAME, - usersContainerName: process.env.USERS_CONTAINER_NAME + usersContainerName: process.env.USERS_CONTAINER_NAME, + devUrl: process.env.DEV_URL } diff --git a/src/controllers/noteController.js b/src/controllers/noteController.js index 586d056..bfc37be 100644 --- a/src/controllers/noteController.js +++ b/src/controllers/noteController.js @@ -9,6 +9,13 @@ import Note from '../models/noteModel.js' import { apiBadRequestError, apiInternalError, apiNotFoundError } from '../utils/apiUtils.js' + +/** + * @function getNotes + * @function createNote + * @function editNote + * @function deleteNote + */ const notesController = { /** @@ -49,6 +56,19 @@ const notesController = { return } + if (!newNote.title) { + apiBadRequestError(res, 'title is a required field in the body') + return + } + + if (!newNote.body) { + apiBadRequestError(res, 'body is a required field in the body') + return + } + + // Check authorization + + const createdItem = await Note.createNote(userId, newNote) res.json(createdItem) } catch (e) { diff --git a/src/controllers/userController.js b/src/controllers/userController.js index acd31e9..be81727 100644 --- a/src/controllers/userController.js +++ b/src/controllers/userController.js @@ -7,29 +7,58 @@ */ import userModel from "../models/userModel.js" -import { apiBadRequestError } from "../utils/apiUtils.js" +import { apiBadRequestError, apiForbiddenError } from "../utils/apiUtils.js" const userController = { - signup(req, res) { - body = req.body + async signup(req, res) { + const { body } = req if (!body) { apiBadRequestError(res, 'Body is required') return } - const username = body.username - const password = body.password + + const { username, password } = body if (!username || !password) { apiBadRequestError(res, 'Missing username or password') return } - userModel.signup(username, password) + // If a username already exists, return a 403 + const users = await userModel.getUser(username) + console.log(users) + + if (users.length > 0) { + apiForbiddenError(res, 'Username already exists.') + return + } + + await userModel.signup(username, password) res.send('User signed up!') }, - login(req, res) { - res.send('This will login the user.') + async login(req, res) { + const { body } = req + if (!body) { + apiBadRequestError(res, 'Body is required') + return + } + + const { username, password } = req.body + + if (!username || !password) { + apiBadRequestError(res, 'Missing username or password') + return + } + + const authToken = await userModel.login(username, password) + + if (authToken) { + res.json({ authToken }) + } + else { + apiForbiddenError(res, 'Invalid credentials') + } } } diff --git a/src/middlewares/authMiddleware.js b/src/middlewares/authMiddleware.js new file mode 100644 index 0000000..7f2d3d3 --- /dev/null +++ b/src/middlewares/authMiddleware.js @@ -0,0 +1,20 @@ +import jwt from "jsonwebtoken" +import config from "../config/config.js" + +export function requireAuth (req, res, next) { + const token = req.headers.authorization + + // 401 if no authtoken + if (!token) { + return res.status(401).json({ error: 'Unauthorized' }) + } + + try { + const decoded = jwt.verify(token.replace('Bearer ', ''), config.key) + req.user = decoded + next() + } catch (error) { + // Invalid token, respond with an error + res.status(401).json({ error: 'Unauthorized' }) + } + } \ No newline at end of file diff --git a/src/models/userModel.js b/src/models/userModel.js index 937c16f..329c54d 100644 --- a/src/models/userModel.js +++ b/src/models/userModel.js @@ -9,6 +9,8 @@ import { generateId } from '../utils/commonUtils.js' import { CosmosClient } from '@azure/cosmos' +import bcrypt from 'bcrypt' +import jwt from 'jsonwebtoken' import config from '../config/config.js' const cosmosClient = new CosmosClient({ @@ -19,23 +21,70 @@ const cosmosClient = new CosmosClient({ const database = cosmosClient.database(config.databaseName) const container = database.container(config.usersContainerName) -export default { +const saltRounds = 10 - async getUser(userName) { +/** + * @function getUser + * @function signup + * @function login + */ +export default { + /** + * @description Get a user based on their username + * @param {String} userName + * @returns username + */ + async getUser(username) { + const querySpec = { + query: 'select * from c where c.username = @username', + parameters: [ + { + name: '@username', + value: username + } + ] + } + const { resources: items } = await container.items.query(querySpec).fetchAll() + return items }, /** * @param {String} username + * @returns created user (and it should return an authtoken, too) */ async signup(username, password) { const userId = generateId() - const user = { userId, username, password } + const hashedPassword = await bcrypt.hash(password, saltRounds) + const user = { userId, username, password: hashedPassword } const { resource: createdUser } = await container.items.create(user) return createdUser }, - login(username, password) { + /** + * Check a username and password. If it matches, returns an authtoken. + * @param {String} username + * @param {String} password + * @returns Authtoken + */ + async login(username, password) { + const users = await this.getUser(username) + if (users.length == 0) return null // Bad username + + const user = users.at(0) + + const match = await bcrypt.compare(password, user.password) + + // If the password matches, return the authtoken + if (match) { + const token = jwt.sign( + { userId: user.userId, username: user.username }, + config.key, + { expiresIn: '1h' } + ) + return token + } + return null // Invalid credentials } } diff --git a/src/routes/notesRouter.js b/src/routes/notesRouter.js index f12995f..c06721e 100644 --- a/src/routes/notesRouter.js +++ b/src/routes/notesRouter.js @@ -8,12 +8,13 @@ import express from 'express' import notesController from '../controllers/noteController.js' +import { requireAuth } from '../middlewares/authMiddleware.js' const router = express.Router() -router.get('/', notesController.getNotes) -router.post('/', notesController.createNote) -router.put('/', notesController.editNote) -router.delete('/', notesController.deleteNote) +router.get('/', requireAuth, notesController.getNotes) +router.post('/', requireAuth, notesController.createNote) +router.put('/', requireAuth, notesController.editNote) +router.delete('/', requireAuth, notesController.deleteNote) export default router diff --git a/src/utils/apiUtils.js b/src/utils/apiUtils.js index fc411e5..4ee7fd0 100644 --- a/src/utils/apiUtils.js +++ b/src/utils/apiUtils.js @@ -8,6 +8,10 @@ export function apiBadRequestError(res, errorMessage) { res.status(400).send(errorMessage) } +export function apiForbiddenError(res, errorMessage) { + res.status(403).send(errorMessage) +} + /** * Send a 404 error, along with an error message. * @param {Object} res