Create enum for user role
This commit is contained in:
parent
8814658142
commit
7ab5923ebb
7 changed files with 152 additions and 37 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { JwtPayload } from "jsonwebtoken";
|
||||
import { UserRole } from "../enums/UserRole.js";
|
||||
|
||||
/**
|
||||
* Shape of the payload stored inside JWT tokens.
|
||||
|
|
@ -6,5 +7,5 @@ import { JwtPayload } from "jsonwebtoken";
|
|||
*/
|
||||
export interface AuthPayload extends JwtPayload {
|
||||
id: string;
|
||||
role?: string; // Add role to the JWT payload
|
||||
role?: UserRole;
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { AbstractDto } from "./AbstractDto.js";
|
||||
import { UserRole } from "../enums/UserRole.js";
|
||||
|
||||
export class UserDto extends AbstractDto {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
userName!: string;
|
||||
email!: string;
|
||||
role?: string;
|
||||
role?: UserRole;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { Entity, Column } from "typeorm";
|
||||
import { AbstractEntity } from "./AbstractEntity.js";
|
||||
import { UserRole } from "../enums/UserRole.js";
|
||||
|
||||
/**
|
||||
* Entity describing a user
|
||||
|
|
@ -15,12 +16,16 @@ export class UserEntity extends AbstractEntity {
|
|||
@Column({ nullable: false })
|
||||
password!: string;
|
||||
|
||||
@Column({ nullable: true, name: "first_name"})
|
||||
@Column({ nullable: true, name: "first_name" })
|
||||
firstName?: string;
|
||||
|
||||
@Column({ nullable: true, name: "last_name"})
|
||||
@Column({ nullable: true, name: "last_name" })
|
||||
lastName?: string;
|
||||
|
||||
@Column({ default: "user" })
|
||||
role!: string;
|
||||
@Column({
|
||||
type: "enum",
|
||||
enum: UserRole,
|
||||
default: UserRole.USER,
|
||||
})
|
||||
role!: UserRole;
|
||||
}
|
||||
60
src/enums/UserRole.ts
Normal file
60
src/enums/UserRole.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* User roles enum
|
||||
* Used across backend, API, and frontend
|
||||
* We need to use a plain enum here for typeORM which is why the enum may differ from the frontend implementation
|
||||
* However, we must ensure that the content of both enums is identical!
|
||||
*/
|
||||
export enum UserRole {
|
||||
USER = "user",
|
||||
ADMIN = "admin",
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper functions for UserRole
|
||||
*/
|
||||
export const UserRoleHelper = {
|
||||
/**
|
||||
* Get all role values as array
|
||||
*/
|
||||
getAllRoles(): UserRole[] {
|
||||
return Object.values(UserRole);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a string is a valid role
|
||||
*/
|
||||
isValidRole(value: string): value is UserRole {
|
||||
return Object.values(UserRole).includes(value as UserRole);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get role from string with validation
|
||||
*/
|
||||
fromString(value: string): UserRole {
|
||||
if (this.isValidRole(value)) {
|
||||
return value as UserRole;
|
||||
}
|
||||
throw new Error(`Invalid role: ${value}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get display name for role (German)
|
||||
*/
|
||||
getDisplayName(role: UserRole): string {
|
||||
const displayNames: Record<UserRole, string> = {
|
||||
[UserRole.USER]: "Benutzer",
|
||||
[UserRole.ADMIN]: "Administrator",
|
||||
};
|
||||
return displayNames[role];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all roles with display names for dropdowns
|
||||
*/
|
||||
getRoleOptions(): Array<{ value: UserRole; label: string }> {
|
||||
return this.getAllRoles().map((role) => ({
|
||||
value: role,
|
||||
label: this.getDisplayName(role),
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
|
@ -7,6 +7,7 @@ import {UserDtoEntityMapper} from "../mappers/UserDtoEntityMapper.js";
|
|||
import {UUID} from "crypto";
|
||||
import {ChangeUserPasswordRequest} from "../dtos/ChangeUserPasswordRequest.js";
|
||||
import {UserEntity} from "../entities/UserEntity.js";
|
||||
import {UserRole, UserRoleHelper} from "../enums/UserRole.js";
|
||||
|
||||
/**
|
||||
* Controls all user specific actions
|
||||
|
|
@ -34,13 +35,22 @@ export class UserHandler {
|
|||
throw new ValidationError("Password is required");
|
||||
}
|
||||
|
||||
|
||||
// user name must be unique
|
||||
const existingUser = await this.userRepository.findByUserName(userName);
|
||||
if (existingUser) {
|
||||
throw new ConflictError("User with this user name already exists");
|
||||
}
|
||||
|
||||
// Validate role if provided
|
||||
if (requestData.userData.role && !UserRoleHelper.isValidRole(requestData.userData.role)) {
|
||||
throw new ValidationError(`Invalid role: ${requestData.userData.role}`);
|
||||
}
|
||||
|
||||
// Set default role if not provided
|
||||
if (!requestData.userData.role) {
|
||||
requestData.userData.role = UserRole.USER;
|
||||
}
|
||||
|
||||
const userEntity = this.mapper.toEntity(requestData.userData);
|
||||
userEntity.password = await encrypt.encryptpass(password);
|
||||
|
||||
|
|
@ -72,6 +82,12 @@ export class UserHandler {
|
|||
// user does not exist yet -> Wrong method is used
|
||||
throw new ValidationError("Cannot save user without valid userId. Use user/create to create a new user");
|
||||
}
|
||||
|
||||
// Validate role if provided
|
||||
if (dto.role && !UserRoleHelper.isValidRole(dto.role)) {
|
||||
throw new ValidationError(`Invalid role: ${dto.role}`);
|
||||
}
|
||||
|
||||
// First: Load current version of user from database
|
||||
const entity = await this.userRepository.findById(userId);
|
||||
if (!entity) {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import { NextFunction, Request, Response } from "express";
|
||||
import { ForbiddenError } from "../errors/httpErrors.js";
|
||||
import { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js";
|
||||
import { UserRole } from "../enums/UserRole.js";
|
||||
|
||||
/**
|
||||
* Middleware to check if the current user has one of the required roles
|
||||
* Must be used after the authentication middleware
|
||||
*
|
||||
* @param allowedRoles Array of role names that are allowed to access the route
|
||||
* @param allowedRoles Array of roles that are allowed to access the route
|
||||
* @returns Express middleware function
|
||||
*
|
||||
* @example
|
||||
* router.get("/admin-only", requireRole(["admin"]), asyncHandler(async (req, res) => { ... }));
|
||||
* router.post("/admin-or-moderator", requireRole(["admin", "moderator"]), asyncHandler(async (req, res) => { ... }));
|
||||
* router.get("/admin-only", requireRole([UserRole.ADMIN]), asyncHandler(async (req, res) => { ... }));
|
||||
* router.post("/admin-or-moderator", requireRole([UserRole.ADMIN, UserRole.MODERATOR]), asyncHandler(async (req, res) => { ... }));
|
||||
*/
|
||||
export const requireRole = (allowedRoles: string[]) => {
|
||||
export const requireRole = (allowedRoles: UserRole[]) => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
// Check if user is authenticated
|
||||
if (!req.currentUser) {
|
||||
|
|
@ -41,7 +42,7 @@ export const requireRole = (allowedRoles: string[]) => {
|
|||
* @example
|
||||
* router.get("/admin-panel", requireAdmin, asyncHandler(async (req, res) => { ... }));
|
||||
*/
|
||||
export const requireAdmin = requireRole(["admin"]);
|
||||
export const requireAdmin = requireRole([UserRole.ADMIN]);
|
||||
|
||||
/**
|
||||
* Middleware to check if user is either an admin or the owner of the resource
|
||||
|
|
@ -51,14 +52,7 @@ export const requireAdmin = requireRole(["admin"]);
|
|||
* @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
|
||||
|
|
@ -74,7 +68,7 @@ export const requireAdminOrOwner = (
|
|||
const targetUserId = getUserIdFromRequest(req);
|
||||
|
||||
// Allow if user is admin
|
||||
if (currentUserRole === "admin") {
|
||||
if (currentUserRole === UserRole.ADMIN) {
|
||||
return next();
|
||||
}
|
||||
|
||||
|
|
@ -93,11 +87,5 @@ export const requireAdminOrOwner = (
|
|||
/**
|
||||
* 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);
|
||||
44
src/migrations/1701234567890-ConvertRoleToEnum.ts
Normal file
44
src/migrations/1701234567890-ConvertRoleToEnum.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Migration to convert role column from varchar to enum
|
||||
*
|
||||
* File name should be: TIMESTAMP-ConvertRoleToEnum.ts
|
||||
* Example: 1701234567890-ConvertRoleToEnum.ts
|
||||
*
|
||||
* Place in: src/migrations/
|
||||
*/
|
||||
export class ConvertRoleToEnum1701234567890 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE user_role_enum AS ENUM ('user', 'admin');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "user"
|
||||
ALTER COLUMN "role" TYPE user_role_enum
|
||||
USING "role"::user_role_enum;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "user"
|
||||
ALTER COLUMN "role" SET DEFAULT 'user';
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "user"
|
||||
ALTER COLUMN "role" TYPE varchar
|
||||
USING "role"::text;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DROP TYPE IF EXISTS user_role_enum;
|
||||
`);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue