Skip to content

Commit 78dea39

Browse files
authored
Merge pull request #267 from Code-4-Community/user-module-docs-and-logs
User module docs and logs
2 parents cb514ce + e689fe9 commit 78dea39

File tree

7 files changed

+1073
-35
lines changed

7 files changed

+1073
-35
lines changed

backend/src/guards/auth.guard.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ export class VerifyAdminRoleGuard implements CanActivate {
7373
}
7474
const result = await this.verifier.verify(accessToken);
7575
const groups = result['cognito:groups'] || [];
76+
77+
// Attach user info to request for use in controllers
78+
request.user = {
79+
userId: result['username'] || result['cognito:username'],
80+
email: result['email'],
81+
position: groups.includes('Admin') ? 'Admin' : (groups.includes('Employee') ? 'Employee' : 'Inactive')
82+
};
83+
7684
console.log("User groups from token:", groups);
7785
if (!groups.includes('Admin')) {
7886
console.warn("Access denied: User is not an Admin");
@@ -87,3 +95,57 @@ export class VerifyAdminRoleGuard implements CanActivate {
8795
}
8896
}
8997
}
98+
99+
@Injectable()
100+
export class VerifyAdminOrEmployeeRoleGuard implements CanActivate {
101+
private verifier: any;
102+
private readonly logger: Logger;
103+
104+
constructor() {
105+
const userPoolId = process.env.COGNITO_USER_POOL_ID;
106+
this.logger = new Logger(VerifyAdminOrEmployeeRoleGuard.name);
107+
108+
if (userPoolId) {
109+
this.verifier = CognitoJwtVerifier.create({
110+
userPoolId,
111+
tokenUse: "access",
112+
clientId: process.env.COGNITO_CLIENT_ID,
113+
});
114+
} else {
115+
throw new Error(
116+
"[AUTH] USER POOL ID is not defined in environment variables"
117+
);
118+
}
119+
}
120+
121+
async canActivate(context: ExecutionContext): Promise<boolean> {
122+
try {
123+
const request = context.switchToHttp().getRequest();
124+
const accessToken = request.cookies["access_token"];
125+
126+
if (!accessToken) {
127+
this.logger.error("No access token found in cookies");
128+
return false;
129+
}
130+
131+
const result = await this.verifier.verify(accessToken);
132+
const groups = result['cognito:groups'] || [];
133+
134+
this.logger.log(`User groups from token: ${groups.join(', ')}`);
135+
136+
// Check if user is either Admin or Employee
137+
const isAuthorized = groups.includes('Admin') || groups.includes('Employee');
138+
139+
if (!isAuthorized) {
140+
this.logger.warn("Access denied: User is not an Admin or Employee");
141+
return false;
142+
}
143+
144+
return true;
145+
146+
} catch (error) {
147+
this.logger.error("Token verification failed:", error);
148+
return false;
149+
}
150+
}
151+
}

backend/src/user/__test__/user.service.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { UserService } from '../user.service';
44

55
import * as AWS from 'aws-sdk';
66

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

1010
// Create mock functions at module level (BEFORE mock)
@@ -77,6 +77,9 @@ vi.mock('../../guards/auth.guard', () => ({
7777
}),
7878
VerifyAdminRoleGuard: vi.fn(class MockVerifyAdminRoleGuard {
7979
canActivate = vi.fn().mockResolvedValue(true);
80+
}),
81+
VerifyAdminOrEmployeeRoleGuard: vi.fn(class MockVerifyAdminOrEmployeeRoleGuard {
82+
canActivate = vi.fn().mockResolvedValue(true);
8083
})
8184
}));
8285

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { UserStatus } from '../../../../middle-layer/types/UserStatus';
3+
4+
export class ChangeRoleBody {
5+
user!: {
6+
userId: string,
7+
position: UserStatus,
8+
email: string
9+
};
10+
groupName!: UserStatus;
11+
}
Lines changed: 161 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,202 @@
1-
import { Controller, Get, Post, Body, Param, UseGuards } from "@nestjs/common";
1+
import { Controller, Get, Patch, Delete, Body, Param, UseGuards, Req } from "@nestjs/common";
22
import { UserService } from "./user.service";
33
import { User } from "../../../middle-layer/types/User";
44
import { UserStatus } from "../../../middle-layer/types/UserStatus";
5-
import { VerifyAdminRoleGuard, VerifyUserGuard } from "../guards/auth.guard";
5+
import { VerifyAdminRoleGuard, VerifyUserGuard, VerifyAdminOrEmployeeRoleGuard } from "../guards/auth.guard";
6+
import { ApiResponse, ApiParam , ApiBearerAuth} from "@nestjs/swagger";
7+
import { ChangeRoleBody } from "./types/user.types";
68

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

13+
/**
14+
* Get all users
15+
*/
1116
@Get()
12-
@UseGuards(VerifyUserGuard)
17+
@ApiResponse({
18+
status : 200,
19+
description : "All users retrieved successfully"
20+
})
21+
@ApiResponse({
22+
status : 403,
23+
description : "Forbidden"
24+
})
25+
@ApiResponse({
26+
status : 500,
27+
description : "Internal Server Error"
28+
})
29+
@UseGuards(VerifyAdminOrEmployeeRoleGuard)
30+
@ApiBearerAuth()
1331
async getAllUsers() {
1432
return await this.userService.getAllUsers();
1533
}
1634

35+
36+
/**
37+
* Get all inactive users
38+
*/
1739
@Get("inactive")
18-
@UseGuards(VerifyUserGuard)
40+
@ApiResponse({
41+
status : 200,
42+
description : "All inactive users retrieved successfully"
43+
})
44+
@ApiResponse({
45+
status : 403,
46+
description : "Forbidden"
47+
})
48+
@ApiResponse({
49+
status : 500,
50+
description : "Internal Server Error"
51+
})
52+
@UseGuards(VerifyAdminOrEmployeeRoleGuard)
53+
@ApiBearerAuth()
1954
async getAllInactiveUsers(): Promise<User[]> {
2055
return await this.userService.getAllInactiveUsers();
2156
}
2257

58+
/**
59+
* Get all active users
60+
*/
2361
@Get("active")
24-
@UseGuards(VerifyUserGuard)
62+
@ApiResponse({
63+
status : 200,
64+
description : "All active users retrieved successfully"
65+
})
66+
@ApiResponse({
67+
status : 403,
68+
description : "Forbidden"
69+
})
70+
@ApiResponse({
71+
status : 500,
72+
description : "Internal Server Error"
73+
})
74+
@UseGuards(VerifyAdminOrEmployeeRoleGuard)
75+
@ApiBearerAuth()
2576
async getAllActiveUsers(): Promise<User[]> {
2677
console.log("Fetching all active users");
2778
return await this.userService.getAllActiveUsers();
2879
}
29-
// Make sure to put a guard on this route
30-
@Post("change-role")
80+
81+
/**
82+
* Change a user's role (make sure guard is on this route)
83+
*/
84+
@Patch("change-role")
85+
@ApiResponse({
86+
status : 200,
87+
description : "User role changed successfully"
88+
})
89+
@ApiResponse({
90+
status : 400,
91+
description : "{Error encountered}"
92+
})
93+
@ApiResponse({
94+
status : 401,
95+
description : "Unauthorized"
96+
})
97+
@ApiResponse({
98+
status : 403,
99+
description : "Forbidden"
100+
})
101+
@ApiResponse({
102+
status : 404,
103+
description : "Not Found"
104+
})
105+
@ApiResponse({
106+
status : 500,
107+
description : "Internal Server Error"
108+
})
31109
@UseGuards(VerifyAdminRoleGuard)
110+
@ApiBearerAuth()
32111
async addToGroup(
33-
@Body("user") user: User,
34-
@Body("groupName") groupName: UserStatus,
35-
@Body("requestedBy") requestedBy: User
112+
@Body() changeRoleBody: ChangeRoleBody,
113+
@Req() req: any
36114
): Promise<User> {
115+
// Get the requesting admin from the authenticated session (attached by guard)
116+
const requestedBy: User = req.user;
117+
37118
let newUser: User = await this.userService.addUserToGroup(
38-
user,
39-
groupName,
119+
changeRoleBody.user,
120+
changeRoleBody.groupName,
40121
requestedBy
41122
);
42123
return newUser as User;
43124
}
44125

45-
@Post("delete-user")
126+
/**
127+
* Delete a user
128+
*/
129+
@Delete("delete-user/:userId")
130+
@ApiParam({
131+
name: 'userId',
132+
description: 'ID of the user to delete',
133+
required: true,
134+
type: String
135+
})
136+
@ApiResponse({
137+
status : 200,
138+
description : "User deleted successfully"
139+
})
140+
@ApiResponse({
141+
status : 400,
142+
description : "{Error encountered}"
143+
})
144+
@ApiResponse({
145+
status : 401,
146+
description : "Unauthorized"
147+
})
148+
@ApiResponse({
149+
status : 403,
150+
description : "Forbidden"
151+
})
152+
@ApiResponse({
153+
status : 404,
154+
description : "Not Found"
155+
})
156+
@ApiResponse({
157+
status : 500,
158+
description : "Internal Server Error"
159+
})
46160
@UseGuards(VerifyAdminRoleGuard)
161+
@ApiBearerAuth()
47162
async deleteUser(
48-
@Body("user") user: User,
49-
@Body("requestedBy") requestedBy: User
163+
@Param('userId') userId: string,
164+
@Req() req: any
50165
): Promise<User> {
51-
let deletedUser = await this.userService.deleteUser(user, requestedBy);
52-
return user as User;
166+
// Get the requesting admin from the authenticated session (attached by guard)
167+
const requestedBy: User = req.user;
168+
169+
// Fetch the user to delete from the database
170+
const userToDelete: User = await this.userService.getUserById(userId);
171+
172+
return await this.userService.deleteUser(userToDelete, requestedBy);
53173
}
54174

175+
/**
176+
* Get user by ID
177+
*/
55178
@Get(":id")
56-
@UseGuards(VerifyUserGuard)
57-
async getUserById(@Param("id") userId: string) {
179+
@ApiParam({
180+
name: 'id',
181+
description: 'User ID to retrieve',
182+
required: true,
183+
type: String
184+
})
185+
@ApiResponse({
186+
status : 200,
187+
description : "User retrieved successfully"
188+
})
189+
@ApiResponse({
190+
status : 403,
191+
description : "Forbidden"
192+
})
193+
@ApiResponse({
194+
status : 500,
195+
description : "Internal Server Error"
196+
})
197+
@UseGuards(VerifyAdminOrEmployeeRoleGuard)
198+
@ApiBearerAuth()
199+
async getUserById(@Param('id') userId: string): Promise<User> {
58200
return await this.userService.getUserById(userId);
59201
}
60202
}

0 commit comments

Comments
 (0)