From 90a517501329f4aaf5681ea1838eec0b5dc63483 Mon Sep 17 00:00:00 2001 From: OJisMe Date: Fri, 16 Feb 2024 18:40:07 -0500 Subject: [PATCH 1/8] sign in error handling --- apps/backend/src/auth/auth.controller.ts | 5 ++++- apps/backend/src/auth/auth.service.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts index e1d7eb8c..aab13846 100644 --- a/apps/backend/src/auth/auth.controller.ts +++ b/apps/backend/src/auth/auth.controller.ts @@ -6,6 +6,7 @@ import { ParseIntPipe, Post, Request, + UnauthorizedException, UseGuards, UseInterceptors, } from '@nestjs/common'; @@ -57,7 +58,9 @@ export class AuthController { @Post('/signin') signin(@Body() signInDto: SignInRequestDto): Promise { - return this.authService.signin(signInDto); + return this.authService.signin(signInDto).catch((err) => { + throw new UnauthorizedException(err.message); + }); } // TODO implement change/forgotPassword endpoint (service methods are already implemented) diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index 9436d5a9..2f3036eb 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -117,6 +117,7 @@ export class AuthService { }); }, onFailure: (err) => { + console.log(err); reject(err); }, }); From 541a07b5b7237d14fded807242cdecdddcb66ae8 Mon Sep 17 00:00:00 2001 From: OJisMe Date: Sat, 2 Mar 2024 16:10:44 -0500 Subject: [PATCH 2/8] error handling for auth/verify endpoint --- apps/backend/src/auth/auth.controller.ts | 16 +++++++------ apps/backend/src/auth/auth.service.ts | 23 +++++++++++-------- .../src/auth/dtos/verify-user.request.dto.ts | 4 ++-- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts index aab13846..4cdc8787 100644 --- a/apps/backend/src/auth/auth.controller.ts +++ b/apps/backend/src/auth/auth.controller.ts @@ -48,16 +48,18 @@ export class AuthController { // TODO will be deprecated if we use Google OAuth @Post('/verify') - verifyUser(@Body() body: VerifyUserRequestDTO): void { - try { - this.authService.verifyUser(body.email, String(body.verificationCode)); - } catch (e) { - throw new BadRequestException(e.message); - } + async verifyUser(@Body() body: VerifyUserRequestDTO) { + return this.authService + .verifyUser(body.email, String(body.verificationCode)) + .catch((err) => { + throw new BadRequestException(err.message); + }); } @Post('/signin') - signin(@Body() signInDto: SignInRequestDto): Promise { + async signin( + @Body() signInDto: SignInRequestDto, + ): Promise { return this.authService.signin(signInDto).catch((err) => { throw new UnauthorizedException(err.message); }); diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index 2f3036eb..299e052b 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -82,16 +82,22 @@ export class AuthService { verifyUser(email: string, verificationCode: string): Promise { return new Promise((resolve, reject) => { - return new CognitoUser({ + const cognitoUser = new CognitoUser({ Username: email, Pool: this.userPool, - }).confirmRegistration(verificationCode, true, (err, result) => { - if (err) { - reject(err); - } else { - resolve(result); - } }); + + return cognitoUser.confirmRegistration( + verificationCode, + true, + (err, result) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }, + ); }); } @@ -108,7 +114,7 @@ export class AuthService { const cognitoUser = new CognitoUser(userData); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { return cognitoUser.authenticateUser(authenticationDetails, { onSuccess: (result) => { resolve({ @@ -117,7 +123,6 @@ export class AuthService { }); }, onFailure: (err) => { - console.log(err); reject(err); }, }); diff --git a/apps/backend/src/auth/dtos/verify-user.request.dto.ts b/apps/backend/src/auth/dtos/verify-user.request.dto.ts index dd695d36..5ea5c3f7 100644 --- a/apps/backend/src/auth/dtos/verify-user.request.dto.ts +++ b/apps/backend/src/auth/dtos/verify-user.request.dto.ts @@ -1,9 +1,9 @@ -import { IsEmail, IsNumber } from 'class-validator'; +import { IsEmail, IsNumberString } from 'class-validator'; export class VerifyUserRequestDTO { @IsEmail() email: string; - @IsNumber() + @IsNumberString() verificationCode: number; } From 9c296e5373c79be33ebeffb826d56d194a3ee574 Mon Sep 17 00:00:00 2001 From: OJisMe Date: Sat, 21 Sep 2024 17:24:15 -0400 Subject: [PATCH 3/8] adding awaits to asunc functions --- apps/backend/src/auth/auth.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts index 4cdc8787..8117c665 100644 --- a/apps/backend/src/auth/auth.controller.ts +++ b/apps/backend/src/auth/auth.controller.ts @@ -49,7 +49,7 @@ export class AuthController { // TODO will be deprecated if we use Google OAuth @Post('/verify') async verifyUser(@Body() body: VerifyUserRequestDTO) { - return this.authService + return await this.authService .verifyUser(body.email, String(body.verificationCode)) .catch((err) => { throw new BadRequestException(err.message); @@ -60,7 +60,7 @@ export class AuthController { async signin( @Body() signInDto: SignInRequestDto, ): Promise { - return this.authService.signin(signInDto).catch((err) => { + return await this.authService.signin(signInDto).catch((err) => { throw new UnauthorizedException(err.message); }); } From 0c473d415bba6fb4b9813bb23915644e5587dcd9 Mon Sep 17 00:00:00 2001 From: mattrwang Date: Mon, 11 Nov 2024 11:39:01 -0500 Subject: [PATCH 4/8] frontend stuff done --- .../src/applications/applications.service.ts | 2 +- .../src/components/ApplicationTables.tsx | 73 +++++++++++++++++-- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/apps/backend/src/applications/applications.service.ts b/apps/backend/src/applications/applications.service.ts index 5fc360d1..24808e01 100644 --- a/apps/backend/src/applications/applications.service.ts +++ b/apps/backend/src/applications/applications.service.ts @@ -140,7 +140,7 @@ export class ApplicationsService { year: getCurrentYear(), semester: getCurrentSemester(), }, - relations: ['reviews'], + relations: ['user'], }); const allApplicationsDto = applications.map((app) => { diff --git a/apps/frontend/src/components/ApplicationTables.tsx b/apps/frontend/src/components/ApplicationTables.tsx index 84a204f7..0f1b043d 100644 --- a/apps/frontend/src/components/ApplicationTables.tsx +++ b/apps/frontend/src/components/ApplicationTables.tsx @@ -8,11 +8,16 @@ import { ListItemText, ListItemIcon, Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Rating, } from '@mui/material'; import { useEffect, useState, useRef } from 'react'; import apiClient from '@api/apiClient'; import { DoneOutline } from '@mui/icons-material'; - enum ApplicationStage { RESUME = 'RESUME', INTERVIEW = 'INTERVIEW', @@ -91,6 +96,23 @@ export function ApplicationTable() { const [selectedApplication, setSelectedApplication] = useState(null); + const [openReviewModal, setOpenReviewModal] = useState(false); + const [reviewComment, setReviewComment] = useState(''); + const [reviewRating, setReviewRating] = useState(0); + + const handleOpenReviewModal = () => { + setOpenReviewModal(true); + }; + + const handleCloseReviewModal = () => { + setOpenReviewModal(false); + setReviewComment(''); + }; + + const handleReviewSubmit = () => { + handleCloseReviewModal(); + }; + const fetchData = async () => { const data = await apiClient.getAllApplications(accessToken); // Each application needs an id for the DataGrid to work @@ -113,11 +135,13 @@ export function ApplicationTable() { useEffect(() => { // Access token comes from OAuth redirect uri https://frontend.com/#access_token=access_token - const hash = window.location.hash; - const accessTokenMatch = hash.match(/access_token=([^&]*)/); - if (accessTokenMatch) { - setAccessToken(accessTokenMatch[1]); - } + // const hash = window.location.hash; + // const accessTokenMatch = hash.match(/access_token=([^&]*)/); + // if (accessTokenMatch) { + setAccessToken( + 'eyJraWQiOiJNR2hHMnNhS1RDb2hid2d1Y1F4aGZVcFBJYWJob3hZTGxIeVhGdlRYSjV3PSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI1ZTQwZWYwYi02Yzc0LTRkM2MtYjUzOC0wOWYzNTUxYWJjMDgiLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtZWFzdC0yLmFtYXpvbmF3cy5jb21cL3VzLWVhc3QtMl9oVHRaNU41WlYiLCJjbGllbnRfaWQiOiI0YzViOG02dG5vOWZ2bGptc2VxZ21rODJmdiIsIm9yaWdpbl9qdGkiOiI4ZmQ0NzdiYy05NDY0LTQwYzMtOTZkMC1jZTlhNjQ2ZTllYzkiLCJldmVudF9pZCI6ImRhOTVjMDAyLTVmMjItNDI4Ni1hZjU2LWJkY2Y1NmViMzFhOSIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoiYXdzLmNvZ25pdG8uc2lnbmluLnVzZXIuYWRtaW4iLCJhdXRoX3RpbWUiOjE3MzA4MTc1MTgsImV4cCI6MTczMDgyMTExOCwiaWF0IjoxNzMwODE3NTE4LCJqdGkiOiI1MDZlOTg3Ni1mMTI4LTQ3YTYtYWQ4MC0wMmRiY2MxOThhNzEiLCJ1c2VybmFtZSI6IjVlNDBlZjBiLTZjNzQtNGQzYy1iNTM4LTA5ZjM1NTFhYmMwOCJ9.Z4e7xWyiWCChNfryq2I_tyDZ-xk-Wd3_cY2ajkcBpkvbwQl_xwb9O7Ud9lr9091EOqTRVN5pJFRvrtVd8bBPU6_ea4dHhYHz1xYw7XY9PG3jgE-5yP8-GazKhcOLW7W5hlcbMsehyWxVFCjVfBj2lxGiE23xqUKMxBK8ij4VaXZ0ZT5kbdw8Q70ce8RtwIjcVGaMU7l9gna-pMUT5X_0p01Q1p58bP2wc1_X41aaUs8kNd3ByDVTkIV3bPTU3_45AEVQ0vFSdcLz4gRLmDVl_Ss5_VVvzhGTMUHiT8ujK05jSirA9Ce8V4VlgFgWo04K9jv1d7m84M9RbLo91drLVg', + ); + // } isPageRendered.current = false; }, []); @@ -258,10 +282,45 @@ export function ApplicationTable() { }} > Reviews: None - + + Write Review + + + Rating: + setReviewRating(value || 0)} + precision={1} + /> + + setReviewComment(e.target.value)} + /> + + + + + + ) : null} From 3100fd2b27e50f7414df68c5533885c0aeeb9c42 Mon Sep 17 00:00:00 2001 From: ahnfikd7 <99137905+ahnfikd7@users.noreply.github.com> Date: Tue, 3 Dec 2024 03:32:51 -0500 Subject: [PATCH 5/8] added backend connection with frontend review --- apps/frontend/src/api/apiClient.ts | 29 +++++++++++++- .../src/components/ApplicationTables.tsx | 38 ++++++++++++++++--- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 89db6b2e..70171c0b 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -2,10 +2,19 @@ import axios, { type AxiosInstance, AxiosRequestConfig } from 'axios'; import type { Application, applicationRow, + ApplicationStage, } from '@components/ApplicationTables'; + const defaultBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000'; +type SubmitReviewRequest = { + applicantId: number; + stage: ApplicationStage; + rating: number; + content: string; +}; + export class ApiClient { private axiosInstance: AxiosInstance; @@ -46,6 +55,17 @@ export class ApiClient { })) as Promise; } + public async submitReview( + accessToken: string, + reviewData: SubmitReviewRequest, + ): Promise { + return this.post('/api/reviews', reviewData, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) as Promise; + } + private async get( path: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -56,9 +76,14 @@ export class ApiClient { .then((response) => response.data); } - private async post(path: string, body: unknown): Promise { + private async post( + path: string, + body: unknown, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + headers: AxiosRequestConfig | undefined = undefined, + ): Promise { return this.axiosInstance - .post(path, body) + .post(path, body, headers) .then((response) => response.data); } diff --git a/apps/frontend/src/components/ApplicationTables.tsx b/apps/frontend/src/components/ApplicationTables.tsx index 0f1b043d..365ad535 100644 --- a/apps/frontend/src/components/ApplicationTables.tsx +++ b/apps/frontend/src/components/ApplicationTables.tsx @@ -18,7 +18,7 @@ import { import { useEffect, useState, useRef } from 'react'; import apiClient from '@api/apiClient'; import { DoneOutline } from '@mui/icons-material'; -enum ApplicationStage { +export enum ApplicationStage { RESUME = 'RESUME', INTERVIEW = 'INTERVIEW', ACCEPTED = 'ACCEPTED', @@ -108,9 +108,27 @@ export function ApplicationTable() { setOpenReviewModal(false); setReviewComment(''); }; + const stageToSubmit = selectedApplication?.stage || ApplicationStage.ACCEPTED; - const handleReviewSubmit = () => { - handleCloseReviewModal(); + const handleReviewSubmit = async () => { + if (!selectedUser || reviewRating === 0 || !reviewComment) { + alert('Please select a user, provide a rating, and add a comment.'); + return; + } + + try { + await apiClient.submitReview(accessToken, { + applicantId: selectedUser.userId, + stage: stageToSubmit, + rating: reviewRating, + content: reviewComment, + }); + alert('Review submitted successfully!'); + handleCloseReviewModal(); + } catch (error) { + console.error('Error submitting review:', error); + alert('Failed to submit review.'); + } }; const fetchData = async () => { @@ -124,9 +142,19 @@ export function ApplicationTable() { } }; + // const getApplication = async (userId: number) => { + // const application = await apiClient.getApplication(accessToken, userId); + // setSelectedApplication(application); + // }; + const getApplication = async (userId: number) => { - const application = await apiClient.getApplication(accessToken, userId); - setSelectedApplication(application); + try { + const application = await apiClient.getApplication(accessToken, userId); + setSelectedApplication(application); + } catch (error) { + console.error('Error fetching application:', error); + alert('Failed to fetch application details.'); + } }; const getFullName = async () => { From 056d1eab2869bc7f3029f4e5f28607d19c0e0f60 Mon Sep 17 00:00:00 2001 From: Olivier Nzia Date: Fri, 6 Dec 2024 09:50:33 -0500 Subject: [PATCH 6/8] Update UserInterceptor to create new user in database on first sign in --- .../interceptors/current-user.interceptor.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/interceptors/current-user.interceptor.ts b/apps/backend/src/interceptors/current-user.interceptor.ts index 0e954261..251da7bf 100644 --- a/apps/backend/src/interceptors/current-user.interceptor.ts +++ b/apps/backend/src/interceptors/current-user.interceptor.ts @@ -24,13 +24,23 @@ export class CurrentUserInterceptor implements NestInterceptor { const userEmail = cognitoUserAttributes.find( (attribute) => attribute.Name === 'email', ).Value; - const users = await this.usersService.findByEmail(userEmail); + const name = cognitoUserAttributes + .find((attribute) => attribute.Name === 'name') + .Value.split(' '); - if (users.length > 0) { - const user = users[0]; + const [firstName, lastName] = [name[0], name.at(-1)]; - request.user = user; + // check if the cognito user has a corresponding user in the database + const users = await this.usersService.findByEmail(userEmail); + let user = null; + if (users.length > 0) { + // if the user exists, use the user from the database + user = users[0]; + } else { + // if the user does not exist, create a new user in the database + user = await this.usersService.create(userEmail, firstName, lastName); } + request.user = user; } return handler.handle(); From ee26ee407766d3da048284622910cf58e83449d6 Mon Sep 17 00:00:00 2001 From: Olivier Nzia Date: Fri, 6 Dec 2024 15:37:46 -0500 Subject: [PATCH 7/8] update default database name --- apps/backend/src/app.module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index a47cebe0..3b0f428e 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -18,6 +18,7 @@ import { ApplicationsModule } from './applications/applications.module'; username: process.env.NX_DB_USERNAME, password: process.env.NX_DB_PASSWORD, autoLoadEntities: true, + database: process.env.NX_DB_DATABASE || 'c4c-ops', // entities: [join(__dirname, '**/**.entity.{ts,js}')], // Setting synchronize: true shouldn't be used in production - otherwise you can lose production data synchronize: true, From 7cd269389583551426bdc7ea1c4c8df9a9934d9d Mon Sep 17 00:00:00 2001 From: Olivier Nzia Date: Fri, 6 Dec 2024 15:39:11 -0500 Subject: [PATCH 8/8] update app and application table --- apps/backend/src/users/users.service.ts | 5 +++-- apps/frontend/src/app.tsx | 4 ---- apps/frontend/src/components/ApplicationTables.tsx | 11 ++++++----- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index f45039a6..80a95828 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -89,11 +89,12 @@ export class UsersService { return user; } - findByEmail(email: string): Promise { - return this.usersRepository.find({ + async findByEmail(email: string): Promise { + const users = await this.usersRepository.find({ where: { email }, relations: ['applications'], }); + return users; } async updateUser( diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index a51df65b..fff9a591 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -19,10 +19,6 @@ const router = createBrowserRouter([ ]); export const App: React.FC = () => { - useEffect(() => { - apiClient.getHello().then((res) => console.log(res)); - }, []); - return ; }; diff --git a/apps/frontend/src/components/ApplicationTables.tsx b/apps/frontend/src/components/ApplicationTables.tsx index 84a204f7..0e435940 100644 --- a/apps/frontend/src/components/ApplicationTables.tsx +++ b/apps/frontend/src/components/ApplicationTables.tsx @@ -67,9 +67,10 @@ export type Application = { response: Response[]; numApps: number; }; +const TODAY = new Date(); const getCurrentSemester = (): Semester => { - const month: number = new Date().getMonth(); + const month: number = TODAY.getMonth(); if (month >= 1 && month <= 7) { return Semester.FALL; // We will be recruiting for the fall semester during Feb - Aug } @@ -77,7 +78,7 @@ const getCurrentSemester = (): Semester => { }; const getCurrentYear = (): number => { - return new Date().getFullYear(); + return TODAY.getFullYear(); }; export function ApplicationTable() { @@ -113,10 +114,10 @@ export function ApplicationTable() { useEffect(() => { // Access token comes from OAuth redirect uri https://frontend.com/#access_token=access_token - const hash = window.location.hash; - const accessTokenMatch = hash.match(/access_token=([^&]*)/); + const urlParams = new URLSearchParams(window.location.hash.substring(1)); + const accessTokenMatch = urlParams.get('access_token'); if (accessTokenMatch) { - setAccessToken(accessTokenMatch[1]); + setAccessToken(accessTokenMatch); } isPageRendered.current = false; }, []);