Implement change password request. Adapt authorizationMiddleware to allow for adminOrOwner access to resources

This commit is contained in:
araemer 2025-11-19 20:55:13 +01:00
parent 555dacfaf5
commit 2a6153002c
4 changed files with 180 additions and 59 deletions

View file

@ -5,23 +5,23 @@ meta {
} }
post { post {
url: https://localhost:4000/user/update url: http://localhost:4000/user/update
body: json body: json
auth: bearer auth: bearer
} }
auth:bearer { auth:bearer {
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc2MzA2NTI0MCwiZXhwIjoxNzYzMTUxNjQwfQ.e7v1JnlNHm7zwSzumlZIy2Dxfojqsxk55aYC9UA7BkE token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc2MzQ5MDcxMywiZXhwIjoxNzYzNTc3MTEzfQ.h8ta-4tVhR7EskZDBLtcFTQ7QllV-PfC09Y0DLjYJa4
} }
body:json { body:json {
{ {
"id": "a447e434-421f-42ba-80de-34d15c2f5a6c", "id": "9c913747-ba57-4b12-87d0-3339f4a8117c",
"userName": "admin", "userName": "test3",
"email": "anika@raemer.net", "email": "test@raemer.net",
"firstName": "Anika", "firstName": "Thea",
"lastName": "Rämer", "lastName": "Test",
"role": "admin" "role": "user"
} }
} }

View file

@ -6,36 +6,44 @@ import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
import { asyncHandler } from "../utils/asyncHandler.js"; import { asyncHandler } from "../utils/asyncHandler.js";
import { CreateUserResponse } from "../dtos/CreateUserResponse.js"; import { CreateUserResponse } from "../dtos/CreateUserResponse.js";
import { UserDto } from "../dtos/UserDto.js"; import { UserDto } from "../dtos/UserDto.js";
import {requireAdmin} from "../middleware/authorizationMiddleware.js"; import {
requireAdmin,
requireAdminOrOwner,
requireAdminOrSelf
} from "../middleware/authorizationMiddleware.js";
import { UserListResponse } from "../dtos/UserListResponse.js"; import { UserListResponse } from "../dtos/UserListResponse.js";
import { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js"; import { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js";
import { InternalServerError, NotFoundError } from "../errors/httpErrors.js";
import { ChangeUserPasswordRequest } from "../dtos/ChangeUserPasswordRequest.js";
export const userBasicRoute = "/user"; export const userBasicRoute = "/user";
/** /**
* Handles all user related routes * Handles all user related routes
*/ */
const router = Router(); const router = Router();
router.use((req, res, next) => { router.use((req, res, next) => {
console.log(`Incoming request: ${req.method} ${req.path}`); console.log(`Incoming request: ${req.method} ${req.path}`);
next(); next();
}); });
// Inject repo + mapper here // Inject repo + mapper here
const handler const handler = new UserHandler(new UserRepository(), new UserDtoEntityMapper());
= new UserHandler(new UserRepository(), new UserDtoEntityMapper());
/** /**
* Create a new user * Create a new user (admin only)
* Consumes CreateUserRequest * Consumes CreateUserRequest
* Responds with UserDto * Responds with UserDto
*/ */
router.post( router.post(
"/create", "/create",
requireAdmin,
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
const request: CreateUserRequest = req.body; const request: CreateUserRequest = req.body;
const user: UserDto = await handler.createUser(request); const user: UserDto = await handler.createUser(request);
const response: CreateUserResponse = { userData: user }; const response: CreateUserResponse = { userData: user };
res.status(201).json(response); res.status(HttpStatusCode.CREATED).json(response);
}) })
); );
@ -44,25 +52,36 @@ router.post(
* Consumes UserDto * Consumes UserDto
* Responds with UserDto * Responds with UserDto
* Does not allow for password change. Use change-password instead * Does not allow for password change. Use change-password instead
*
* Users can update their own data, admins can update any user
*/ */
router.post( router.post(
"/update", "/update",
requireAdminOrSelf, // Checks req.body.id or req.body.userId
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
const dto: UserDto = req.body; const dto: UserDto = req.body;
const response = await handler.updateUserData(dto); const response = await handler.updateUserData(dto);
res.status(HttpStatusCode.CREATED).json(response); res.status(HttpStatusCode.OK).json(response);
}) })
); );
/** /**
* Update password of existing user * Update password of existing user
* Consumes ChangeUserPasswordRequest * Consumes ChangeUserPasswordRequest
* Responds with code 201 indicating success * Responds with code 200 indicating success
*
* Users can change their own password, admins can change any password
*/ */
router.post( router.post(
"/change-password", "/change-password",
requireAdminOrOwner(req => req.body.userId),
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
throw Error("not implemented!"); const requestData: ChangeUserPasswordRequest = req.body;
const success = await handler.changePassword(requestData);
if (!success) {
throw new InternalServerError("Failed to change password");
}
res.status(HttpStatusCode.OK).json({ message: "Password changed successfully" });
}) })
); );
@ -72,11 +91,10 @@ router.post(
*/ */
router.get( router.get(
"/all", "/all",
//requireAdmin, requireAdmin,
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
const id = req.currentUser?.id;
const users = await handler.getAllUsers(); const users = await handler.getAllUsers();
const response : UserListResponse = {valueList: users} const response: UserListResponse = { valueList: users };
res.status(HttpStatusCode.OK).json(response); res.status(HttpStatusCode.OK).json(response);
}) })
); );
@ -85,15 +103,33 @@ router.get(
* Get user data for current user * Get user data for current user
* Responds with UserDto * Responds with UserDto
*/ */
router.get("/me", router.get(
"/me",
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
const id = req.currentUser?.id; const id = req.currentUser?.id;
if (id) { if (id) {
const responseDto = await handler.getUserById(id); const responseDto = await handler.getUserById(id);
res.status(HttpStatusCode.OK).json(responseDto); res.status(HttpStatusCode.OK).json(responseDto);
} else {
throw new NotFoundError("There is no user id for current session!");
} }
}) })
); );
/**
* Get specific user by ID
* Responds with UserDto
*
* Users can get their own data, admins can get any user's data
*/
router.get(
"/:userId",
requireAdminOrOwner(req => req.params.userId),
asyncHandler(async (req, res) => {
const userId = req.params.userId;
const responseDto = await handler.getUserById(userId);
res.status(HttpStatusCode.OK).json(responseDto);
})
);
export default router; export default router;

View file

@ -1,10 +1,12 @@
import { ValidationError, ConflictError, NotFoundError } from "../errors/httpErrors.js"; import {ConflictError, NotFoundError, ValidationError} from "../errors/httpErrors.js";
import {CreateUserRequest} from "../dtos/CreateUserRequest.js"; import {CreateUserRequest} from "../dtos/CreateUserRequest.js";
import {UserDto} from "../dtos/UserDto.js"; import {UserDto} from "../dtos/UserDto.js";
import {encrypt} from "../utils/encryptionUtils.js"; import {encrypt} from "../utils/encryptionUtils.js";
import {UserRepository} from "../repositories/UserRepository.js"; import {UserRepository} from "../repositories/UserRepository.js";
import {UserDtoEntityMapper} from "../mappers/UserDtoEntityMapper.js"; import {UserDtoEntityMapper} from "../mappers/UserDtoEntityMapper.js";
import {UUID} from "crypto"; import {UUID} from "crypto";
import {ChangeUserPasswordRequest} from "../dtos/ChangeUserPasswordRequest.js";
import {UserEntity} from "../entities/UserEntity.js";
/** /**
* Controls all user specific actions * Controls all user specific actions
@ -17,17 +19,17 @@ export class UserHandler {
/** /**
* Create a new user * Create a new user
* @param dto CreateUserRequest containing data for the user to add * @param requestData CreateUserRequest containing data for the user to add
* @returns UserDto Data of the user as stored in the database * @returns UserDto Data of the user as stored in the database
*/ */
async createUser(dto: CreateUserRequest): Promise<UserDto> { async createUser(requestData: CreateUserRequest): Promise<UserDto> {
// check mandatory fields // check mandatory fields
if(!dto.userData){ if(!requestData.userData){
throw new ValidationError("User data is required") throw new ValidationError("User data is required")
} }
this.validateUserData(dto.userData); this.validateUserData(requestData.userData);
const userName = dto.userData.userName; const userName = requestData.userData.userName;
const password = dto.password; const password = requestData.password;
if(!password || (password && password.length == 0)){ if(!password || (password && password.length == 0)){
throw new ValidationError("Password is required"); throw new ValidationError("Password is required");
} }
@ -39,7 +41,7 @@ export class UserHandler {
throw new ConflictError("User with this user name already exists"); throw new ConflictError("User with this user name already exists");
} }
const userEntity = this.mapper.toEntity(dto.userData); const userEntity = this.mapper.toEntity(requestData.userData);
userEntity.password = await encrypt.encryptpass(password); userEntity.password = await encrypt.encryptpass(password);
const savedUser = await this.userRepository.create(userEntity); const savedUser = await this.userRepository.create(userEntity);
@ -73,7 +75,7 @@ export class UserHandler {
// First: Load current version of user from database // First: Load current version of user from database
const entity = await this.userRepository.findById(userId); const entity = await this.userRepository.findById(userId);
if (!entity) { if (!entity) {
throw new ValidationError("No user with ID " + userId + " found in database!") throw new ValidationError("No user with ID " + userId + " found in database!");
} }
// merge changes into entity // merge changes into entity
this.mapper.mergeDtoIntoEntity(dto, entity); this.mapper.mergeDtoIntoEntity(dto, entity);
@ -82,6 +84,31 @@ export class UserHandler {
return this.mapper.toDto(savedEntity); return this.mapper.toDto(savedEntity);
} }
/**
* Update password for a specific user
* @param requestData ChangeUserPasswordRequest containing both password and userId
* @return true on success
*/
async changePassword(requestData : ChangeUserPasswordRequest) : Promise<boolean> {
const userId = requestData.userId;
if(!userId) {
throw new ValidationError("User id is required to change password");
}
const password = requestData.password;
if(!password || (password && password.length == 0)){
throw new ValidationError("Password is required");
}
// load user from database
let entity : UserEntity | null = await this.userRepository.findById(userId);
if(!entity) {
throw new ValidationError("No user with ID " + userId + " found in database!");
}
entity.password = password;
const savedEntity = await this.userRepository.update(entity);
return true;
}
/** /**
* Load data of a specific user * Load data of a specific user

View file

@ -22,7 +22,6 @@ export const requireRole = (allowedRoles: string[]) => {
// Get user's role from the auth payload // Get user's role from the auth payload
const userRole = req.currentUser.role; const userRole = req.currentUser.role;
console.log("userRole", userRole);
// Check if user has one of the allowed roles // Check if user has one of the allowed roles
if (!userRole || !allowedRoles.includes(userRole)) { if (!userRole || !allowedRoles.includes(userRole)) {
@ -43,3 +42,62 @@ export const requireRole = (allowedRoles: string[]) => {
* router.get("/admin-panel", requireAdmin, asyncHandler(async (req, res) => { ... })); * router.get("/admin-panel", requireAdmin, asyncHandler(async (req, res) => { ... }));
*/ */
export const requireAdmin = requireRole(["admin"]); export const requireAdmin = requireRole(["admin"]);
/**
* Middleware to check if user is either an admin or the owner of the resource
* Must be used after the authentication middleware
*
* @param getUserIdFromRequest Function to extract the target user ID from the request
* @returns Express middleware function
*
* @example
* // For route params: /user/:userId
* router.put("/:userId", requireAdminOrOwner(req => req.params.userId), asyncHandler(...));
*
* // For request body: { userId: "123" }
* router.post("/update", requireAdminOrOwner(req => req.body.userId), asyncHandler(...));
*
* // For query params: /user?userId=123
* router.get("/profile", requireAdminOrOwner(req => req.query.userId), asyncHandler(...));
*/
export const requireAdminOrOwner = (
getUserIdFromRequest: (req: Request) => string | undefined
) => {
return (req: Request, res: Response, next: NextFunction) => {
// Check if user is authenticated
if (!req.currentUser) {
return res.status(HttpStatusCode.UNAUTHORIZED).json({ message: "Unauthorized" });
}
const currentUserId = req.currentUser.id;
const currentUserRole = req.currentUser.role;
const targetUserId = getUserIdFromRequest(req);
// Allow if user is admin
if (currentUserRole === "admin") {
return next();
}
// Allow if user is accessing their own resource
if (targetUserId && currentUserId === targetUserId) {
return next();
}
// Deny access
throw new ForbiddenError(
"Access denied. You can only modify your own data."
);
};
};
/**
* Middleware for routes where the user ID is in the request body
* Common for update operations
*
* @example
* router.post("/update", requireAdminOrSelf, asyncHandler(async (req, res) => {
* const dto: UserDto = req.body;
* // dto.id is checked against current user
* }));
*/
export const requireAdminOrSelf = requireAdminOrOwner(req => req.body.id || req.body.userId);