diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index a47cebe..3b0f428 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, diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts index d621e3a..ca717af 100644 --- a/apps/backend/src/auth/auth.controller.ts +++ b/apps/backend/src/auth/auth.controller.ts @@ -61,17 +61,21 @@ 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 await this.authService + .verifyUser(body.email, String(body.verificationCode)) + .catch((err) => { + throw new BadRequestException(err.message); + }); } @Post('/signin') - signin(@Body() signInDto: SignInRequestDto): Promise { - return this.authService.signin(signInDto); + async signin( + @Body() signInDto: SignInRequestDto, + ): Promise { + return await this.authService.signin(signInDto).catch((err) => { + throw new UnauthorizedException(err.message); + }); } @Post('/delete/:userId') diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index 897a396..603a71d 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({ 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 dd695d3..5ea5c3f 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; } diff --git a/apps/backend/src/interceptors/current-user.interceptor.ts b/apps/backend/src/interceptors/current-user.interceptor.ts index 0e95426..251da7b 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(); diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index f45039a..80a9582 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/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 89db6b2..70171c0 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/app.tsx b/apps/frontend/src/app.tsx index a51df65..fff9a59 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 84a204f..baf8b8f 100644 --- a/apps/frontend/src/components/ApplicationTables.tsx +++ b/apps/frontend/src/components/ApplicationTables.tsx @@ -8,12 +8,17 @@ 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 { +export enum ApplicationStage { RESUME = 'RESUME', INTERVIEW = 'INTERVIEW', ACCEPTED = 'ACCEPTED', @@ -67,9 +72,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 +83,7 @@ const getCurrentSemester = (): Semester => { }; const getCurrentYear = (): number => { - return new Date().getFullYear(); + return TODAY.getFullYear(); }; export function ApplicationTable() { @@ -91,6 +97,41 @@ 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 stageToSubmit = selectedApplication?.stage || ApplicationStage.ACCEPTED; + + 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 () => { const data = await apiClient.getAllApplications(accessToken); // Each application needs an id for the DataGrid to work @@ -102,9 +143,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 () => { @@ -113,10 +164,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; }, []); @@ -258,10 +309,45 @@ export function ApplicationTable() { }} > Reviews: None - + + Write Review + + + Rating: + setReviewRating(value || 0)} + precision={1} + /> + + setReviewComment(e.target.value)} + /> + + + + + + ) : null}