Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨forms✨ #105

Merged
merged 12 commits into from
May 25, 2024
16 changes: 15 additions & 1 deletion backend/src/routes/courses.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Imports
import { Request, Response } from 'express'; // Import Request and Response types
import { CourseBodyParams, CourseQueryParams, CourseRouteParams, RateObject } from '../types';
import { CourseBodyParams, CourseQueryParams, CourseRouteParams, RateObject, UserRole } from '../types';

import { getConnection } from '../db/index';
import { User as UserDb } from '../db/User';
Expand Down Expand Up @@ -64,13 +64,27 @@ export async function postCourse(req: Request<any, any, CourseBodyParams>, res:
courseName,
courseDescription,
university,
userId,
} = req.body;

if (!courseCode || !courseName || !courseDescription || !university) {
res.status(400).json('Missing courseCode, courseName, courseDescription, or university');
return;
}

if (!userId) {
res.status(400).json('Missing userId');
return;
}

const userRepository = getConnection().getRepository(UserDb);
const user = await userRepository.findOne({ where: { userId } });

if (!user || user.role !== UserRole.ADMIN) {
res.status(403).json('Unauthorized user');
return;
}

const courseRepository = getConnection().getRepository(CourseDb);
const course = await courseRepository.findOne({ where: { courseCode } });
if (course) {
Expand Down
21 changes: 17 additions & 4 deletions backend/src/routes/exams.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
// Imports
import { Request, Response, Router } from 'express'; // Import Request and Response types
import { ExamBodyParams, ExamRouteParams } from '../types';
import { ExamBodyParams, ExamRouteParams, UserRole } from '../types';

import { getConnection } from '../db/index';
import { getConnection } from '../db';
import { Course as CourseDb } from '../db/Course';
import { Exam as ExamDb } from '../db/Exam';
import { Question as QuestionDb } from '../db/Questions';
import { User as UserDb } from '../db/User';

// Export Routers
export const router = Router();

export async function postExam(req: Request<any, any, ExamBodyParams>, res: Response) {
const {
examYear,
examSemester,
examType,
courseCode,
userId,
} = req.body;

// Check key
Expand All @@ -24,6 +24,19 @@ export async function postExam(req: Request<any, any, ExamBodyParams>, res: Resp
return;
}

if (!userId) {
res.status(400).json('Missing userId');
return;
}

const userRepository = getConnection().getRepository(UserDb);
const user = await userRepository.findOne({ where: { userId } });

if (!user || user.role !== UserRole.ADMIN) {
res.status(403).json('Unauthorized user');
return;
}

// Check course code
const courseRepository = getConnection().getRepository(CourseDb);
const course = await courseRepository.findOne({ where: { courseCode } });
Expand Down
5 changes: 4 additions & 1 deletion backend/src/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { getExamInfo, getExamQuestions, postExam } from './exams';

import { editQuestion, getQuestion, getQuestionComments, postQuestion } from './questions';

import { postUser } from './users';
import { getUserRole, postUser } from './users';

import { EVAN, healthCheck } from './health';

Expand Down Expand Up @@ -105,6 +105,9 @@ router.post('/courses', postCourse);
*
*/

// Gets a user's role
router.get('/users/:userId/role', getUserRole);

// Gets comment by comment id
router.get('/comments/:commentId', getComment);

Expand Down
21 changes: 21 additions & 0 deletions backend/src/routes/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,24 @@ export async function postUser(req: Request, res: Response) {

res.status(201).json('User Added');
}

export async function getUserRole(req: Request, res: Response) {
const { userId } = req.params;

if (!userId) {
res.status(400).json('Missing userId');
return;
}

const userRepository = getConnection().getRepository(UserDb);

// Check for user
const user = await userRepository.findOne({ where: { userId } });

if (!user) {
res.status(404).json('User not found');
return;
}

res.status(200).json(user.role);
}
10 changes: 9 additions & 1 deletion backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,16 @@ export type QuestionQueryParams = {

export type ExamBodyParams = Partial<Omit<Exam, 'examId'>> & {
courseCode?: string
userId?: string
}

export type ExamRouteParams = {
examId: number
}

export type CourseBodyParams = Course
export type CourseBodyParams = Course & {
userId?: string
}

export type CourseRouteParams = {
courseCode: string
Expand All @@ -82,3 +85,8 @@ export type RateObject = {
courseCode: string
stars: number
}

export enum UserRole {
USER = 0,
ADMIN = 1,
}
16 changes: 16 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-highlight-words": "^0.20.0",
"react-hook-form": "^7.51.5",
"react-hot-toast": "^2.4.1",
"remark": "^15.0.1",
"remark-gfm": "^4.0.0",
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/api/usePostCourse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';
import { useSWRConfig } from 'swr';
import toast from 'react-hot-toast';
import { AddCourseFormFields } from '@/types';

const ENDPOINT = `${process.env.API_URL}/api`;

export default function usePostCourse(onSuccess?: () => void) {
// use the global mutate
const { mutate } = useSWRConfig();

const postCourse = async (userId: string, course: AddCourseFormFields) => {
// send the course data
const res = await fetch(`${ENDPOINT}/courses`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId,
courseCode: course.courseCode,
courseName: course.courseName,
courseDescription: course.courseDescription,
university: course.university,
}),
});

if (res.ok) {
toast.success('Added course', { id: 'coursePost' });
// course successfully created, invalidate the courses cache so it refetches updated data
await mutate(ENDPOINT + '/courses');
onSuccess?.();
} else {
toast.error('Error adding course', { id: 'coursePostError' });
}
};

return {
postCourse,
};
}
39 changes: 39 additions & 0 deletions frontend/src/api/usePostExam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';
import { useSWRConfig } from 'swr';
import toast from 'react-hot-toast';
import { AddExamFormFields } from '@/types';

const ENDPOINT = `${process.env.API_URL}/api`;

export default function usePostExam(courseCode: string, onSuccess?: () => void) {
// use the global mutate
const { mutate } = useSWRConfig();

const postExam = async (userId: string, exam: AddExamFormFields) => {
// send the exam data
const res = await fetch(`${ENDPOINT}/exams`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId,
courseCode,
...exam,
}),
});

if (res.ok) {
toast.success('Added exam', { id: 'examPost' });
// exam successfully created, invalidate the course's exam cache so it refetches updated data
await mutate(ENDPOINT + '/courses/' + courseCode + '/exams');
onSuccess?.();
} else {
toast.error('Error adding exam', { id: 'examPostError' });
}
};

return {
postExam,
};
}
25 changes: 16 additions & 9 deletions frontend/src/api/useRecentChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,32 @@
import useSWR, { Fetcher } from 'swr';
import { RecentChange } from '@/types';
import { usePinned } from '@/api/usePins';
import { useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import toast from 'react-hot-toast';

const ENDPOINT = `${process.env.API_URL}/api/recent_changes`;

const fetcher: Fetcher<RecentChange[], string> = async (...args) => {
const res = await fetch(...args);

if (!res.ok) {
throw new Error(await res.json());
}

return res.json();
};

export default function useRecentChanges() {
const lastVisited: string | null = localStorage.getItem('lastVisited');
const { pinned } = usePinned();

const fetcher: Fetcher<RecentChange[], string> = useCallback(async (...args) => {
if (!pinned.length) {
return [];
}

const res = await fetch(...args);

if (!res.ok) {
throw new Error(await res.json());
}

return res.json();
}, [pinned]);


let url = ENDPOINT + `?lastVisited=${lastVisited}`;
pinned.forEach((p) => {
url += `&courseCodes=${p.code}`;
Expand Down
33 changes: 33 additions & 0 deletions frontend/src/api/useUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client';
import useSWR, { Fetcher } from 'swr';
import { UserRole } from '@/types';
import { useEffect } from 'react';
import toast from 'react-hot-toast';

const ENDPOINT = `${process.env.API_URL}/api/users/`;

const fetcher: Fetcher<UserRole, string> = async (...args) => {
const res = await fetch(...args);

if (!res.ok) {
throw new Error(await res.json());
}

return res.json();
};

export default function useUserRole(userId: string) {
const { data, error, isLoading } = useSWR(ENDPOINT + userId +'/role', fetcher);

useEffect(() => {
if (error) {
toast.error('Error loading user role', { id: 'userRoleError' });
}
}, [error]);

return {
userRole: data,
isLoading,
isError: error,
};
}
33 changes: 33 additions & 0 deletions frontend/src/app/courses/[courseCode]/exams/add/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client';
import requireRole from '@/app/requireRole';
import requireAuth from '@/app/requireAuth';
import { UserRole } from '@/types';
import Title from '@/components/Title';
import AddExamForm from '@/components/Exams/AddExamForm';
import { useUser } from '@auth0/nextjs-auth0/client';
import { useRouter } from 'next/navigation';
import usePostExam from '@/api/usePostExam';

function AddExam({ params }: { params: { courseCode: string } }) {
const { user } = useUser();
const router = useRouter();
const { postExam } = usePostExam(params.courseCode, () => {
router.push(`/courses/${params.courseCode}`);
});

return (
<main>
<div className="max-w-4xl w-full mx-auto flex flex-col items-center justify-center">
<Title title="Add Exam"/>
<AddExamForm
onSubmit={async (data) => {
console.log(data);
await postExam(user?.sub || '', data);
}}
/>
</div>
</main>
);
}

export default requireAuth(requireRole(AddExam, UserRole.ADMIN));
Loading
Loading