feat(role-guards): added role-guards group and added role check to controllers

This commit is contained in:
Matthieu Haineault 2025-11-05 14:27:54 -05:00
parent 02ebb23d7a
commit 1a0532846f
9 changed files with 146 additions and 132 deletions

View File

@ -37,9 +37,9 @@ export class RolesGuard implements CanActivate {
* or returns `false` if the user is not authenticated.
*/
canActivate(ctx: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<Roles[]>(
const requiredRoles = this.reflector.getAllAndOverride<Roles[]>(
ROLES_KEY,
ctx.getHandler(),
[ctx.getHandler(), ctx.getClass()],
);
//for "deny-by-default" when role is wrong or unavailable
if (!requiredRoles || requiredRoles.length === 0) {

View File

@ -0,0 +1,15 @@
import { Roles as RoleEnum } from ".prisma/client";
export const GLOBAL_CONTROLLER_ROLES: readonly RoleEnum[] = [
RoleEnum.EMPLOYEE,
RoleEnum.ACCOUNTING,
RoleEnum.HR,
RoleEnum.SUPERVISOR,
RoleEnum.ADMIN,
];
export const MANAGER_ROLES: readonly RoleEnum[] = [
RoleEnum.HR,
RoleEnum.SUPERVISOR,
RoleEnum.ADMIN,
]

View File

@ -1,13 +1,13 @@
import { Controller, Get, Patch, Param, Body, NotFoundException } from "@nestjs/common";
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes";
import { EmployeeListItemDto } from "src/identity-and-account/employees/dtos/list-employee.dto";
import { EmployeeProfileItemDto } from "src/identity-and-account/employees/dtos/profil-employee.dto";
import { UpdateEmployeeDto } from "src/identity-and-account/employees/dtos/update-employee.dto";
import { EmployeesArchivalService } from "src/identity-and-account/employees/services/employees-archival.service";
import { EmployeesService } from "src/identity-and-account/employees/services/employees.service";
@ApiBearerAuth('access-token')
// @UseGuards()
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
@Controller('employees')
export class EmployeesController {
constructor(
@ -16,15 +16,13 @@ export class EmployeesController {
) { }
@Get('employee-list')
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING)
@RolesAllowed(...MANAGER_ROLES)
findListEmployees(): Promise<EmployeeListItemDto[]> {
return this.employeesService.findListEmployees();
}
@Patch(':email')
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiBearerAuth('access-token')
@RolesAllowed(...MANAGER_ROLES)
async updateOrArchiveOrRestore(@Param('email') email: string, @Body() dto: UpdateEmployeeDto,) {
// if last_work_day is set => archive the employee
// else if employee is archived and first_work_day or last_work_day = null => restore
@ -68,10 +66,6 @@ export class EmployeesController {
// }
@Get('profile/:email')
@ApiOperation({summary: 'Find employee profile' })
@ApiParam({ name: 'email', type: String, description: 'Identifier of the employee' })
@ApiResponse({ status: 200, description: 'Employee profile found', type: EmployeeProfileItemDto })
@ApiResponse({ status: 400, description: 'Employee profile not found' })
findOneProfile(@Param('email') email: string): Promise<EmployeeProfileItemDto> {
return this.employeesService.findOneProfile(email);
}

View File

@ -6,8 +6,10 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracke
import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service";
import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service";
import { Roles as RoleEnum } from '.prisma/client';
import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes";
@Controller('schedule-presets')
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
export class SchedulePresetsController {
constructor(
private readonly upsertService: SchedulePresetsUpsertService,
@ -17,7 +19,7 @@ export class SchedulePresetsController {
//used to create a schedule preset
@Post('create')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN)
@RolesAllowed(...MANAGER_ROLES)
async createPreset(@Req() req, @Body() dto: SchedulePresetsDto) {
const email = req.user?.email;
return await this.upsertService.createPreset(email, dto);
@ -25,7 +27,7 @@ export class SchedulePresetsController {
//used to update an already existing schedule preset
@Patch('update/:preset_id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN)
@RolesAllowed(...MANAGER_ROLES)
async updatePreset(@Param('preset_id', ParseIntPipe) preset_id: number, @Body() dto: SchedulePresetsUpdateDto) {
return await this.upsertService.updatePreset(preset_id, dto);
}
@ -40,7 +42,7 @@ export class SchedulePresetsController {
//used to show the list of available schedule presets
@Get('find-list')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN)
@RolesAllowed(...MANAGER_ROLES)
async findListById(@Req() req) {
const email = req.user?.email;
return this.getService.getSchedulePresets(email);
@ -48,7 +50,6 @@ export class SchedulePresetsController {
//used to apply a preset to a timesheet
@Post('apply-presets')
@RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN)
async applyPresets(@Req() req, @Query('preset') preset_name: string, @Query('start') start_date: string) {
const email = req.user?.email;
if (!preset_name?.trim()) throw new BadRequestException('Query "preset" is required');

View File

@ -3,17 +3,18 @@ import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift
import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto";
import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service";
import { CreateShiftResult, UpdateShiftResult } from "src/time-and-attendance/utils/type.utils";
import { Roles as RoleEnum } from '.prisma/client';
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes";
@Controller('shift')
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
export class ShiftController {
constructor(
private readonly upsert_service: ShiftsUpsertService,
){}
@Post('create')
@RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN)
createBatch( @Req() req, @Body()dtos: ShiftDto[]): Promise<CreateShiftResult[]> {
const email = req.user?.email;
const list = Array.isArray(dtos) ? dtos : [];
@ -21,10 +22,7 @@ export class ShiftController {
return this.upsert_service.createShifts(email, dtos)
}
//change Body to receive dtos
@Patch('update')
@RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN)
updateBatch( @Body() dtos: UpdateShiftDto[]): Promise<UpdateShiftResult[]>{
const list = Array.isArray(dtos) ? dtos: [];
if(list.length === 0) throw new BadRequestException('Body is missing or invalid (update shifts)');
@ -32,7 +30,6 @@ export class ShiftController {
}
@Delete(':shift_id')
@RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN)
remove(@Param('shift_id') shift_id: number ) {
return this.upsert_service.deleteShift(shift_id);
}

View File

@ -236,11 +236,11 @@ export class ShiftsUpsertService {
// recalculate overtime after update
// return an updated version to display
async updateShifts(dtos: UpdateShiftDto[]): Promise<UpdateShiftResult[]> {
if (!Array.isArray(dtos) || dtos.length === 0) return [];
if (!Array.isArray(dtos) || dtos.length === 0) throw new BadRequestException({ error_code: 'SHIFT_MISSING' });
const updates: UpdateShiftPayload[] = await Promise.all(dtos.map((item) => {
const { id, ...rest } = item;
if (!Number.isInteger(id)) throw new ConflictException({ error_code: 'INVALID_SHIFT'});
if (!id) throw new BadRequestException({ error_code: 'SHIFT_INVALID' });
const changes: UpdateShiftChanges = {};
if (rest.date !== undefined) changes.date = rest.date;
@ -265,13 +265,15 @@ export class ShiftsUpsertService {
const existing = regroup_id.get(update.id);
if (!existing) {
return updates.map(exist => exist.id === update.id
? ({ ok: false, id: update.id, error: new NotFoundException(`Shift with id: ${update.id} not found`) } as UpdateShiftResult)
: ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to missing shift') }));
? ({ ok: false, id: update.id, error: new NotFoundException({ error_code: 'SHIFT_MISSING' }) } as UpdateShiftResult)
: ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) })
);
}
if (existing.is_approved) {
return updates.map(exist => exist.id === update.id
? ({ ok: false, id: update.id, error: new BadRequestException('Approved shift cannot be updated') } as UpdateShiftResult)
: ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to approved shift in update set') }));
? ({ ok: false, id: update.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) } as UpdateShiftResult)
: ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) })
);
}
}
@ -307,12 +309,14 @@ export class ShiftsUpsertService {
where: { timesheet_id: group.timesheet_id, date: day_date },
select: { id: true, start_time: true, end_time: true, date: true },
});
groups.set(key(group.timesheet_id, day_date), { existing: existing.map(row => ({
groups.set(key(group.timesheet_id, day_date), {
existing: existing.map(row => ({
id: row.id,
start: row.start_time,
end: row.end_time,
date: row.date,
})), incoming: planned_updates });
})), incoming: planned_updates
});
}
for (const planned of planned_updates) {
@ -336,7 +340,7 @@ export class ShiftsUpsertService {
},
}
} as UpdateShiftResult)
: ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to overlap in another update') })
: ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_OVERLAP' }) })
);
}
}
@ -426,7 +430,7 @@ export class ShiftsUpsertService {
where: { id: shift_id },
select: { id: true, date: true, timesheet_id: true },
});
if (!shift) throw new ConflictException({ error_code: 'INVALID_SHIFT'});
if (!shift) throw new ConflictException({ error_code: 'SHIFT_INVALID' });
await tx.shifts.delete({ where: { id: shift_id } });

View File

@ -1,11 +1,12 @@
import { Body, Controller, Get, ParseBoolPipe, ParseIntPipe, Patch, Query, Req, UnauthorizedException } from "@nestjs/common";
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service";
import { Roles as RoleEnum } from '.prisma/client';
import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service";
import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes";
@Controller('timesheets')
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
export class TimesheetController {
constructor(
private readonly timesheetOverview: GetTimesheetsOverviewService,
@ -13,17 +14,19 @@ export class TimesheetController {
) { }
@Get()
@RolesAllowed(RoleEnum.SUPERVISOR, RoleEnum.HR, RoleEnum.ACCOUNTING, RoleEnum.ADMIN)
async getTimesheetByIds(
@Req() req, @Query('year', ParseIntPipe) year:number, @Query('period_number', ParseIntPipe) period_number: number) {
getTimesheetByPayPeriod(
@Req() req,
@Query('year', ParseIntPipe) year: number,
@Query('period_number', ParseIntPipe) period_number: number
) {
const email = req.user?.email;
if (!email) throw new UnauthorizedException('Unauthorized User');
return this.timesheetOverview.getTimesheetsForEmployeeByPeriod(email, year, period_number);
}
@Patch('timesheet-approval')
@RolesAllowed(RoleEnum.SUPERVISOR, RoleEnum.HR, RoleEnum.ACCOUNTING, RoleEnum.ADMIN)
async approveTimesheet(
@RolesAllowed(...MANAGER_ROLES)
approveTimesheet(
@Body('timesheet_id', ParseIntPipe) timesheet_id: number,
@Body('is_approved', ParseBoolPipe) is_approved: boolean,
) {