backend: implement password change functionality and move user profile retrieval to users endpoints

This commit is contained in:
juancarmore 2025-06-09 21:42:47 +02:00
parent 927035c1ea
commit a64f48bc5b
11 changed files with 98 additions and 28 deletions

View File

@ -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);
};

View File

@ -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';

View File

@ -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');
}
};

View File

@ -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';

View File

@ -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();
};

View File

@ -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
);

View File

@ -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';

View File

@ -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
);

View File

@ -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);

View File

@ -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<User | null> {
const user = await this.userService.getUser(username);

View File

@ -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;