Skip to content

Commit

Permalink
frontend stuff done (#72)
Browse files Browse the repository at this point in the history
* sign in error handling

* error handling for auth/verify endpoint

* adding awaits to async functions

* frontend stuff done

* added backend connection with frontend review

* Update UserInterceptor to create new user in database on first sign in

* update default database name

* update app and application table

---------

Co-authored-by: ahnfikd7 <[email protected]>
Co-authored-by: Olivier Nzia <[email protected]>
  • Loading branch information
3 people authored Dec 7, 2024
1 parent 76b1d63 commit 2ef9b2d
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 40 deletions.
1 change: 1 addition & 0 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 12 additions & 8 deletions apps/backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SignInResponseDto> {
return this.authService.signin(signInDto);
async signin(
@Body() signInDto: SignInRequestDto,
): Promise<SignInResponseDto> {
return await this.authService.signin(signInDto).catch((err) => {
throw new UnauthorizedException(err.message);
});
}

@Post('/delete/:userId')
Expand Down
22 changes: 14 additions & 8 deletions apps/backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,22 @@ export class AuthService {

verifyUser(email: string, verificationCode: string): Promise<unknown> {
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);
}
},
);
});
}

Expand All @@ -108,7 +114,7 @@ export class AuthService {

const cognitoUser = new CognitoUser(userData);

return new Promise<SignInResponseDto>((resolve, reject) => {
return new Promise((resolve, reject) => {
return cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: (result) => {
resolve({
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/auth/dtos/verify-user.request.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
18 changes: 14 additions & 4 deletions apps/backend/src/interceptors/current-user.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 3 additions & 2 deletions apps/backend/src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,12 @@ export class UsersService {
return user;
}

findByEmail(email: string): Promise<User[]> {
return this.usersRepository.find({
async findByEmail(email: string): Promise<User[]> {
const users = await this.usersRepository.find({
where: { email },
relations: ['applications'],
});
return users;
}

async updateUser(
Expand Down
29 changes: 27 additions & 2 deletions apps/frontend/src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -46,6 +55,17 @@ export class ApiClient {
})) as Promise<string>;
}

public async submitReview(
accessToken: string,
reviewData: SubmitReviewRequest,
): Promise<void> {
return this.post('/api/reviews', reviewData, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
}) as Promise<void>;
}

private async get(
path: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -56,9 +76,14 @@ export class ApiClient {
.then((response) => response.data);
}

private async post(path: string, body: unknown): Promise<unknown> {
private async post(
path: string,
body: unknown,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
headers: AxiosRequestConfig<any> | undefined = undefined,
): Promise<unknown> {
return this.axiosInstance
.post(path, body)
.post(path, body, headers)
.then((response) => response.data);
}

Expand Down
4 changes: 0 additions & 4 deletions apps/frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ const router = createBrowserRouter([
]);

export const App: React.FC = () => {
useEffect(() => {
apiClient.getHello().then((res) => console.log(res));
}, []);

return <RouterProvider router={router} />;
};

Expand Down
106 changes: 96 additions & 10 deletions apps/frontend/src/components/ApplicationTables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -67,17 +72,18 @@ 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
}
return Semester.SPRING; // We will be recruiting for the spring semester during Sep - Jan
};

const getCurrentYear = (): number => {
return new Date().getFullYear();
return TODAY.getFullYear();
};

export function ApplicationTable() {
Expand All @@ -91,6 +97,41 @@ export function ApplicationTable() {
const [selectedApplication, setSelectedApplication] =
useState<Application | null>(null);

const [openReviewModal, setOpenReviewModal] = useState(false);
const [reviewComment, setReviewComment] = useState('');
const [reviewRating, setReviewRating] = useState<number>(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
Expand All @@ -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 () => {
Expand All @@ -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;
}, []);
Expand Down Expand Up @@ -258,10 +309,45 @@ export function ApplicationTable() {
}}
>
<Typography variant="body1">Reviews: None</Typography>
<Button variant="contained" size="small">
<Button
variant="contained"
size="small"
onClick={handleOpenReviewModal}
>
Start Review
</Button>
</Stack>
<Dialog open={openReviewModal} onClose={handleCloseReviewModal}>
<DialogTitle>Write Review</DialogTitle>
<DialogContent>
<Stack direction="row" alignItems="center" spacing={2} mb={2}>
<Typography variant="body1">Rating:</Typography>
<Rating
name="review-rating"
value={reviewRating}
onChange={(_, value) => setReviewRating(value || 0)}
precision={1}
/>
</Stack>
<TextField
autoFocus
margin="dense"
id="review"
label="Review Comments"
type="text"
fullWidth
multiline
rows={4}
variant="outlined"
value={reviewComment}
onChange={(e) => setReviewComment(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseReviewModal}>Cancel</Button>
<Button onClick={handleReviewSubmit}>Submit Review</Button>
</DialogActions>
</Dialog>
</>
) : null}
</Container>
Expand Down

0 comments on commit 2ef9b2d

Please sign in to comment.