From a64f48bc5b5795a40f895b054140e8ba8c141b5e Mon Sep 17 00:00:00 2001 From: juancarmore Date: Mon, 9 Jun 2025 21:42:47 +0200 Subject: [PATCH] backend: implement password change functionality and move user profile retrieval to users endpoints --- backend/src/controllers/auth.controller.ts | 14 -------- backend/src/controllers/index.ts | 1 + backend/src/controllers/user.controller.ts | 36 +++++++++++++++++++ backend/src/middlewares/index.ts | 1 + .../user-validator.middleware.ts | 18 ++++++++++ backend/src/routes/auth.routes.ts | 8 +---- backend/src/routes/index.ts | 1 + backend/src/routes/user.routes.ts | 22 ++++++++++++ backend/src/server.ts | 4 ++- backend/src/services/auth.service.ts | 8 ++--- backend/src/services/user.service.ts | 13 +++++++ 11 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 backend/src/controllers/user.controller.ts create mode 100644 backend/src/middlewares/request-validators/user-validator.middleware.ts create mode 100644 backend/src/routes/user.routes.ts diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index 28cf415..8a5710c 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -8,7 +8,6 @@ import { errorInvalidRefreshToken, errorInvalidTokenSubject, errorRefreshTokenNotPresent, - errorUnauthorized, handleError, rejectRequestFromMeetError } from '../models/error.model.js'; @@ -103,16 +102,3 @@ export const refreshToken = async (req: Request, res: Response) => { handleError(res, error, 'refreshing token'); } }; - -export const getProfile = (req: Request, res: Response) => { - const user = req.session?.user; - - if (!user) { - const error = errorUnauthorized(); - return rejectRequestFromMeetError(res, error); - } - - const userService = container.get(UserService); - const userDTO = userService.convertToDTO(user); - return res.status(200).json(userDTO); -}; diff --git a/backend/src/controllers/index.ts b/backend/src/controllers/index.ts index b5b0496..940361c 100644 --- a/backend/src/controllers/index.ts +++ b/backend/src/controllers/index.ts @@ -1,4 +1,5 @@ export * from './auth.controller.js'; +export * from './user.controller.js'; export * from './room.controller.js'; export * from './meeting.controller.js'; export * from './participant.controller.js'; diff --git a/backend/src/controllers/user.controller.ts b/backend/src/controllers/user.controller.ts new file mode 100644 index 0000000..1647acc --- /dev/null +++ b/backend/src/controllers/user.controller.ts @@ -0,0 +1,36 @@ +import { Request, Response } from 'express'; +import { container } from '../config/index.js'; +import { errorUnauthorized, handleError, rejectRequestFromMeetError } from '../models/error.model.js'; +import { UserService } from '../services/index.js'; + +export const getProfile = (req: Request, res: Response) => { + const user = req.session?.user; + + if (!user) { + const error = errorUnauthorized(); + return rejectRequestFromMeetError(res, error); + } + + const userService = container.get(UserService); + const userDTO = userService.convertToDTO(user); + return res.status(200).json(userDTO); +}; + +export const changePassword = async (req: Request, res: Response) => { + const user = req.session?.user; + + if (!user) { + const error = errorUnauthorized(); + return rejectRequestFromMeetError(res, error); + } + + const { newPassword } = req.body as { newPassword: string }; + + try { + const userService = container.get(UserService); + await userService.changePassword(user.username, newPassword); + return res.status(200).json({ message: 'Password changed successfully.' }); + } catch (error) { + handleError(res, error, 'changing password'); + } +}; diff --git a/backend/src/middlewares/index.ts b/backend/src/middlewares/index.ts index 25da0c3..a4b159e 100644 --- a/backend/src/middlewares/index.ts +++ b/backend/src/middlewares/index.ts @@ -5,6 +5,7 @@ export * from './participant.middleware.js'; export * from './recording.middleware.js'; export * from './request-validators/auth-validator.middleware.js'; +export * from './request-validators/user-validator.middleware.js'; export * from './request-validators/room-validator.middleware.js'; export * from './request-validators/participant-validator.middleware.js'; export * from './request-validators/recording-validator.middleware.js'; diff --git a/backend/src/middlewares/request-validators/user-validator.middleware.ts b/backend/src/middlewares/request-validators/user-validator.middleware.ts new file mode 100644 index 0000000..be3db47 --- /dev/null +++ b/backend/src/middlewares/request-validators/user-validator.middleware.ts @@ -0,0 +1,18 @@ +import { NextFunction, Request, Response } from 'express'; +import { z } from 'zod'; +import { rejectUnprocessableRequest } from '../../models/error.model.js'; + +const ChangePasswordRequestSchema = z.object({ + newPassword: z.string().min(4, 'New password must be at least 4 characters long') +}); + +export const validateChangePasswordRequest = (req: Request, res: Response, next: NextFunction) => { + const { success, error, data } = ChangePasswordRequestSchema.safeParse(req.body); + + if (!success) { + return rejectUnprocessableRequest(res, error); + } + + req.body = data; + next(); +}; diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts index 2ea225d..b093f30 100644 --- a/backend/src/routes/auth.routes.ts +++ b/backend/src/routes/auth.routes.ts @@ -1,8 +1,7 @@ -import { UserRole } from '@typings-ce'; import bodyParser from 'body-parser'; import { Router } from 'express'; import * as authCtrl from '../controllers/auth.controller.js'; -import { tokenAndRoleValidator, validateLoginRequest, withAuth, withLoginLimiter } from '../middlewares/index.js'; +import { validateLoginRequest, withLoginLimiter } from '../middlewares/index.js'; export const authRouter = Router(); authRouter.use(bodyParser.urlencoded({ extended: true })); @@ -12,8 +11,3 @@ authRouter.use(bodyParser.json()); authRouter.post('/login', validateLoginRequest, withLoginLimiter, authCtrl.login); authRouter.post('/logout', authCtrl.logout); authRouter.post('/refresh', authCtrl.refreshToken); -authRouter.get( - '/profile', - withAuth(tokenAndRoleValidator(UserRole.ADMIN), tokenAndRoleValidator(UserRole.USER)), - authCtrl.getProfile -); diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 6c23332..f2abe31 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -1,5 +1,6 @@ export * from './global-preferences.routes.js'; export * from './auth.routes.js'; +export * from './user.routes.js'; export * from './room.routes.js'; export * from './meeting.routes.js'; export * from './participant.routes.js'; diff --git a/backend/src/routes/user.routes.ts b/backend/src/routes/user.routes.ts new file mode 100644 index 0000000..de1465c --- /dev/null +++ b/backend/src/routes/user.routes.ts @@ -0,0 +1,22 @@ +import { UserRole } from '@typings-ce'; +import bodyParser from 'body-parser'; +import { Router } from 'express'; +import * as userCtrl from '../controllers/user.controller.js'; +import { tokenAndRoleValidator, validateChangePasswordRequest, withAuth } from '../middlewares/index.js'; + +export const userRouter = Router(); +userRouter.use(bodyParser.urlencoded({ extended: true })); +userRouter.use(bodyParser.json()); + +// Users Routes +userRouter.get( + '/profile', + withAuth(tokenAndRoleValidator(UserRole.ADMIN), tokenAndRoleValidator(UserRole.USER)), + userCtrl.getProfile +); +userRouter.post( + '/change-password', + withAuth(tokenAndRoleValidator(UserRole.ADMIN), tokenAndRoleValidator(UserRole.USER)), + validateChangePasswordRequest, + userCtrl.changePassword +); diff --git a/backend/src/server.ts b/backend/src/server.ts index 1e0097b..8468ce3 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -15,7 +15,8 @@ import { livekitWebhookRouter, preferencesRouter, recordingRouter, - roomRouter + roomRouter, + userRouter } from './routes/index.js'; import { frontendDirectoryPath, @@ -56,6 +57,7 @@ const createApp = () => { res.sendFile(internalApiHtmlFilePath) ); app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/auth`, authRouter); + app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/users`, userRouter); app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/rooms`, internalRoomRouter); app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/meetings`, internalMeetingRouter); app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/participants`, internalParticipantRouter); diff --git a/backend/src/services/auth.service.ts b/backend/src/services/auth.service.ts index d5be7a7..57aa40b 100644 --- a/backend/src/services/auth.service.ts +++ b/backend/src/services/auth.service.ts @@ -1,15 +1,11 @@ import { User } from '@typings-ce'; import { inject, injectable } from 'inversify'; import { PasswordHelper } from '../helpers/index.js'; -import { LoggerService, MeetStorageService, UserService } from './index.js'; +import { UserService } from './index.js'; @injectable() export class AuthService { - constructor( - @inject(LoggerService) protected logger: LoggerService, - @inject(UserService) protected userService: UserService, - @inject(MeetStorageService) protected globalPrefService: MeetStorageService - ) {} + constructor(@inject(UserService) protected userService: UserService) {} async authenticate(username: string, password: string): Promise { const user = await this.userService.getUser(username); diff --git a/backend/src/services/user.service.ts b/backend/src/services/user.service.ts index 9225687..1cce99e 100644 --- a/backend/src/services/user.service.ts +++ b/backend/src/services/user.service.ts @@ -1,6 +1,8 @@ import { User, UserDTO, UserRole } from '@typings-ce'; import { inject, injectable } from 'inversify'; import INTERNAL_CONFIG from '../config/internal-config.js'; +import { PasswordHelper } from '../helpers/password.helper.js'; +import { internalError } from '../models/error.model.js'; import { MeetStorageService } from './index.js'; @injectable() @@ -27,6 +29,17 @@ export class UserService { }; } + async changePassword(username: string, newPassword: string) { + const user = await this.storageService.getUser(username); + + if (!user) { + throw internalError(`getting user ${username} for password change`); + } + + user.passwordHash = await PasswordHelper.hashPassword(newPassword); + await this.storageService.saveUser(user); + } + // Convert user to UserDTO to remove sensitive information convertToDTO(user: User): UserDTO { const { passwordHash, ...userDTO } = user;