diff --git a/src/controllers/deleteUser.ts b/src/controllers/deleteUser.ts new file mode 100644 index 0000000..c646cb7 --- /dev/null +++ b/src/controllers/deleteUser.ts @@ -0,0 +1,20 @@ +import bcrypt from 'bcryptjs' +import User from '../model/User' + +export const deleteUser = async ( + username: string, + isAdmin: boolean, + data: any +) => { + const { password } = data + + const user = await User.findOne({ username }) + if (!user) throw new Error('Username is not found.') + + if (!isAdmin) { + const validPass = await bcrypt.compare(password, user.password) + if (!validPass) throw new Error('Invalid password.') + } + + await User.deleteOne({ username }) +} diff --git a/src/controllers/updateUser.ts b/src/controllers/updateUser.ts new file mode 100644 index 0000000..33cb3be --- /dev/null +++ b/src/controllers/updateUser.ts @@ -0,0 +1,35 @@ +import bcrypt from 'bcryptjs' +import User from '../model/User' + +export const updateUser = async (currentUsername: string, data: any) => { + const { displayName, username, password, isAdmin, isActive } = data + + const params: any = { displayName, isAdmin, isActive } + + if (username && currentUsername !== username) { + // Checking if username is already in the database + const usernameExist = await User.findOne({ username }) + if (usernameExist) throw new Error('Username already exists.') + + params.username = username + } + + if (password) { + // Hash passwords + const salt = await bcrypt.genSalt(10) + params.password = await bcrypt.hash(password, salt) + } + + const updatedUser = await User.findOneAndUpdate( + { username: currentUsername }, + params, + { new: true } + ) + + return { + displayName: updatedUser.displayName, + username: updatedUser.username, + isAdmin: updatedUser.isAdmin, + isActive: updatedUser.isActive + } +} diff --git a/src/utils/authenticateToken.ts b/src/middlewares/authenticateToken.ts similarity index 95% rename from src/utils/authenticateToken.ts rename to src/middlewares/authenticateToken.ts index a148539..ae1c041 100644 --- a/src/utils/authenticateToken.ts +++ b/src/middlewares/authenticateToken.ts @@ -1,5 +1,5 @@ import jwt from 'jsonwebtoken' -import { verifyTokenInDB } from './verifyTokenInDB' +import { verifyTokenInDB } from '../utils' export const authenticateAccessToken = (req: any, res: any, next: any) => { authenticateToken( diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts new file mode 100644 index 0000000..e3b1a1e --- /dev/null +++ b/src/middlewares/index.ts @@ -0,0 +1,2 @@ +export * from './authenticateToken' +export * from './verifyAdmin' diff --git a/src/middlewares/verifyAdmin.ts b/src/middlewares/verifyAdmin.ts new file mode 100644 index 0000000..619128d --- /dev/null +++ b/src/middlewares/verifyAdmin.ts @@ -0,0 +1,5 @@ +export const verifyAdmin = (req: any, res: any, next: any) => { + const { user } = req + if (!user?.isAdmin) return res.status(401).send('Admin account required') + next() +} diff --git a/src/routes/api/auth.ts b/src/routes/api/auth.ts index 5d11a56..69debc7 100644 --- a/src/routes/api/auth.ts +++ b/src/routes/api/auth.ts @@ -5,9 +5,13 @@ import jwt from 'jsonwebtoken' import Client from '../../model/Client' import User from '../../model/User' + import { authenticateAccessToken, - authenticateRefreshToken, + authenticateRefreshToken +} from '../../middlewares' + +import { authorizeValidation, removeTokensInDB, saveTokensInDB, diff --git a/src/routes/api/index.ts b/src/routes/api/index.ts index 3ce25da..2ebb7c9 100644 --- a/src/routes/api/index.ts +++ b/src/routes/api/index.ts @@ -1,12 +1,13 @@ import express from 'express' import dotenv from 'dotenv' +import { authenticateAccessToken, verifyAdmin } from '../../middlewares' + import driveRouter from './drive' import stpRouter from './stp' import userRouter from './user' import clientRouter from './client' import authRouter, { connectDB } from './auth' -import { authenticateAccessToken } from '../../utils' dotenv.config() connectDB() @@ -15,14 +16,8 @@ const router = express.Router() router.use('/drive', authenticateAccessToken, driveRouter) router.use('/stp', authenticateAccessToken, stpRouter) -router.use('/user', authenticateAccessToken, verifyAdmin, userRouter) +router.use('/user', userRouter) router.use('/client', authenticateAccessToken, verifyAdmin, clientRouter) router.use('/auth', authRouter) -function verifyAdmin(req: any, res: any, next: any) { - const { user } = req - if (!user?.isAdmin) return res.status(403).send('Admin account required') - next() -} - export default router diff --git a/src/routes/api/user.ts b/src/routes/api/user.ts index c3b4dad..c43774d 100644 --- a/src/routes/api/user.ts +++ b/src/routes/api/user.ts @@ -1,25 +1,99 @@ import express from 'express' import { createUser } from '../../controllers/createUser' -import { registerUserValidation } from '../../utils' +import { updateUser } from '../../controllers/updateUser' +import { deleteUser } from '../../controllers/deleteUser' +import { authenticateAccessToken, verifyAdmin } from '../../middlewares' +import User from '../../model/User' +import { + deleteUserValidation, + registerUserValidation, + updateUserValidation +} from '../../utils' const userRouter = express.Router() -userRouter.post('/', async (req, res) => { +userRouter.post('/', authenticateAccessToken, verifyAdmin, async (req, res) => { const { error, value: data } = registerUserValidation(req.body) if (error) return res.status(400).send(error.details[0].message) try { const savedUser = await createUser(data) - res.send({ - displayName: savedUser.displayName, - username: savedUser.username, - isAdmin: savedUser.isAdmin, - isActive: savedUser.isActive, - tokens: [] - }) + res.send(savedUser) } catch (err: any) { res.status(403).send(err.toString()) } }) +userRouter.get('/', authenticateAccessToken, async (req, res) => { + try { + const users = await User.find({}) + .select({ _id: 0, username: 1, displayName: 1, isAdmin: 1, isActive: 1 }) + .exec() + res.send(users) + } catch (err: any) { + res.status(403).send(err.toString()) + } +}) + +userRouter.get('/:username', authenticateAccessToken, async (req: any, res) => { + const { username } = req.params + try { + const user = await User.findOne({ username }) + .select({ _id: 0, username: 1, displayName: 1, isAdmin: 1, isActive: 1 }) + .exec() + res.send(user) + } catch (err: any) { + res.status(403).send(err.toString()) + } +}) + +userRouter.patch( + '/:username', + authenticateAccessToken, + async (req: any, res) => { + const { user } = req + const { username } = req.params + + // only an admin can update other users + if (!user.isAdmin && user.username !== username) { + return res.status(401).send('Admin account required') + } + + // only an admin can update `isActive` and `isAdmin` fields + const { error, value: data } = updateUserValidation(req.body, user.isAdmin) + if (error) return res.status(400).send(error.details[0].message) + + try { + const user = await updateUser(username, data) + res.send(user) + } catch (err: any) { + res.status(403).send(err.toString()) + } + } +) + +userRouter.delete( + '/:username', + authenticateAccessToken, + async (req: any, res) => { + const { user } = req + const { username } = req.params + + // only an admin can delete other users + if (!user.isAdmin && user.username !== username) { + return res.status(401).send('Admin account required') + } + + const { error, value: data } = deleteUserValidation(req.body, user.isAdmin) + if (error) return res.status(400).send(error.details[0].message) + + try { + await deleteUser(username, user.isAdmin, data) + res.status(200).send('Account Deleted!') + } catch (err: any) { + res.status(403).send(err.toString()) + } + } +) + export default userRouter diff --git a/src/utils/index.ts b/src/utils/index.ts index cb8ef98..b503a6a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,3 @@ -export * from './authenticateToken' export * from './file' export * from './removeTokensInDB' export * from './saveTokensInDB' diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 233d6a6..a8ec8d3 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -1,12 +1,12 @@ import Joi from 'joi' -const usernameSchema = Joi.string().alphanum().min(6).max(20).required() -const passwordSchema = Joi.string().min(6).max(1024).required() +const usernameSchema = Joi.string().alphanum().min(6).max(20) +const passwordSchema = Joi.string().min(6).max(1024) export const authorizeValidation = (data: any): Joi.ValidationResult => Joi.object({ - username: usernameSchema, - password: passwordSchema, + username: usernameSchema.required(), + password: passwordSchema.required(), clientId: Joi.string().required() }).validate(data) @@ -19,12 +19,40 @@ export const tokenValidation = (data: any): Joi.ValidationResult => export const registerUserValidation = (data: any): Joi.ValidationResult => Joi.object({ displayName: Joi.string().min(6).required(), - username: usernameSchema, - password: passwordSchema, + username: usernameSchema.required(), + password: passwordSchema.required(), isAdmin: Joi.boolean(), isActive: Joi.boolean() }).validate(data) +export const deleteUserValidation = ( + data: any, + isAdmin: boolean = false +): Joi.ValidationResult => + Joi.object( + isAdmin + ? {} + : { + password: passwordSchema.required() + } + ).validate(data) + +export const updateUserValidation = ( + data: any, + isAdmin: boolean = false +): Joi.ValidationResult => { + const validationChecks: any = { + displayName: Joi.string().min(6), + username: usernameSchema, + password: passwordSchema + } + if (isAdmin) { + validationChecks.isAdmin = Joi.boolean() + validationChecks.isActive = Joi.boolean() + } + return Joi.object(validationChecks).validate(data) +} + export const registerClientValidation = (data: any): Joi.ValidationResult => Joi.object({ clientId: Joi.string().required(), diff --git a/src/utils/verifyTokenInDB.ts b/src/utils/verifyTokenInDB.ts index 9c9f899..b6d8220 100644 --- a/src/utils/verifyTokenInDB.ts +++ b/src/utils/verifyTokenInDB.ts @@ -8,6 +8,8 @@ export const verifyTokenInDB = async ( ) => { const dbUser = await User.findOne({ username }) + if (!dbUser) return undefined + const currentTokenObj = dbUser.tokens.find( (tokenObj: any) => tokenObj.clientId === clientId )