Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { AuthService } from './auth.service';
import { User } from '../types/User';
import { Response } from 'express';
import { VerifyAdminRoleGuard, VerifyUserGuard } from "../guards/auth.guard";
import { RegisterBody } from './types/auth.types';
import { LoginBody, RegisterBody } from './types/auth.types';
import { ApiBearerAuth, ApiResponse } from '@nestjs/swagger';

@Controller('auth')
Expand Down Expand Up @@ -43,14 +43,14 @@ export class AuthController {
status : 201,
description : "User registered successfully"
})
@ApiResponse({
status : 400,
description : "{Error encountered}"}
)
@ApiResponse({
status: 500,
description : "Internal Server Error"
})
@ApiResponse({
status : 400,
description : "{Error encountered}"}
)
@ApiResponse({
status: 409,
description : "{Error encountered}"
Expand All @@ -65,8 +65,7 @@ export class AuthController {
@Post('login')
async login(
@Res({ passthrough: true }) response: Response,
@Body('username') username: string,
@Body('password') password: string,
@Body() body:LoginBody
): Promise<{
user: User;
session?: string;
Expand All @@ -75,7 +74,7 @@ export class AuthController {
username?: string;
position?: string;
}> {
const result = await this.authService.login(username, password);
const result = await this.authService.login(body.username, body.password);

// Set cookie with access token
if (result.access_token) {
Expand Down
62 changes: 62 additions & 0 deletions backend/src/guards/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ export class VerifyAdminRoleGuard implements CanActivate {
}
const result = await this.verifier.verify(accessToken);
const groups = result['cognito:groups'] || [];

// Attach user info to request for use in controllers
request.user = {
userId: result['username'] || result['cognito:username'],
email: result['email'],
position: groups.includes('Admin') ? 'Admin' : (groups.includes('Employee') ? 'Employee' : 'Inactive')
};

console.log("User groups from token:", groups);
if (!groups.includes('Admin')) {
console.warn("Access denied: User is not an Admin");
Expand All @@ -87,3 +95,57 @@ export class VerifyAdminRoleGuard implements CanActivate {
}
}
}

@Injectable()
export class VerifyAdminOrEmployeeRoleGuard implements CanActivate {
private verifier: any;
private readonly logger: Logger;

constructor() {
const userPoolId = process.env.COGNITO_USER_POOL_ID;
this.logger = new Logger(VerifyAdminOrEmployeeRoleGuard.name);

if (userPoolId) {
this.verifier = CognitoJwtVerifier.create({
userPoolId,
tokenUse: "access",
clientId: process.env.COGNITO_CLIENT_ID,
});
} else {
throw new Error(
"[AUTH] USER POOL ID is not defined in environment variables"
);
}
}

async canActivate(context: ExecutionContext): Promise<boolean> {
try {
const request = context.switchToHttp().getRequest();
const accessToken = request.cookies["access_token"];

if (!accessToken) {
this.logger.error("No access token found in cookies");
return false;
}

const result = await this.verifier.verify(accessToken);
const groups = result['cognito:groups'] || [];

this.logger.log(`User groups from token: ${groups.join(', ')}`);

// Check if user is either Admin or Employee
const isAuthorized = groups.includes('Admin') || groups.includes('Employee');

if (!isAuthorized) {
this.logger.warn("Access denied: User is not an Admin or Employee");
return false;
}

Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new VerifyAdminOrEmployeeRoleGuard doesn't attach user information to the request object like VerifyAdminRoleGuard does (lines 77-82). While this guard is not currently used in endpoints that need req.user, it's inconsistent with the other guard and could lead to bugs if it's used in endpoints that expect req.user to be populated.

Suggested change
// Attach verified user information to the request, consistent with VerifyAdminRoleGuard
request.user = result;

Copilot uses AI. Check for mistakes.
return true;

} catch (error) {
this.logger.error("Token verification failed:", error);
return false;
}
}
}
5 changes: 4 additions & 1 deletion backend/src/user/__test__/user.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { UserService } from '../user.service';

import * as AWS from 'aws-sdk';

import { VerifyUserGuard, VerifyAdminRoleGuard } from '../../guards/auth.guard';
import { VerifyUserGuard, VerifyAdminRoleGuard, VerifyAdminOrEmployeeRoleGuard } from '../../guards/auth.guard';
import { describe, it, expect, beforeEach, beforeAll, vi } from 'vitest';

// Create mock functions at module level (BEFORE mock)
Expand Down Expand Up @@ -77,6 +77,9 @@ vi.mock('../../guards/auth.guard', () => ({
}),
VerifyAdminRoleGuard: vi.fn(class MockVerifyAdminRoleGuard {
canActivate = vi.fn().mockResolvedValue(true);
}),
VerifyAdminOrEmployeeRoleGuard: vi.fn(class MockVerifyAdminOrEmployeeRoleGuard {
canActivate = vi.fn().mockResolvedValue(true);
})
}));

Expand Down
11 changes: 11 additions & 0 deletions backend/src/user/types/user.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import ApiProperty.

Suggested change
import { ApiProperty } from '@nestjs/swagger';

Copilot uses AI. Check for mistakes.
import { UserStatus } from '../../../../middle-layer/types/UserStatus';

export class ChangeRoleBody {
user!: {
userId: string,
position: UserStatus,
email: string
};
Comment on lines +5 to +9
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing @ApiProperty decorators on the class properties. While the ApiProperty import is present, the properties lack decorators, which means they won't appear in the Swagger documentation with proper descriptions and types. Each property should have an @ApiProperty decorator to document the expected structure in Swagger UI.

Suggested change
user!: {
userId: string,
position: UserStatus,
email: string
};
@ApiProperty({
type: 'object',
properties: {
userId: { type: 'string' },
position: { enum: Object.values(UserStatus) },
email: { type: 'string', format: 'email' },
},
})
user!: {
userId: string,
position: UserStatus,
email: string
};
@ApiProperty({ enum: UserStatus })

Copilot uses AI. Check for mistakes.
groupName!: UserStatus;
}
180 changes: 161 additions & 19 deletions backend/src/user/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,202 @@
import { Controller, Get, Post, Body, Param, UseGuards } from "@nestjs/common";
import { Controller, Get, Patch, Delete, Body, Param, UseGuards, Req } from "@nestjs/common";
import { UserService } from "./user.service";
import { User } from "../../../middle-layer/types/User";
import { UserStatus } from "../../../middle-layer/types/UserStatus";
import { VerifyAdminRoleGuard, VerifyUserGuard } from "../guards/auth.guard";
import { VerifyAdminRoleGuard, VerifyUserGuard, VerifyAdminOrEmployeeRoleGuard } from "../guards/auth.guard";
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import: VerifyUserGuard is imported but never used in this file. All endpoints now use either VerifyAdminRoleGuard or VerifyAdminOrEmployeeRoleGuard.

Suggested change
import { VerifyAdminRoleGuard, VerifyUserGuard, VerifyAdminOrEmployeeRoleGuard } from "../guards/auth.guard";
import { VerifyAdminRoleGuard, VerifyAdminOrEmployeeRoleGuard } from "../guards/auth.guard";

Copilot uses AI. Check for mistakes.
import { ApiResponse, ApiParam , ApiBearerAuth} from "@nestjs/swagger";
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent spacing in ApiParam decorator. There's a space after the comma between properties (e.g., "ApiParam , ApiBearerAuth"), which is inconsistent with the rest of the codebase and standard formatting conventions.

Suggested change
import { ApiResponse, ApiParam , ApiBearerAuth} from "@nestjs/swagger";
import { ApiResponse, ApiParam, ApiBearerAuth } from "@nestjs/swagger";

Copilot uses AI. Check for mistakes.
import { ChangeRoleBody } from "./types/user.types";

@Controller("user")
export class UserController {
constructor(private readonly userService: UserService) {}

/**
* Get all users
*/
@Get()
@UseGuards(VerifyUserGuard)
@ApiResponse({
status : 200,
description : "All users retrieved successfully"
})
@ApiResponse({
status : 403,
description : "Forbidden"
})
@ApiResponse({
status : 500,
description : "Internal Server Error"
})
@UseGuards(VerifyAdminOrEmployeeRoleGuard)
@ApiBearerAuth()
async getAllUsers() {
return await this.userService.getAllUsers();
}


/**
* Get all inactive users
*/
@Get("inactive")
@UseGuards(VerifyUserGuard)
@ApiResponse({
status : 200,
description : "All inactive users retrieved successfully"
})
@ApiResponse({
status : 403,
description : "Forbidden"
})
@ApiResponse({
status : 500,
description : "Internal Server Error"
})
@UseGuards(VerifyAdminOrEmployeeRoleGuard)
@ApiBearerAuth()
async getAllInactiveUsers(): Promise<User[]> {
return await this.userService.getAllInactiveUsers();
}

/**
* Get all active users
*/
@Get("active")
@UseGuards(VerifyUserGuard)
@ApiResponse({
status : 200,
description : "All active users retrieved successfully"
})
@ApiResponse({
status : 403,
description : "Forbidden"
})
@ApiResponse({
status : 500,
description : "Internal Server Error"
})
@UseGuards(VerifyAdminOrEmployeeRoleGuard)
@ApiBearerAuth()
async getAllActiveUsers(): Promise<User[]> {
console.log("Fetching all active users");
return await this.userService.getAllActiveUsers();
}
// Make sure to put a guard on this route
@Post("change-role")

/**
* Change a user's role (make sure guard is on this route)
*/
@Patch("change-role")
@ApiResponse({
status : 200,
description : "User role changed successfully"
})
@ApiResponse({
status : 400,
description : "{Error encountered}"
})
@ApiResponse({
status : 401,
description : "Unauthorized"
})
@ApiResponse({
status : 403,
description : "Forbidden"
})
@ApiResponse({
status : 404,
description : "Not Found"
})
@ApiResponse({
status : 500,
description : "Internal Server Error"
})
@UseGuards(VerifyAdminRoleGuard)
@ApiBearerAuth()
async addToGroup(
@Body("user") user: User,
@Body("groupName") groupName: UserStatus,
@Body("requestedBy") requestedBy: User
@Body() changeRoleBody: ChangeRoleBody,
@Req() req: any
): Promise<User> {
// Get the requesting admin from the authenticated session (attached by guard)
const requestedBy: User = req.user;

let newUser: User = await this.userService.addUserToGroup(
user,
groupName,
changeRoleBody.user,
changeRoleBody.groupName,
requestedBy
);
return newUser as User;
}
Comment on lines +84 to 124
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking API change: Changed from POST to PATCH and modified request signature. The frontend code still uses POST method with the old signature that includes requestedBy in the body (see ApprovedUserCard.tsx:38-53 and PendingUserCard.tsx:33-44). This will break existing functionality until the frontend is updated to use PATCH and remove requestedBy from the request body.

Copilot uses AI. Check for mistakes.

@Post("delete-user")
/**
* Delete a user
*/
@Delete("delete-user/:userId")
@ApiParam({
name: 'userId',
description: 'ID of the user to delete',
required: true,
type: String
})
@ApiResponse({
status : 200,
description : "User deleted successfully"
})
@ApiResponse({
status : 400,
description : "{Error encountered}"
})
@ApiResponse({
status : 401,
description : "Unauthorized"
})
@ApiResponse({
status : 403,
description : "Forbidden"
})
@ApiResponse({
status : 404,
description : "Not Found"
})
@ApiResponse({
status : 500,
description : "Internal Server Error"
})
@UseGuards(VerifyAdminRoleGuard)
@ApiBearerAuth()
async deleteUser(
@Body("user") user: User,
@Body("requestedBy") requestedBy: User
@Param('userId') userId: string,
@Req() req: any
): Promise<User> {
let deletedUser = await this.userService.deleteUser(user, requestedBy);
return user as User;
// Get the requesting admin from the authenticated session (attached by guard)
const requestedBy: User = req.user;

// Fetch the user to delete from the database
const userToDelete: User = await this.userService.getUserById(userId);

return await this.userService.deleteUser(userToDelete, requestedBy);
}
Comment on lines +129 to 173
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking API change: Changed from POST to DELETE and modified from body parameters to URL parameter. The frontend code still uses POST method with a request body containing user and requestedBy (see ApprovedUserCard.tsx:81-92 and PendingUserCard.tsx:64-75). This will break existing functionality until the frontend is updated to use DELETE with the userId as a URL parameter.

Copilot uses AI. Check for mistakes.

/**
* Get user by ID
*/
@Get(":id")
@UseGuards(VerifyUserGuard)
async getUserById(@Param("id") userId: string) {
@ApiParam({
name: 'id',
description: 'User ID to retrieve',
required: true,
type: String
})
@ApiResponse({
status : 200,
description : "User retrieved successfully"
})
@ApiResponse({
status : 403,
description : "Forbidden"
})
@ApiResponse({
status : 500,
description : "Internal Server Error"
})
@UseGuards(VerifyAdminOrEmployeeRoleGuard)
@ApiBearerAuth()
async getUserById(@Param('id') userId: string): Promise<User> {
return await this.userService.getUserById(userId);
}
}
Loading