From 187ccad15b2b48b82680508e544dad6eec907d8f Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 8 Nov 2022 15:13:47 -0800 Subject: [PATCH] Add user routes --- server/controllers/users.controller.ts | 39 +++++++++++++- server/helpers/api-model.ts | 1 + server/routes/users.route.ts | 26 +++++++--- server/services/users.service.ts | 71 ++++++++++++++++++++++++-- server/validations/auth.validation.ts | 2 +- server/validations/users.validation.ts | 31 ++++++++++- 6 files changed, 157 insertions(+), 13 deletions(-) diff --git a/server/controllers/users.controller.ts b/server/controllers/users.controller.ts index bbeb941d9..c7bc2cae9 100644 --- a/server/controllers/users.controller.ts +++ b/server/controllers/users.controller.ts @@ -2,8 +2,13 @@ import { Request, Response } from 'express'; import { ApiSuccess, getSuccessResponse } from '@/utils'; import { toApiModel } from '@helpers/api-model'; import { service } from '@services/index'; +import { validation } from '@validations/index'; +import { TypedRequest } from '@validations/shared.validation'; -const getUserDetail = async (req: Request, res: Response) => { +const getUserDetail = async ( + req: TypedRequest, + res: Response +) => { const { id } = req.params; const user = await service.users.findById(req.authUser, { id }); const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] }); @@ -16,7 +21,39 @@ const getUserList = async (_req: Request, res: Response) => { return res.status(success.statusCode).json(getSuccessResponse(success)); }; +const createUser = async ( + req: TypedRequest, + res: Response +) => { + const user = await service.users.createUser(req.body); + const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] }); + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; + +const updateUser = async ( + req: TypedRequest, + res: Response +) => { + const { userId } = req.params; + const user = await service.users.updateUser({ userId }, req.body); + const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] }); + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; + +const deleteUser = async ( + req: TypedRequest, + res: Response +) => { + const { userId } = req.params; + await service.users.deleteUser({ userId }); + const success = ApiSuccess.noContent({ data: null }); + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; + export const usersController = { + createUser, + deleteUser, getUserDetail, getUserList, + updateUser, }; diff --git a/server/helpers/api-model.ts b/server/helpers/api-model.ts index f29cd235b..95671e93c 100644 --- a/server/helpers/api-model.ts +++ b/server/helpers/api-model.ts @@ -598,6 +598,7 @@ const users = ( /* eslint-disable sort-keys-fix/sort-keys-fix */ id: item.id, username: item.username, + displayName: item.displayName, accessToken: item.accessToken, refreshToken: item.refreshToken, enabled: item.enabled, diff --git a/server/routes/users.route.ts b/server/routes/users.route.ts index 21d6b77c7..60d72ed7b 100644 --- a/server/routes/users.route.ts +++ b/server/routes/users.route.ts @@ -1,14 +1,26 @@ import express, { Router } from 'express'; import { controller } from '@controllers/index'; -import { validateRequest, validation } from '@validations/index'; +import { service } from '@services/index'; +import { ApiError } from '@utils/index'; import { authenticateAdmin } from '../middleware/authenticate-admin'; export const router: Router = express.Router({ mergeParams: true }); -router.get('/', authenticateAdmin, controller.users.getUserList); +router + .route('/') + .get(authenticateAdmin, controller.users.getUserList) + .post(authenticateAdmin, controller.users.createUser); -router.get( - ':serverId', - validateRequest(validation.users.detail), - controller.users.getUserDetail -); +router.param('userId', async (req, _res, next, userId) => { + await service.users.findById(req.authUser, { id: userId }); + + if (req.authUser.isAdmin || req.authUser.id === userId) { + return next(); + } + + throw ApiError.forbidden('You are not allowed to access this resource'); +}); + +router.route('/:userId/update').post(controller.users.updateUser); + +router.route('/:userId/delete').post(controller.users.deleteUser); diff --git a/server/services/users.service.ts b/server/services/users.service.ts index db68b4916..4ec8eca66 100644 --- a/server/services/users.service.ts +++ b/server/services/users.service.ts @@ -1,6 +1,7 @@ -import { prisma } from '../lib'; -import { AuthUser } from '../middleware'; -import { ApiError } from '../utils'; +import bcrypt from 'bcryptjs'; +import { prisma } from '@lib/prisma'; +import { AuthUser } from '@middleware/authenticate'; +import { randomString, ApiError } from '@utils/index'; const findById = async (user: AuthUser, options: { id: string }) => { const { id } = options; @@ -26,7 +27,71 @@ const findMany = async () => { return users; }; +const createUser = async (options: { + displayName?: string; + password: string; + username: string; +}) => { + const { password, username, displayName } = options; + + const [userExists, displayNameExists] = await prisma.$transaction([ + prisma.user.findUnique({ where: { username } }), + prisma.user.findUnique({ where: { displayName } }), + ]); + + if (userExists) { + throw ApiError.conflict('The user already exists.'); + } + + if (displayNameExists) { + throw ApiError.conflict('The display name already exists.'); + } + + const hashedPassword = await bcrypt.hash(password, 12); + + const user = await prisma.user.create({ + data: { + deviceId: `${username}_${randomString(10)}`, + enabled: false, + password: hashedPassword, + username, + }, + }); + + return user; +}; + +const deleteUser = async (options: { userId: string }) => { + const { userId } = options; + + const user = await prisma.user.delete({ where: { id: userId } }); + return user; +}; + +const updateUser = async ( + options: { userId: string }, + data: { + password?: string; + username?: string; + } +) => { + const { userId } = options; + const { username, password } = data; + + const hashedPassword = password && (await bcrypt.hash(password, 12)); + + const user = await prisma.user.update({ + data: { password: hashedPassword, username }, + where: { id: userId }, + }); + + return user; +}; + export const usersService = { + createUser, + deleteUser, findById, findMany, + updateUser, }; diff --git a/server/validations/auth.validation.ts b/server/validations/auth.validation.ts index b1b765542..ded3cf5bb 100644 --- a/server/validations/auth.validation.ts +++ b/server/validations/auth.validation.ts @@ -12,7 +12,7 @@ const login = { const register = { body: z.object({ password: z.string().min(6).max(255), - username: z.string().min(4).max(26), + username: z.string().min(2).max(255), }), params: z.object({}), query: z.object({}), diff --git a/server/validations/users.validation.ts b/server/validations/users.validation.ts index 05fcd4f23..149ce3fca 100644 --- a/server/validations/users.validation.ts +++ b/server/validations/users.validation.ts @@ -3,10 +3,39 @@ import { idValidation } from './shared.validation'; const detail = { body: z.object({}), - params: z.object({ ...idValidation('id') }), + params: z.object({ ...idValidation('userId') }), + query: z.object({}), +}; + +const createUser = { + body: z.object({ + displayName: z.optional(z.string()), + password: z.string().min(6).max(255), + username: z.string().min(2).max(255), + }), + params: z.object({}), + query: z.object({}), +}; + +const deleteUser = { + body: z.object({}), + params: z.object({ ...idValidation('userId') }), + query: z.object({}), +}; + +const updateUser = { + body: z.object({ + displayName: z.optional(z.string().min(2).max(255)), + password: z.optional(z.string().min(6).max(255)), + username: z.optional(z.string().min(2).max(255)), + }), + params: z.object({ ...idValidation('userId') }), query: z.object({}), }; export const usersValidation = { + createUser, + deleteUser, detail, + updateUser, };