From 76955e2f4ee89595b0a0176b3668d4402a737481 Mon Sep 17 00:00:00 2001 From: lyannne Date: Thu, 22 Jan 2026 21:15:05 -0500 Subject: [PATCH 01/10] new types file --- backend/src/user/types/user.types.ts | 0 backend/src/user/user.service.ts | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 backend/src/user/types/user.types.ts diff --git a/backend/src/user/types/user.types.ts b/backend/src/user/types/user.types.ts new file mode 100644 index 00000000..e69de29b diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index 032a3732..4a0b4944 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -41,10 +41,12 @@ export class UserService { // 2. Validate input if (!user || !user.userId) { + this.logger.error("Invalid user object provided for deletion"); throw new BadRequestException("Valid user object is required"); } if (!requestedBy || !requestedBy.userId) { + this.logger.error("Invalid requesting user object provided for deletion"); throw new BadRequestException("Valid requesting user is required"); } From 0d37649bad82bf5195e4b4aee72a5f21e33d80e3 Mon Sep 17 00:00:00 2001 From: lyannne Date: Fri, 23 Jan 2026 14:50:38 -0500 Subject: [PATCH 02/10] logging --- backend/src/user/user.service.ts | 72 ++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index 4a0b4944..3f55ecf1 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -23,6 +23,8 @@ export class UserService { private dynamoDb = new AWS.DynamoDB.DocumentClient(); private ses = new AWS.SES({ region: process.env.AWS_REGION }); + // purpose statement: deletes user from database; only admin can delete users + // use case: employee is no longer with BCAN async deleteUser(user: User, requestedBy: User): Promise { const userPoolId = process.env.COGNITO_USER_POOL_ID; const tableName = process.env.DYNAMODB_USER_TABLE_NAME; @@ -60,6 +62,7 @@ export class UserService { // 4. Prevent self-deletion if (requestedBy.userId === username) { + this.logger.warn(`Administrator ${requestedBy.userId} attempted to delete their own account`); throw new BadRequestException("Administrators cannot delete their own account"); } @@ -179,19 +182,30 @@ export class UserService { return userToDelete; } - async getAllUsers(): Promise { +// purpose statement: retrieves all users from the database +// use case: admin wants to see all users on user management page +async getAllUsers(): Promise { const params = { TableName: process.env.DYNAMODB_USER_TABLE_NAME || "TABLE_FAILURE", }; try { + this.logger.log('Executing DynamoDB scan to retrieve all users...'); const data = await this.dynamoDb.scan(params).promise(); + + const userCount = data.Items?.length || 0; + this.logger.log(`✅ Successfully retrieved ${userCount} users from database`); + return data.Items; } catch (error) { - throw new Error("Could not retrieve users."); + this.logger.error('Failed to retrieve users from DynamoDB:', error); + throw new InternalServerErrorException("Could not retrieve users."); } } - async addUserToGroup( + + // purpose statement: adds user to a group/changes their role; only admin can do this + // use case: admin wants to promote an employee to admin or activate an inactive user +async addUserToGroup( user: User, groupName: UserStatus, requestedBy: User @@ -374,6 +388,9 @@ export class UserService { const result = await this.dynamoDb.update(params).promise(); if (!result.Attributes) { + this.logger.error( + `DynamoDB update did not return updated attributes for ${username}` + ); throw new InternalServerErrorException( "Failed to retrieve updated user data" ); @@ -441,6 +458,8 @@ export class UserService { } } + // purpose statement: retrieves user by their userId + // use case: not actually sure right now, maybe is there is an option for admin to click on a specific user to see details? async getUserById(userId: string): Promise { const params = { TableName: process.env.DYNAMODB_USER_TABLE_NAME || "TABLE_FAILURE", @@ -450,15 +469,22 @@ export class UserService { }; try { + this.logger.log(`Fetching user ${userId} from DynamoDB...`); const data = await this.dynamoDb.get(params).promise(); + + this.logger.log(`✅ Successfully retrieved user ${userId}`); return data.Item; } catch (error) { - throw new Error('Could not retrieve user.'); + this.logger.error(`Failed to retrieve user ${userId} from DynamoDB:`, error); + throw new InternalServerErrorException('Could not retrieve user.'); } } + // purpose statement: retrieves all inactive users + // use case: for admin to see all users that need account approval on user management page (pending users tab) async getAllInactiveUsers(): Promise { this.logger.log("Fetching all inactive users in service"); + const params = { TableName: process.env.DYNAMODB_USER_TABLE_NAME || "TABLE_FAILURE", FilterExpression: "#pos IN (:inactive)", @@ -471,7 +497,9 @@ export class UserService { }; try { + this.logger.log('Executing DynamoDB scan with filter for Inactive users...'); const result = await this.dynamoDb.scan(params).promise(); + const users: User[] = (result.Items || []).map((item) => ({ userId: item.userId, // Assign name to userId position: item.position as UserStatus, @@ -479,6 +507,7 @@ export class UserService { name: item.userId, // Keep name as name })); + this.logger.log(`✅ Successfully retrieved ${users.length} inactive users`); return users; } catch (error) { this.logger.error("Error scanning DynamoDB:", error); @@ -489,7 +518,10 @@ export class UserService { } } - async getAllActiveUsers(): Promise { + // purpose statement: retrieves all active users (Admins and Employees) + // use case: for admin to see all current users on user management page (current users tab) +async getAllActiveUsers(): Promise { + this.logger.log('Starting getAllActiveUsers - Fetching all active users (Admin and Employee)'); const params = { TableName: process.env.DYNAMODB_USER_TABLE_NAME || "TABLE_FAILURE", FilterExpression: "#pos IN (:admin, :employee)", @@ -503,18 +535,21 @@ export class UserService { }; try { + this.logger.log('Executing DynamoDB scan with filter for Admin and Employee users...'); const result = await this.dynamoDb.scan(params).promise(); if (!result.Items) { this.logger.error("No active users found."); this.logger.error("DynamoDB scan result:", result); throw new NotFoundException("No active users found."); } + const users: User[] = (result.Items || []).map((item) => ({ userId: item.userId, // Assign name to userId position: item.position as UserStatus, email: item.email, name: item.userId, // Keep name as name })); + this.logger.debug(`Fetched ${users.length} active users.`); return users; @@ -522,19 +557,25 @@ export class UserService { this.logger.error("Error scanning DynamoDB:", error); if (error instanceof NotFoundException) throw error; throw new InternalServerErrorException( - "Failed to retrieve inactive users." + "Failed to retrieve active users." ); } } - // sends email to user once account is approved, used in method above when a user + // purpose statement: sends email to user once account is approved, used in method above when a user // is added to the Employee or Admin group from Inactive async sendVerificationEmail(userEmail: string): Promise { + this.logger.log(`Starting sendVerificationEmail for email: ${userEmail}`); + + if (!userEmail || !this.isValidEmail(userEmail)) { + this.logger.error(`Invalid email address provided: ${userEmail}`); + throw new BadRequestException("Valid email address is required"); + } + // remove actual email and add to env later!! - const fromEmail = process.env.NOTIFICATION_EMAIL_SENDER || - 'c4cneu.bcan@gmail.com'; + const fromEmail = process.env.NOTIFICATION_EMAIL_SENDER || 'c4cneu.bcan@gmail.com'; const params: AWS.SES.SendEmailRequest = { Source: fromEmail, @@ -551,13 +592,20 @@ export class UserService { }; try { + this.logger.log(`Calling AWS SES to send email to ${userEmail}...`); const result = await this.ses.sendEmail(params).promise(); - this.logger.log(`Verification email sent to ${userEmail}`); + this.logger.log(`✅ Verification email sent successfully to ${userEmail}. MessageId: ${result.MessageId}`); return result; } catch (err: unknown) { this.logger.error('Error sending email: ', err); - const errMessage = (err instanceof Error) ? err.message : 'Generic'; - throw new Error(`Failed to send email: ${errMessage}`); + const errMessage = (err instanceof Error) ? err.message : 'Unknown error'; + throw new InternalServerErrorException(`Failed to send email: ${errMessage}`); } } + + // Helper method for email validation + private isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } } From 3932439ed28277c78086ad2d45e4cbaf6fa30471 Mon Sep 17 00:00:00 2001 From: lyannne Date: Fri, 23 Jan 2026 15:58:15 -0500 Subject: [PATCH 03/10] swagger docs and types made, also switched change-roles route to a patch instead of post since that made more sense --- backend/src/user/types/user.types.ts | 29 +++++++ backend/src/user/user.controller.ts | 124 ++++++++++++++++++++++++--- backend/src/user/user.service.ts | 5 +- 3 files changed, 143 insertions(+), 15 deletions(-) diff --git a/backend/src/user/types/user.types.ts b/backend/src/user/types/user.types.ts index e69de29b..83423f7d 100644 --- a/backend/src/user/types/user.types.ts +++ b/backend/src/user/types/user.types.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { UserStatus } from '../../../../middle-layer/types/UserStatus'; + +export class ChangeRoleBody { + user!: { + userId: string, + position: UserStatus, + email: string + }; + groupName!: UserStatus; + requestedBy!: { + userId: string, + position: UserStatus, + email: string + }; +} + +export class DeleteUserBody { + user!: { + userId: string, + position: UserStatus, + email: string + }; + requestedBy!: { + userId: string, + position: UserStatus, + email: string + }; +} \ No newline at end of file diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index 8d72da9c..0833c6c9 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -1,60 +1,156 @@ -import { Controller, Get, Post, Body, Param, UseGuards } from "@nestjs/common"; +import { Controller, Get, Post, Patch, Body, Param, UseGuards } 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 { ApiResponse, ApiParam } from "@nestjs/swagger"; +import { ChangeRoleBody, DeleteUserBody } from "./types/user.types"; @Controller("user") export class UserController { constructor(private readonly userService: UserService) {} + /** + * Get all users + */ @Get() + @ApiResponse({ + status : 200, + description : "All users retrieved successfully" + }) + @ApiResponse({ + status : 500, + description : "Internal Server Error" + }) @UseGuards(VerifyUserGuard) async getAllUsers() { return await this.userService.getAllUsers(); } + + /** + * Get all inactive users + */ @Get("inactive") + @ApiResponse({ + status : 200, + description : "All inactive users retrieved successfully" + }) + @ApiResponse({ + status : 500, + description : "Internal Server Error" + }) @UseGuards(VerifyUserGuard) async getAllInactiveUsers(): Promise { return await this.userService.getAllInactiveUsers(); } + /** + * Get all active users + */ @Get("active") + @ApiResponse({ + status : 200, + description : "All active users retrieved successfully" + }) + @ApiResponse({ + status : 500, + description : "Internal Server Error" + }) @UseGuards(VerifyUserGuard) async getAllActiveUsers(): Promise { 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 : 404, + description : "Not Found" + }) + @ApiResponse({ + status : 500, + description : "Internal Server Error" + }) @UseGuards(VerifyAdminRoleGuard) async addToGroup( - @Body("user") user: User, - @Body("groupName") groupName: UserStatus, - @Body("requestedBy") requestedBy: User + @Body() changeRoleBody: ChangeRoleBody ): Promise { let newUser: User = await this.userService.addUserToGroup( - user, - groupName, - requestedBy + changeRoleBody.user, + changeRoleBody.groupName, + changeRoleBody.requestedBy ); return newUser as User; } + /** + * Delete a user + */ @Post("delete-user") + @ApiResponse({ + status : 201, + description : "User deleted successfully" + }) + @ApiResponse({ + status : 400, + description : "{Error encountered}" + }) + @ApiResponse({ + status : 401, + description : "Unauthorized" + }) + @ApiResponse({ + status : 404, + description : "Not Found" + }) + @ApiResponse({ + status : 500, + description : "Internal Server Error" + }) @UseGuards(VerifyAdminRoleGuard) async deleteUser( - @Body("user") user: User, - @Body("requestedBy") requestedBy: User + @Body() deleteUserBody: DeleteUserBody ): Promise { - let deletedUser = await this.userService.deleteUser(user, requestedBy); - return user as User; + let deletedUser = await this.userService.deleteUser(deleteUserBody.user, deleteUserBody.requestedBy); + return deletedUser as User; } + /** + * Get user by ID + */ @Get(":id") + @ApiParam({ + name: 'id', + description: 'User ID to retrieve', + required: true, + type: String + }) + @ApiResponse({ + status : 200, + description : "User retrieved successfully" + }) + @ApiResponse({ + status : 500, + description : "Internal Server Error" + }) @UseGuards(VerifyUserGuard) - async getUserById(@Param("id") userId: string) { + async getUserById(@Param('id') userId: string): Promise { return await this.userService.getUserById(userId); } } diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index 3f55ecf1..a1fadaf1 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -102,6 +102,7 @@ export class UserService { const deleteResult = await this.dynamoDb.delete(deleteParams).promise(); if (!deleteResult.Attributes) { + this.logger.error(`DynamoDB delete did not return deleted attributes for ${username}`); throw new InternalServerErrorException( "Failed to delete user from database" ); @@ -275,6 +276,9 @@ async addUserToGroup( requestedBy.position === UserStatus.Admin && groupName !== UserStatus.Admin ) { + this.logger.warn( + `Administrator ${requestedBy.userId} attempted to demote themselves` + ); throw new BadRequestException( "Administrators cannot demote themselves" ); @@ -542,7 +546,6 @@ async getAllActiveUsers(): Promise { this.logger.error("DynamoDB scan result:", result); throw new NotFoundException("No active users found."); } - const users: User[] = (result.Items || []).map((item) => ({ userId: item.userId, // Assign name to userId position: item.position as UserStatus, From 11b5bdc19591d4efb6bf8c1c4fb3e56b2ee6b8a4 Mon Sep 17 00:00:00 2001 From: prooflesben Date: Fri, 23 Jan 2026 16:44:04 -0500 Subject: [PATCH 04/10] added in --- backend/src/auth/auth.controller.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index ab77991c..72bd0665 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -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') @@ -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}" @@ -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; @@ -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) { From 34e75b6005abe6d2b4aa3b2e57a4145a02b85c56 Mon Sep 17 00:00:00 2001 From: lyannne Date: Thu, 22 Jan 2026 21:15:05 -0500 Subject: [PATCH 05/10] new types file --- backend/src/user/types/user.types.ts | 0 backend/src/user/user.service.ts | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 backend/src/user/types/user.types.ts diff --git a/backend/src/user/types/user.types.ts b/backend/src/user/types/user.types.ts new file mode 100644 index 00000000..e69de29b diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index 032a3732..4a0b4944 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -41,10 +41,12 @@ export class UserService { // 2. Validate input if (!user || !user.userId) { + this.logger.error("Invalid user object provided for deletion"); throw new BadRequestException("Valid user object is required"); } if (!requestedBy || !requestedBy.userId) { + this.logger.error("Invalid requesting user object provided for deletion"); throw new BadRequestException("Valid requesting user is required"); } From a1d68047c9f76d951c81c40bb3e27265232eb5a4 Mon Sep 17 00:00:00 2001 From: lyannne Date: Fri, 23 Jan 2026 14:50:38 -0500 Subject: [PATCH 06/10] logging --- backend/src/user/user.service.ts | 72 ++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index 4a0b4944..3f55ecf1 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -23,6 +23,8 @@ export class UserService { private dynamoDb = new AWS.DynamoDB.DocumentClient(); private ses = new AWS.SES({ region: process.env.AWS_REGION }); + // purpose statement: deletes user from database; only admin can delete users + // use case: employee is no longer with BCAN async deleteUser(user: User, requestedBy: User): Promise { const userPoolId = process.env.COGNITO_USER_POOL_ID; const tableName = process.env.DYNAMODB_USER_TABLE_NAME; @@ -60,6 +62,7 @@ export class UserService { // 4. Prevent self-deletion if (requestedBy.userId === username) { + this.logger.warn(`Administrator ${requestedBy.userId} attempted to delete their own account`); throw new BadRequestException("Administrators cannot delete their own account"); } @@ -179,19 +182,30 @@ export class UserService { return userToDelete; } - async getAllUsers(): Promise { +// purpose statement: retrieves all users from the database +// use case: admin wants to see all users on user management page +async getAllUsers(): Promise { const params = { TableName: process.env.DYNAMODB_USER_TABLE_NAME || "TABLE_FAILURE", }; try { + this.logger.log('Executing DynamoDB scan to retrieve all users...'); const data = await this.dynamoDb.scan(params).promise(); + + const userCount = data.Items?.length || 0; + this.logger.log(`✅ Successfully retrieved ${userCount} users from database`); + return data.Items; } catch (error) { - throw new Error("Could not retrieve users."); + this.logger.error('Failed to retrieve users from DynamoDB:', error); + throw new InternalServerErrorException("Could not retrieve users."); } } - async addUserToGroup( + + // purpose statement: adds user to a group/changes their role; only admin can do this + // use case: admin wants to promote an employee to admin or activate an inactive user +async addUserToGroup( user: User, groupName: UserStatus, requestedBy: User @@ -374,6 +388,9 @@ export class UserService { const result = await this.dynamoDb.update(params).promise(); if (!result.Attributes) { + this.logger.error( + `DynamoDB update did not return updated attributes for ${username}` + ); throw new InternalServerErrorException( "Failed to retrieve updated user data" ); @@ -441,6 +458,8 @@ export class UserService { } } + // purpose statement: retrieves user by their userId + // use case: not actually sure right now, maybe is there is an option for admin to click on a specific user to see details? async getUserById(userId: string): Promise { const params = { TableName: process.env.DYNAMODB_USER_TABLE_NAME || "TABLE_FAILURE", @@ -450,15 +469,22 @@ export class UserService { }; try { + this.logger.log(`Fetching user ${userId} from DynamoDB...`); const data = await this.dynamoDb.get(params).promise(); + + this.logger.log(`✅ Successfully retrieved user ${userId}`); return data.Item; } catch (error) { - throw new Error('Could not retrieve user.'); + this.logger.error(`Failed to retrieve user ${userId} from DynamoDB:`, error); + throw new InternalServerErrorException('Could not retrieve user.'); } } + // purpose statement: retrieves all inactive users + // use case: for admin to see all users that need account approval on user management page (pending users tab) async getAllInactiveUsers(): Promise { this.logger.log("Fetching all inactive users in service"); + const params = { TableName: process.env.DYNAMODB_USER_TABLE_NAME || "TABLE_FAILURE", FilterExpression: "#pos IN (:inactive)", @@ -471,7 +497,9 @@ export class UserService { }; try { + this.logger.log('Executing DynamoDB scan with filter for Inactive users...'); const result = await this.dynamoDb.scan(params).promise(); + const users: User[] = (result.Items || []).map((item) => ({ userId: item.userId, // Assign name to userId position: item.position as UserStatus, @@ -479,6 +507,7 @@ export class UserService { name: item.userId, // Keep name as name })); + this.logger.log(`✅ Successfully retrieved ${users.length} inactive users`); return users; } catch (error) { this.logger.error("Error scanning DynamoDB:", error); @@ -489,7 +518,10 @@ export class UserService { } } - async getAllActiveUsers(): Promise { + // purpose statement: retrieves all active users (Admins and Employees) + // use case: for admin to see all current users on user management page (current users tab) +async getAllActiveUsers(): Promise { + this.logger.log('Starting getAllActiveUsers - Fetching all active users (Admin and Employee)'); const params = { TableName: process.env.DYNAMODB_USER_TABLE_NAME || "TABLE_FAILURE", FilterExpression: "#pos IN (:admin, :employee)", @@ -503,18 +535,21 @@ export class UserService { }; try { + this.logger.log('Executing DynamoDB scan with filter for Admin and Employee users...'); const result = await this.dynamoDb.scan(params).promise(); if (!result.Items) { this.logger.error("No active users found."); this.logger.error("DynamoDB scan result:", result); throw new NotFoundException("No active users found."); } + const users: User[] = (result.Items || []).map((item) => ({ userId: item.userId, // Assign name to userId position: item.position as UserStatus, email: item.email, name: item.userId, // Keep name as name })); + this.logger.debug(`Fetched ${users.length} active users.`); return users; @@ -522,19 +557,25 @@ export class UserService { this.logger.error("Error scanning DynamoDB:", error); if (error instanceof NotFoundException) throw error; throw new InternalServerErrorException( - "Failed to retrieve inactive users." + "Failed to retrieve active users." ); } } - // sends email to user once account is approved, used in method above when a user + // purpose statement: sends email to user once account is approved, used in method above when a user // is added to the Employee or Admin group from Inactive async sendVerificationEmail(userEmail: string): Promise { + this.logger.log(`Starting sendVerificationEmail for email: ${userEmail}`); + + if (!userEmail || !this.isValidEmail(userEmail)) { + this.logger.error(`Invalid email address provided: ${userEmail}`); + throw new BadRequestException("Valid email address is required"); + } + // remove actual email and add to env later!! - const fromEmail = process.env.NOTIFICATION_EMAIL_SENDER || - 'c4cneu.bcan@gmail.com'; + const fromEmail = process.env.NOTIFICATION_EMAIL_SENDER || 'c4cneu.bcan@gmail.com'; const params: AWS.SES.SendEmailRequest = { Source: fromEmail, @@ -551,13 +592,20 @@ export class UserService { }; try { + this.logger.log(`Calling AWS SES to send email to ${userEmail}...`); const result = await this.ses.sendEmail(params).promise(); - this.logger.log(`Verification email sent to ${userEmail}`); + this.logger.log(`✅ Verification email sent successfully to ${userEmail}. MessageId: ${result.MessageId}`); return result; } catch (err: unknown) { this.logger.error('Error sending email: ', err); - const errMessage = (err instanceof Error) ? err.message : 'Generic'; - throw new Error(`Failed to send email: ${errMessage}`); + const errMessage = (err instanceof Error) ? err.message : 'Unknown error'; + throw new InternalServerErrorException(`Failed to send email: ${errMessage}`); } } + + // Helper method for email validation + private isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } } From d93b28bea3952fcda7cd477db8a9b1f42e09a88b Mon Sep 17 00:00:00 2001 From: lyannne Date: Fri, 23 Jan 2026 15:58:15 -0500 Subject: [PATCH 07/10] swagger docs and types made, also switched change-roles route to a patch instead of post since that made more sense --- backend/src/user/types/user.types.ts | 29 +++++++ backend/src/user/user.controller.ts | 124 ++++++++++++++++++++++++--- backend/src/user/user.service.ts | 5 +- 3 files changed, 143 insertions(+), 15 deletions(-) diff --git a/backend/src/user/types/user.types.ts b/backend/src/user/types/user.types.ts index e69de29b..83423f7d 100644 --- a/backend/src/user/types/user.types.ts +++ b/backend/src/user/types/user.types.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { UserStatus } from '../../../../middle-layer/types/UserStatus'; + +export class ChangeRoleBody { + user!: { + userId: string, + position: UserStatus, + email: string + }; + groupName!: UserStatus; + requestedBy!: { + userId: string, + position: UserStatus, + email: string + }; +} + +export class DeleteUserBody { + user!: { + userId: string, + position: UserStatus, + email: string + }; + requestedBy!: { + userId: string, + position: UserStatus, + email: string + }; +} \ No newline at end of file diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index 8d72da9c..0833c6c9 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -1,60 +1,156 @@ -import { Controller, Get, Post, Body, Param, UseGuards } from "@nestjs/common"; +import { Controller, Get, Post, Patch, Body, Param, UseGuards } 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 { ApiResponse, ApiParam } from "@nestjs/swagger"; +import { ChangeRoleBody, DeleteUserBody } from "./types/user.types"; @Controller("user") export class UserController { constructor(private readonly userService: UserService) {} + /** + * Get all users + */ @Get() + @ApiResponse({ + status : 200, + description : "All users retrieved successfully" + }) + @ApiResponse({ + status : 500, + description : "Internal Server Error" + }) @UseGuards(VerifyUserGuard) async getAllUsers() { return await this.userService.getAllUsers(); } + + /** + * Get all inactive users + */ @Get("inactive") + @ApiResponse({ + status : 200, + description : "All inactive users retrieved successfully" + }) + @ApiResponse({ + status : 500, + description : "Internal Server Error" + }) @UseGuards(VerifyUserGuard) async getAllInactiveUsers(): Promise { return await this.userService.getAllInactiveUsers(); } + /** + * Get all active users + */ @Get("active") + @ApiResponse({ + status : 200, + description : "All active users retrieved successfully" + }) + @ApiResponse({ + status : 500, + description : "Internal Server Error" + }) @UseGuards(VerifyUserGuard) async getAllActiveUsers(): Promise { 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 : 404, + description : "Not Found" + }) + @ApiResponse({ + status : 500, + description : "Internal Server Error" + }) @UseGuards(VerifyAdminRoleGuard) async addToGroup( - @Body("user") user: User, - @Body("groupName") groupName: UserStatus, - @Body("requestedBy") requestedBy: User + @Body() changeRoleBody: ChangeRoleBody ): Promise { let newUser: User = await this.userService.addUserToGroup( - user, - groupName, - requestedBy + changeRoleBody.user, + changeRoleBody.groupName, + changeRoleBody.requestedBy ); return newUser as User; } + /** + * Delete a user + */ @Post("delete-user") + @ApiResponse({ + status : 201, + description : "User deleted successfully" + }) + @ApiResponse({ + status : 400, + description : "{Error encountered}" + }) + @ApiResponse({ + status : 401, + description : "Unauthorized" + }) + @ApiResponse({ + status : 404, + description : "Not Found" + }) + @ApiResponse({ + status : 500, + description : "Internal Server Error" + }) @UseGuards(VerifyAdminRoleGuard) async deleteUser( - @Body("user") user: User, - @Body("requestedBy") requestedBy: User + @Body() deleteUserBody: DeleteUserBody ): Promise { - let deletedUser = await this.userService.deleteUser(user, requestedBy); - return user as User; + let deletedUser = await this.userService.deleteUser(deleteUserBody.user, deleteUserBody.requestedBy); + return deletedUser as User; } + /** + * Get user by ID + */ @Get(":id") + @ApiParam({ + name: 'id', + description: 'User ID to retrieve', + required: true, + type: String + }) + @ApiResponse({ + status : 200, + description : "User retrieved successfully" + }) + @ApiResponse({ + status : 500, + description : "Internal Server Error" + }) @UseGuards(VerifyUserGuard) - async getUserById(@Param("id") userId: string) { + async getUserById(@Param('id') userId: string): Promise { return await this.userService.getUserById(userId); } } diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index 3f55ecf1..a1fadaf1 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -102,6 +102,7 @@ export class UserService { const deleteResult = await this.dynamoDb.delete(deleteParams).promise(); if (!deleteResult.Attributes) { + this.logger.error(`DynamoDB delete did not return deleted attributes for ${username}`); throw new InternalServerErrorException( "Failed to delete user from database" ); @@ -275,6 +276,9 @@ async addUserToGroup( requestedBy.position === UserStatus.Admin && groupName !== UserStatus.Admin ) { + this.logger.warn( + `Administrator ${requestedBy.userId} attempted to demote themselves` + ); throw new BadRequestException( "Administrators cannot demote themselves" ); @@ -542,7 +546,6 @@ async getAllActiveUsers(): Promise { this.logger.error("DynamoDB scan result:", result); throw new NotFoundException("No active users found."); } - const users: User[] = (result.Items || []).map((item) => ({ userId: item.userId, // Assign name to userId position: item.position as UserStatus, From 29c297ab90756e53f020a428b7645d86ddf979a7 Mon Sep 17 00:00:00 2001 From: lyannne Date: Sat, 24 Jan 2026 15:39:38 -0500 Subject: [PATCH 08/10] created VerifyAdminOrEmployeeRoleGuard since inactive accounts shouldnt be able to make any API calls, changed verifyuserguard instances to that --- backend/src/guards/auth.guard.ts | 54 +++++++++++++++++++++++++++++ backend/src/user/user.controller.ts | 18 ++++++---- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/backend/src/guards/auth.guard.ts b/backend/src/guards/auth.guard.ts index cfc9241f..3b943741 100644 --- a/backend/src/guards/auth.guard.ts +++ b/backend/src/guards/auth.guard.ts @@ -87,3 +87,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 { + 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; + } + + return true; + + } catch (error) { + this.logger.error("Token verification failed:", error); + return false; + } + } +} diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index 0833c6c9..71078449 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -2,8 +2,8 @@ import { Controller, Get, Post, Patch, Body, Param, UseGuards } from "@nestjs/co 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 { ApiResponse, ApiParam } from "@nestjs/swagger"; +import { VerifyAdminRoleGuard, VerifyUserGuard, VerifyAdminOrEmployeeRoleGuard } from "../guards/auth.guard"; +import { ApiResponse, ApiParam , ApiBearerAuth} from "@nestjs/swagger"; import { ChangeRoleBody, DeleteUserBody } from "./types/user.types"; @Controller("user") @@ -22,7 +22,8 @@ export class UserController { status : 500, description : "Internal Server Error" }) - @UseGuards(VerifyUserGuard) + @UseGuards(VerifyAdminOrEmployeeRoleGuard) + @ApiBearerAuth() async getAllUsers() { return await this.userService.getAllUsers(); } @@ -40,7 +41,8 @@ export class UserController { status : 500, description : "Internal Server Error" }) - @UseGuards(VerifyUserGuard) + @UseGuards(VerifyAdminOrEmployeeRoleGuard) + @ApiBearerAuth() async getAllInactiveUsers(): Promise { return await this.userService.getAllInactiveUsers(); } @@ -57,7 +59,8 @@ export class UserController { status : 500, description : "Internal Server Error" }) - @UseGuards(VerifyUserGuard) + @UseGuards(VerifyAdminOrEmployeeRoleGuard) + @ApiBearerAuth() async getAllActiveUsers(): Promise { console.log("Fetching all active users"); return await this.userService.getAllActiveUsers(); @@ -88,6 +91,7 @@ export class UserController { description : "Internal Server Error" }) @UseGuards(VerifyAdminRoleGuard) + @ApiBearerAuth() async addToGroup( @Body() changeRoleBody: ChangeRoleBody ): Promise { @@ -124,6 +128,7 @@ export class UserController { description : "Internal Server Error" }) @UseGuards(VerifyAdminRoleGuard) + @ApiBearerAuth() async deleteUser( @Body() deleteUserBody: DeleteUserBody ): Promise { @@ -149,7 +154,8 @@ export class UserController { status : 500, description : "Internal Server Error" }) - @UseGuards(VerifyUserGuard) + @UseGuards(VerifyAdminOrEmployeeRoleGuard) + @ApiBearerAuth() async getUserById(@Param('id') userId: string): Promise { return await this.userService.getUserById(userId); } From 20f1fc0bc8eb8253879f9d43dfd5bf64c7b58f7d Mon Sep 17 00:00:00 2001 From: lyannne Date: Sat, 24 Jan 2026 16:31:20 -0500 Subject: [PATCH 09/10] made delete route a delete, changed adminguard to contain user information, changed request bodies to use that information to get the user requested --- backend/src/guards/auth.guard.ts | 8 ++++ backend/src/user/types/user.types.ts | 18 --------- backend/src/user/user.controller.ts | 58 +++++++++++++++++++++++----- backend/src/user/user.service.ts | 4 +- frontend/src/Register.tsx | 2 +- 5 files changed, 60 insertions(+), 30 deletions(-) diff --git a/backend/src/guards/auth.guard.ts b/backend/src/guards/auth.guard.ts index 3b943741..1c907612 100644 --- a/backend/src/guards/auth.guard.ts +++ b/backend/src/guards/auth.guard.ts @@ -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"); diff --git a/backend/src/user/types/user.types.ts b/backend/src/user/types/user.types.ts index 83423f7d..8cd99c2c 100644 --- a/backend/src/user/types/user.types.ts +++ b/backend/src/user/types/user.types.ts @@ -8,22 +8,4 @@ export class ChangeRoleBody { email: string }; groupName!: UserStatus; - requestedBy!: { - userId: string, - position: UserStatus, - email: string - }; -} - -export class DeleteUserBody { - user!: { - userId: string, - position: UserStatus, - email: string - }; - requestedBy!: { - userId: string, - position: UserStatus, - email: string - }; } \ No newline at end of file diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index 71078449..b6a97722 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -1,10 +1,10 @@ -import { Controller, Get, Post, Patch, 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, VerifyAdminOrEmployeeRoleGuard } from "../guards/auth.guard"; import { ApiResponse, ApiParam , ApiBearerAuth} from "@nestjs/swagger"; -import { ChangeRoleBody, DeleteUserBody } from "./types/user.types"; +import { ChangeRoleBody } from "./types/user.types"; @Controller("user") export class UserController { @@ -18,6 +18,10 @@ export class UserController { status : 200, description : "All users retrieved successfully" }) + @ApiResponse({ + status : 403, + description : "Forbidden" + }) @ApiResponse({ status : 500, description : "Internal Server Error" @@ -37,6 +41,10 @@ export class UserController { status : 200, description : "All inactive users retrieved successfully" }) + @ApiResponse({ + status : 403, + description : "Forbidden" + }) @ApiResponse({ status : 500, description : "Internal Server Error" @@ -55,6 +63,10 @@ export class UserController { status : 200, description : "All active users retrieved successfully" }) + @ApiResponse({ + status : 403, + description : "Forbidden" + }) @ApiResponse({ status : 500, description : "Internal Server Error" @@ -82,6 +94,10 @@ export class UserController { status : 401, description : "Unauthorized" }) + @ApiResponse({ + status : 403, + description : "Forbidden" + }) @ApiResponse({ status : 404, description : "Not Found" @@ -93,12 +109,16 @@ export class UserController { @UseGuards(VerifyAdminRoleGuard) @ApiBearerAuth() async addToGroup( - @Body() changeRoleBody: ChangeRoleBody + @Body() changeRoleBody: ChangeRoleBody, + @Req() req: any ): Promise { + // Get the requesting admin from the authenticated session (attached by guard) + const requestedBy: User = req.user; + let newUser: User = await this.userService.addUserToGroup( changeRoleBody.user, changeRoleBody.groupName, - changeRoleBody.requestedBy + requestedBy ); return newUser as User; } @@ -106,9 +126,15 @@ export class UserController { /** * Delete a user */ - @Post("delete-user") + @Delete("delete-user/:userId") + @ApiParam({ + name: 'userId', + description: 'ID of the user to delete', + required: true, + type: String + }) @ApiResponse({ - status : 201, + status : 200, description : "User deleted successfully" }) @ApiResponse({ @@ -119,6 +145,10 @@ export class UserController { status : 401, description : "Unauthorized" }) + @ApiResponse({ + status : 403, + description : "Forbidden" + }) @ApiResponse({ status : 404, description : "Not Found" @@ -130,10 +160,16 @@ export class UserController { @UseGuards(VerifyAdminRoleGuard) @ApiBearerAuth() async deleteUser( - @Body() deleteUserBody: DeleteUserBody + @Param('userId') userId: string, + @Req() req: any ): Promise { - let deletedUser = await this.userService.deleteUser(deleteUserBody.user, deleteUserBody.requestedBy); - return deletedUser 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); } /** @@ -150,6 +186,10 @@ export class UserController { status : 200, description : "User retrieved successfully" }) + @ApiResponse({ + status : 403, + description : "Forbidden" + }) @ApiResponse({ status : 500, description : "Internal Server Error" diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index a1fadaf1..ad46eedb 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -464,7 +464,7 @@ async addUserToGroup( // purpose statement: retrieves user by their userId // use case: not actually sure right now, maybe is there is an option for admin to click on a specific user to see details? - async getUserById(userId: string): Promise { + async getUserById(userId: string): Promise { const params = { TableName: process.env.DYNAMODB_USER_TABLE_NAME || "TABLE_FAILURE", Key: { @@ -477,7 +477,7 @@ async addUserToGroup( const data = await this.dynamoDb.get(params).promise(); this.logger.log(`✅ Successfully retrieved user ${userId}`); - return data.Item; + return data.Item as User; } catch (error) { this.logger.error(`Failed to retrieve user ${userId} from DynamoDB:`, error); throw new InternalServerErrorException('Could not retrieve user.'); diff --git a/frontend/src/Register.tsx b/frontend/src/Register.tsx index 58fe0a11..136be513 100644 --- a/frontend/src/Register.tsx +++ b/frontend/src/Register.tsx @@ -205,7 +205,7 @@ const Register = observer(() => {
- Don't have an account?{" "} + Have an account?{" "}