Implement change password request. Adapt authorizationMiddleware to allow for adminOrOwner access to resources
This commit is contained in:
parent
555dacfaf5
commit
2a6153002c
4 changed files with 180 additions and 59 deletions
|
|
@ -5,24 +5,24 @@ meta {
|
|||
}
|
||||
|
||||
post {
|
||||
url: https://localhost:4000/user/update
|
||||
url: http://localhost:4000/user/update
|
||||
body: json
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc2MzA2NTI0MCwiZXhwIjoxNzYzMTUxNjQwfQ.e7v1JnlNHm7zwSzumlZIy2Dxfojqsxk55aYC9UA7BkE
|
||||
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc2MzQ5MDcxMywiZXhwIjoxNzYzNTc3MTEzfQ.h8ta-4tVhR7EskZDBLtcFTQ7QllV-PfC09Y0DLjYJa4
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"id": "a447e434-421f-42ba-80de-34d15c2f5a6c",
|
||||
"userName": "admin",
|
||||
"email": "anika@raemer.net",
|
||||
"firstName": "Anika",
|
||||
"lastName": "Rämer",
|
||||
"role": "admin"
|
||||
}
|
||||
"id": "9c913747-ba57-4b12-87d0-3339f4a8117c",
|
||||
"userName": "test3",
|
||||
"email": "test@raemer.net",
|
||||
"firstName": "Thea",
|
||||
"lastName": "Test",
|
||||
"role": "user"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
|
|
|
|||
|
|
@ -4,39 +4,47 @@ import { CreateUserRequest } from "../dtos/CreateUserRequest.js";
|
|||
import { UserRepository } from "../repositories/UserRepository.js";
|
||||
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
|
||||
import { asyncHandler } from "../utils/asyncHandler.js";
|
||||
import {CreateUserResponse} from "../dtos/CreateUserResponse.js";
|
||||
import {UserDto} from "../dtos/UserDto.js";
|
||||
import {requireAdmin} from "../middleware/authorizationMiddleware.js";
|
||||
import {UserListResponse} from "../dtos/UserListResponse.js";
|
||||
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
||||
import { CreateUserResponse } from "../dtos/CreateUserResponse.js";
|
||||
import { UserDto } from "../dtos/UserDto.js";
|
||||
import {
|
||||
requireAdmin,
|
||||
requireAdminOrOwner,
|
||||
requireAdminOrSelf
|
||||
} from "../middleware/authorizationMiddleware.js";
|
||||
import { UserListResponse } from "../dtos/UserListResponse.js";
|
||||
import { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js";
|
||||
import { InternalServerError, NotFoundError } from "../errors/httpErrors.js";
|
||||
import { ChangeUserPasswordRequest } from "../dtos/ChangeUserPasswordRequest.js";
|
||||
|
||||
export const userBasicRoute = "/user";
|
||||
|
||||
/**
|
||||
* Handles all user related routes
|
||||
*/
|
||||
const router = Router();
|
||||
|
||||
router.use((req, res, next) => {
|
||||
console.log(`Incoming request: ${req.method} ${req.path}`);
|
||||
next();
|
||||
});
|
||||
|
||||
// Inject repo + mapper here
|
||||
const handler
|
||||
= new UserHandler(new UserRepository(), new UserDtoEntityMapper());
|
||||
const handler = new UserHandler(new UserRepository(), new UserDtoEntityMapper());
|
||||
|
||||
/**
|
||||
* Create a new user
|
||||
* Create a new user (admin only)
|
||||
* Consumes CreateUserRequest
|
||||
* Responds with UserDto
|
||||
*/
|
||||
router.post(
|
||||
"/create",
|
||||
asyncHandler(async (req, res) => {
|
||||
const request: CreateUserRequest = req.body;
|
||||
const user : UserDto = await handler.createUser(request);
|
||||
const response : CreateUserResponse = { userData: user };
|
||||
res.status(201).json(response);
|
||||
})
|
||||
"/create",
|
||||
requireAdmin,
|
||||
asyncHandler(async (req, res) => {
|
||||
const request: CreateUserRequest = req.body;
|
||||
const user: UserDto = await handler.createUser(request);
|
||||
const response: CreateUserResponse = { userData: user };
|
||||
res.status(HttpStatusCode.CREATED).json(response);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
@ -44,25 +52,36 @@ router.post(
|
|||
* Consumes UserDto
|
||||
* Responds with UserDto
|
||||
* Does not allow for password change. Use change-password instead
|
||||
*
|
||||
* Users can update their own data, admins can update any user
|
||||
*/
|
||||
router.post(
|
||||
"/update",
|
||||
requireAdminOrSelf, // Checks req.body.id or req.body.userId
|
||||
asyncHandler(async (req, res) => {
|
||||
const dto : UserDto = req.body;
|
||||
const dto: UserDto = req.body;
|
||||
const response = await handler.updateUserData(dto);
|
||||
res.status(HttpStatusCode.CREATED).json(response);
|
||||
res.status(HttpStatusCode.OK).json(response);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Update password of existing user
|
||||
* 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(
|
||||
"/change-password",
|
||||
requireAdminOrOwner(req => req.body.userId),
|
||||
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(
|
||||
"/all",
|
||||
//requireAdmin,
|
||||
requireAdmin,
|
||||
asyncHandler(async (req, res) => {
|
||||
const id = req.currentUser?.id;
|
||||
const users = await handler.getAllUsers();
|
||||
const response : UserListResponse = {valueList: users}
|
||||
const response: UserListResponse = { valueList: users };
|
||||
res.status(HttpStatusCode.OK).json(response);
|
||||
})
|
||||
);
|
||||
|
|
@ -85,15 +103,33 @@ router.get(
|
|||
* Get user data for current user
|
||||
* Responds with UserDto
|
||||
*/
|
||||
router.get("/me",
|
||||
asyncHandler(async (req, res) => {
|
||||
|
||||
const id = req.currentUser?.id;
|
||||
if(id){
|
||||
const responseDto = await handler.getUserById(id);
|
||||
res.status(HttpStatusCode.OK).json(responseDto);
|
||||
}
|
||||
})
|
||||
router.get(
|
||||
"/me",
|
||||
asyncHandler(async (req, res) => {
|
||||
const id = req.currentUser?.id;
|
||||
if (id) {
|
||||
const responseDto = await handler.getUserById(id);
|
||||
res.status(HttpStatusCode.OK).json(responseDto);
|
||||
} else {
|
||||
throw new NotFoundError("There is no user id for current session!");
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
/**
|
||||
* 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;
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import { ValidationError, ConflictError, NotFoundError } from "../errors/httpErrors.js";
|
||||
import { CreateUserRequest } from "../dtos/CreateUserRequest.js";
|
||||
import { UserDto } from "../dtos/UserDto.js";
|
||||
import { encrypt } from "../utils/encryptionUtils.js";
|
||||
import { UserRepository } from "../repositories/UserRepository.js";
|
||||
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
|
||||
import { UUID } from "crypto";
|
||||
import {ConflictError, NotFoundError, ValidationError} from "../errors/httpErrors.js";
|
||||
import {CreateUserRequest} from "../dtos/CreateUserRequest.js";
|
||||
import {UserDto} from "../dtos/UserDto.js";
|
||||
import {encrypt} from "../utils/encryptionUtils.js";
|
||||
import {UserRepository} from "../repositories/UserRepository.js";
|
||||
import {UserDtoEntityMapper} from "../mappers/UserDtoEntityMapper.js";
|
||||
import {UUID} from "crypto";
|
||||
import {ChangeUserPasswordRequest} from "../dtos/ChangeUserPasswordRequest.js";
|
||||
import {UserEntity} from "../entities/UserEntity.js";
|
||||
|
||||
/**
|
||||
* Controls all user specific actions
|
||||
|
|
@ -17,17 +19,17 @@ export class UserHandler {
|
|||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
async createUser(dto: CreateUserRequest): Promise<UserDto> {
|
||||
async createUser(requestData: CreateUserRequest): Promise<UserDto> {
|
||||
// check mandatory fields
|
||||
if(!dto.userData){
|
||||
if(!requestData.userData){
|
||||
throw new ValidationError("User data is required")
|
||||
}
|
||||
this.validateUserData(dto.userData);
|
||||
const userName = dto.userData.userName;
|
||||
const password = dto.password;
|
||||
this.validateUserData(requestData.userData);
|
||||
const userName = requestData.userData.userName;
|
||||
const password = requestData.password;
|
||||
if(!password || (password && password.length == 0)){
|
||||
throw new ValidationError("Password is required");
|
||||
}
|
||||
|
|
@ -39,7 +41,7 @@ export class UserHandler {
|
|||
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);
|
||||
|
||||
const savedUser = await this.userRepository.create(userEntity);
|
||||
|
|
@ -73,7 +75,7 @@ export class UserHandler {
|
|||
// First: Load current version of user from database
|
||||
const entity = await this.userRepository.findById(userId);
|
||||
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
|
||||
this.mapper.mergeDtoIntoEntity(dto, entity);
|
||||
|
|
@ -82,6 +84,31 @@ export class UserHandler {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextFunction, Request, Response } from "express";
|
||||
import { ForbiddenError } from "../errors/httpErrors.js";
|
||||
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
||||
import { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js";
|
||||
|
||||
/**
|
||||
* Middleware to check if the current user has one of the required roles
|
||||
|
|
@ -22,7 +22,6 @@ export const requireRole = (allowedRoles: string[]) => {
|
|||
|
||||
// Get user's role from the auth payload
|
||||
const userRole = req.currentUser.role;
|
||||
console.log("userRole", userRole);
|
||||
|
||||
// Check if user has one of the allowed roles
|
||||
if (!userRole || !allowedRoles.includes(userRole)) {
|
||||
|
|
@ -42,4 +41,63 @@ export const requireRole = (allowedRoles: string[]) => {
|
|||
* @example
|
||||
* 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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue