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
88 changes: 75 additions & 13 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { Controller, Post, Body, Get, Req, Res, UseGuards, UnauthorizedException
import { AuthService } from './auth.service';
import { User } from '../types/User';
import { Response } from 'express';
import { VerifyAdminRoleGuard, VerifyUserGuard } from "../guards/auth.guard";
import { LoginBody, RegisterBody } from './types/auth.types';
import { VerifyUserGuard } from "../guards/auth.guard";
import { LoginBody, RegisterBody, SetPasswordBody, UpdateProfileBody } from './types/auth.types';
import { ApiBearerAuth, ApiResponse } from '@nestjs/swagger';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

/**
* Checks if the user has a valid session
*/
@Get('session')
async getSession(@Req() req: any) {
try {
Expand Down Expand Up @@ -55,14 +58,35 @@ export class AuthController {
status: 409,
description : "{Error encountered}"
})

async register(
@Body() body: RegisterBody
): Promise<{ message: string }> {
await this.authService.register(body.username, body.password, body.email);
return { message: 'User registered successfully' };
}


/**
* Logs in a user
*/
@Post('login')
@ApiResponse({
status: 200,
description: "User logged in successfully"
})
@ApiResponse({
status: 400,
description: "{Error encountered}"
})
@ApiResponse({
status: 401,
description: "Invalid credentials"
})
@ApiResponse({
status: 500,
description: "Internal server error"
})

async login(
@Res({ passthrough: true }) response: Response,
@Body() body:LoginBody
Expand Down Expand Up @@ -90,27 +114,65 @@ export class AuthController {
return result
}

/**
*
* Set new password
*/
@Post('set-password')
@UseGuards(VerifyUserGuard)
@ApiBearerAuth()
@ApiResponse({
status: 200,
description: "Password set successfully"
})
@ApiResponse({
status: 400,
description: "{Error encountered}"
})
@ApiResponse({
status: 401,
description: "Invalid credentials"
})
@ApiResponse({
status: 500,
description: "Internal server error"
})

async setNewPassword(
@Body('newPassword') newPassword: string,
@Body('session') session: string,
@Body('username') username: string,
@Body('email') email?: string,
): Promise<{ access_token: string }> {
return await this.authService.setNewPassword(newPassword, session, username, email);
@Body() body: SetPasswordBody
): Promise<{ message: string }> {
await this.authService.setNewPassword(body.newPassword, body.session, body.username, body.email);
return { message: 'Password has been set successfully' };
}

/**
*
* Update user profile for username, email, and position_or_role
*/
@Post('update-profile')
@UseGuards(VerifyUserGuard)
@ApiBearerAuth()
@ApiResponse({
status: 200,
description: "Profile updated successfully"
})
@ApiResponse({
status: 400,
description: "{Error encountered}"
})
@ApiResponse({
status: 401,
description: "Invalid credentials"
})
@ApiResponse({
status: 500,
description: "Internal server error"
})

async updateProfile(
@Body('username') username: string,
@Body('email') email: string,
@Body('position_or_role') position_or_role: string
@Body() body: UpdateProfileBody
): Promise<{ message: string }> {
await this.authService.updateProfile(username, email, position_or_role);
await this.authService.updateProfile(body.username, body.email, body.position_or_role);
return { message: 'Profile has been updated' };
}

Expand Down
72 changes: 67 additions & 5 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ constructor() {
}


// purpose statement: registers an user into cognito and dynamodb
// use case: new employee is joining
async register(
username: string,
password: string,
Expand All @@ -72,21 +74,25 @@ constructor() {
throw new InternalServerErrorException("Server configuration error");
}

// Validate environment variables
if (!tableName) {
this.logger.error("DynamoDB User Table Name is not defined in environment variables.");
throw new InternalServerErrorException("Server configuration error");
}

// Validate input parameters
// Validate input parameters for username, password, and email
if (!username || username.trim().length === 0) {
this.logger.error("Registration failed: Username is required");
throw new BadRequestException("Username is required");
}

if (!password || password.length < 8) {
this.logger.error("Registration failed: Password must be at least 8 characters long");
throw new BadRequestException("Password must be at least 8 characters long");
}

if (!email || !this.isValidEmail(email)) {
this.logger.error("Registration failed: Valid email address is required");
throw new BadRequestException("Valid email address is required");
}

Expand All @@ -110,7 +116,7 @@ constructor() {
const emailCheckResult = await this.dynamoDb.scan(emailCheckParams).promise();

if (emailCheckResult.Items && emailCheckResult.Items.length > 0) {
this.logger.warn(`Registration failed: Email ${email} already exists`);
this.logger.error(`Registration failed: Email ${email} already exists`);
throw new ConflictException("An account with this email already exists");
}

Expand All @@ -125,7 +131,7 @@ constructor() {
const usernameCheckResult = await this.dynamoDb.get(usernameCheckParams).promise();

if (usernameCheckResult.Item) {
this.logger.warn(`Registration failed: Username ${username} already exists`);
this.logger.error(`Registration failed: Username ${username} already exists`);
throw new ConflictException("This username is already taken");
}

Expand Down Expand Up @@ -177,6 +183,7 @@ constructor() {
} catch (passwordError: any) {
this.logger.error(`Failed to set password for ${username}:`, passwordError);


// Rollback: Delete Cognito user if password setting fails
if (cognitoUserCreated) {
this.logger.warn(`Rolling back: Deleting Cognito user ${username}...`);
Expand Down Expand Up @@ -298,7 +305,8 @@ private isValidEmail(email: string): boolean {



// Overall, needs better undefined handling and optional adding
// purpose statement: logs in an user via cognito and retrieves user data from dynamodb
// use case: employee is trying to access the app, needs to have an account already
async login(
username: string,
password: string
Expand All @@ -314,11 +322,23 @@ private isValidEmail(email: string): boolean {
const clientId = process.env.COGNITO_CLIENT_ID;
const clientSecret = process.env.COGNITO_CLIENT_SECRET;

// Validate environment variables
if (!clientId || !clientSecret) {
this.logger.error("Cognito Client ID or Secret is not defined.");
throw new Error("Cognito Client ID or Secret is not defined.");
}

// Validate input parameters for username and password
if (!username || username.trim().length === 0) {
this.logger.error("Login failed: Username is required");
throw new BadRequestException("Username is required");
}

if (!password || password.length === 0) {
this.logger.error("Login failed: Password is required");
throw new BadRequestException("Password is required");
}

const hatch = this.computeHatch(username, clientId, clientSecret);

// Todo, change constants of AUTH_FLOW types & other constants in repo
Expand Down Expand Up @@ -435,7 +455,7 @@ private isValidEmail(email: string): boolean {
if (cognitoError.code) {
switch (cognitoError.code) {
case "NotAuthorizedException":
this.logger.warn(`Login failed: ${cognitoError.message}`);
this.logger.error(`Login failed: ${cognitoError.message}`);
throw new UnauthorizedException("Incorrect username or password.");
default:
this.logger.error(
Expand All @@ -460,6 +480,8 @@ private isValidEmail(email: string): boolean {
}
}

// purpose statement: sets a new password for an user in cognito
// use case: employee changing password after forgetting password
async setNewPassword(
newPassword: string,
session: string,
Expand All @@ -474,6 +496,22 @@ private isValidEmail(email: string): boolean {
throw new Error("Cognito Client ID or Secret is not defined.");
}

// Validate input parameters for newPassword, session, and username
if (!newPassword || newPassword.length === 0) {
this.logger.error("Set New Password failed: New password is required");
throw new BadRequestException("New password is required");
}

if (!session || session.length === 0) {
this.logger.error("Set New Password failed: Session is required");
throw new BadRequestException("Session is required");
}

if (!username || username.trim().length === 0) {
this.logger.error("Set New Password failed: Username is required");
throw new BadRequestException("Username is required");
}

const hatch = this.computeHatch(username, clientId, clientSecret);

const challengeResponses: any = {
Expand All @@ -483,6 +521,7 @@ private isValidEmail(email: string): boolean {
};

if (email) {
this.logger.log("Including email in challenge responses");
challengeResponses.email = email;
}

Expand All @@ -497,6 +536,7 @@ private isValidEmail(email: string): boolean {
const response = await this.cognito
.respondToAuthChallenge(params)
.promise();
this.logger.log("Responded to auth challenge for new password");

if (
!response.AuthenticationResult ||
Expand All @@ -516,11 +556,29 @@ private isValidEmail(email: string): boolean {
}
}

// purpose statement: updates user profile info in dynamodb
// use case: employee is updating their profile information
async updateProfile(
username: string,
email: string,
position_or_role: string
) {
// Validate input parameters for username, email, and position_or_role
if (!username || username.trim().length === 0) {
this.logger.error("Update Profile failed: Username is required");
throw new BadRequestException("Username is required");
}

if (!email || email.trim().length === 0) {
this.logger.error("Update Profile failed: Email is required");
throw new BadRequestException("Email is required");
}

if (!position_or_role || position_or_role.trim().length === 0) {
this.logger.error("Update Profile failed: Position or role is required");
throw new BadRequestException("Position or role is required");
}
this.logger.log(`Updating profile for user ${username}`);
const tableName = process.env.DYNAMODB_USER_TABLE_NAME || "TABLE_FAILURE";

const params = {
Expand Down Expand Up @@ -551,6 +609,8 @@ private isValidEmail(email: string): boolean {

// Add this to auth.service.ts

// purpose statement: validates a user's session token via cognito and retrieves user data from dynamodb
// use case: employee is accessing the app with an existing session token
async validateSession(accessToken: string): Promise<any> {
try {
// Use Cognito's getUser method to validate the token
Expand All @@ -564,6 +624,7 @@ async validateSession(accessToken: string): Promise<any> {
// Extract email from user attributes
for (const attribute of getUserResponse.UserAttributes) {
if (attribute.Name === 'email') {
this.logger.log(`Extracted email from user attributes: ${attribute.Value}`);
email = attribute.Value;
break;
}
Expand All @@ -582,6 +643,7 @@ async validateSession(accessToken: string): Promise<any> {
const user = userResult.Item;

if (!user) {
this.logger.error(`User not found in database for username: ${username}`);
throw new Error('User not found in database');
}

Expand Down