From a0b50b7edc1400f677873c0a26d0daf265671c52 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Tue, 2 Apr 2024 22:39:03 +0800 Subject: [PATCH 01/37] Add files for service containerization --- backend/assignment-service/.dockerignore | 8 ++++ backend/assignment-service/Dockerfile | 13 ++++++ backend/grading-service/.dockerignore | 25 +++++++++++ backend/grading-service/Dockerfile | 13 ++++++ backend/user-service/Dockerfile | 9 ++-- backend/user-service/package.json | 1 + docker-compose.yml | 55 ++++++++++++++++++++++++ frontend/.dockerignore | 20 +++++++++ frontend/Dockerfile | 17 ++++++++ nginx/Dockerfile | 8 ++++ nginx/default.conf | 38 ++++++++++++++++ 11 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 backend/assignment-service/.dockerignore create mode 100644 backend/assignment-service/Dockerfile create mode 100644 backend/grading-service/.dockerignore create mode 100644 backend/grading-service/Dockerfile create mode 100644 docker-compose.yml create mode 100644 frontend/.dockerignore create mode 100644 frontend/Dockerfile create mode 100644 nginx/Dockerfile create mode 100644 nginx/default.conf diff --git a/backend/assignment-service/.dockerignore b/backend/assignment-service/.dockerignore new file mode 100644 index 00000000..54f659f4 --- /dev/null +++ b/backend/assignment-service/.dockerignore @@ -0,0 +1,8 @@ +# .dockerignore +/node_modules + +# testing +/coverage + +# production +/build \ No newline at end of file diff --git a/backend/assignment-service/Dockerfile b/backend/assignment-service/Dockerfile new file mode 100644 index 00000000..fb30c034 --- /dev/null +++ b/backend/assignment-service/Dockerfile @@ -0,0 +1,13 @@ +FROM node:latest + +COPY ./package.json ./yarn.lock ./ + +RUN yarn install + +COPY . . + +RUN yarn build + +EXPOSE 8080 + +CMD ["yarn", "start"] \ No newline at end of file diff --git a/backend/grading-service/.dockerignore b/backend/grading-service/.dockerignore new file mode 100644 index 00000000..a1ffa469 --- /dev/null +++ b/backend/grading-service/.dockerignore @@ -0,0 +1,25 @@ +# dependencies +./node_modules +/.pnp +.pnp.js + +# testing +/coverage +playground + +# next.js +/.next/ +/out/ + +# production +/build +/dist + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/backend/grading-service/Dockerfile b/backend/grading-service/Dockerfile new file mode 100644 index 00000000..5d2d77ac --- /dev/null +++ b/backend/grading-service/Dockerfile @@ -0,0 +1,13 @@ +FROM node:latest + +COPY ./package.json ./yarn.lock ./ + +RUN yarn install + +COPY . . + +RUN yarn build + +EXPOSE 8088 + +CMD ["yarn", "start"] \ No newline at end of file diff --git a/backend/user-service/Dockerfile b/backend/user-service/Dockerfile index a8787e1a..74db8f21 100644 --- a/backend/user-service/Dockerfile +++ b/backend/user-service/Dockerfile @@ -1,10 +1,11 @@ -FROM node:16-alpine -WORKDIR '/app' -COPY package*.json ./ +FROM node:latest + +COPY ./package.json ./yarn.lock ./ + RUN yarn install COPY . . EXPOSE 3001 -CMD ["ts-node", "index.ts"] \ No newline at end of file +CMD ["yarn", "start"] \ No newline at end of file diff --git a/backend/user-service/package.json b/backend/user-service/package.json index a256bd63..c399e81f 100644 --- a/backend/user-service/package.json +++ b/backend/user-service/package.json @@ -29,6 +29,7 @@ }, "scripts": { "dev": "ts-node index.ts", + "start": "ts-node index.ts", "lint": "eslint ./**/*.ts ./*.ts --ext .ts && prettier --check --ignore-path .gitignore .", "lint:autofix": "eslint ./**/*.ts ./*.ts --ext .ts --fix --ignore-path .gitignore . && prettier --write --ignore-path .gitignore ." } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..cbe8f8ef --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,55 @@ +version: '3' +services: + users: + container_name: users + hostname: users + build: + context: ./backend/user-service + env_file: + - ./backend/user-service/.env + ports: + - "3001:3001" + + assignment: + container_name: assignment + hostname: assignment + build: + context: ./backend/assignment-service + env_file: + - ./backend/assignment-service/.env + ports: + - "8080:8080" + + grading: + container_name: grading + hostname: grading + build: + context: ./backend/grading-service + env_file: + - ./backend/grading-service/.env + ports: + - "8088:8088" + + frontend: + container_name: frontend + hostname: frontend + build: + context: ./frontend + volumes: + - /app/node_modules + - ./frontend:/app + ports: + - "3000:3000" + + nginx: + container_name: nginx + hostname: nginx + build: + context: ./nginx + ports: + - "80:80" + depends_on: + - frontend + - users + - grading + - assignment \ No newline at end of file diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..a08a3b3b --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,20 @@ +# .dockerignore +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# vercel +.vercel diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..a1f0a083 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,17 @@ +# Use Node.js base image +FROM node:latest + +# Copy package.json and yarn.lock to container +COPY ./package.json ./yarn.lock ./ + +# Install dependencies +RUN yarn install + +# Copy rest of the application files +COPY . . + +# Expose port if necessary +EXPOSE 3000 + +# Command to run the application +CMD ["yarn", "start"] \ No newline at end of file diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 00000000..91bca380 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,8 @@ +# Use the official Nginx image as the base image +FROM nginx + +# Copy your custom Nginx configuration file to the Nginx configuration directory +COPY default.conf /etc/nginx/conf.d/default.conf + +# Start Nginx in the foreground +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 00000000..2c85d188 --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,38 @@ +upstream users { + server users:3001; +} + +upstream assignment { + server assignment:8080; +} + +upstream grading { + server grading:8088; +} + +upstream frontend { + server frontend:3000; +} + +server { + listen 80; + server_name localhost; + location /user { + rewrite /user/(.*) /$1 break; + proxy_pass http://users; + } + + location /assignment { + rewrite /assignment/api/(.*) /$1 break; + proxy_pass http://assignment; + } + + location /grading { + rewrite /grading/api/(.*) /$1 break; + proxy_pass http://grading; + } + + location / { + proxy_pass http://frontend/; + } +} \ No newline at end of file From b3f2631079c7952f8ac1af98cab0642dd79734df Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Fri, 5 Apr 2024 08:05:40 +0800 Subject: [PATCH 02/37] Fix Dockerfile for grading service and frontend --- backend/grading-service/.dockerignore | 23 +++-------------------- backend/grading-service/Dockerfile | 2 +- backend/grading-service/package.json | 2 +- frontend/Dockerfile | 2 ++ 4 files changed, 7 insertions(+), 22 deletions(-) diff --git a/backend/grading-service/.dockerignore b/backend/grading-service/.dockerignore index a1ffa469..54f659f4 100644 --- a/backend/grading-service/.dockerignore +++ b/backend/grading-service/.dockerignore @@ -1,25 +1,8 @@ -# dependencies -./node_modules -/.pnp -.pnp.js +# .dockerignore +/node_modules # testing /coverage -playground - -# next.js -/.next/ -/out/ # production -/build -/dist - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* \ No newline at end of file +/build \ No newline at end of file diff --git a/backend/grading-service/Dockerfile b/backend/grading-service/Dockerfile index 5d2d77ac..159d517f 100644 --- a/backend/grading-service/Dockerfile +++ b/backend/grading-service/Dockerfile @@ -2,7 +2,7 @@ FROM node:latest COPY ./package.json ./yarn.lock ./ -RUN yarn install +RUN yarn COPY . . diff --git a/backend/grading-service/package.json b/backend/grading-service/package.json index 25277e8b..cbd46100 100644 --- a/backend/grading-service/package.json +++ b/backend/grading-service/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "prisma generate && nodemon src/app.ts", "pure-dev": "nodemon src/app.ts", - "build": "prisma generate && tsc -p .", + "build": "prisma generate --schema='src/models/prisma/schema.prisma'&& tsc -p .", "start": "node dist/app.js", "test": "jest --coverage --verbose", "db": "prisma studio", diff --git a/frontend/Dockerfile b/frontend/Dockerfile index a1f0a083..4d977999 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -10,6 +10,8 @@ RUN yarn install # Copy rest of the application files COPY . . +RUN yarn build + # Expose port if necessary EXPOSE 3000 From c1ca40c265abccb4a29541a8dbc491ff6b96dc47 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Fri, 5 Apr 2024 08:06:16 +0800 Subject: [PATCH 03/37] Add kubernetes .yaml file for deployment --- k8s/startup.yaml | 182 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 k8s/startup.yaml diff --git a/k8s/startup.yaml b/k8s/startup.yaml new file mode 100644 index 00000000..b4620348 --- /dev/null +++ b/k8s/startup.yaml @@ -0,0 +1,182 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress3 + namespace: ingress-nginx + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + ingressClassName: webapprouting.kubernetes.azure.com + rules: + - http: + paths: + - path: /(.*) + pathType: Prefix + backend: + service: + name: frontend + port: + number: 3000 + - path: /user(/|$)(.*) + pathType: Prefix + backend: + service: + name: users + port: + number: 3001 + - path: /assignment/api/* + pathType: ImplementationSpecific + backend: + service: + name: assignment + port: + number: 8080 + - path: /grading/api/* + pathType: ImplementationSpecific + backend: + service: + name: grading + port: + number: 8088 +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend +spec: + ports: + - name: http + port: 3000 + targetPort: 3000 + selector: + app: frontend +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend +spec: + replicas: 1 + selector: + matchLabels: + app: frontend + template: + metadata: + labels: + app: frontend + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: frontend + image: fmdevsimages.azurecr.io/cs3213-frontend:latest + imagePullPolicy: Always + ports: + - containerPort: 3000 +--- +apiVersion: v1 +kind: Service +metadata: + name: users +spec: + ports: + - name: http + port: 3001 + targetPort: 3001 + selector: + app: users +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: users +spec: + replicas: 1 + selector: + matchLabels: + app: users + template: + metadata: + labels: + app: users + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: users + image: fmdevsimages.azurecr.io/cs3213-users:latest + imagePullPolicy: Always + ports: + - containerPort: 3001 +--- +apiVersion: v1 +kind: Service +metadata: + name: assignment +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: assignment +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: assignment +spec: + replicas: 1 + selector: + matchLabels: + app: assignment + template: + metadata: + labels: + app: assignment + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: assignment + image: fmdevsimages.azurecr.io/cs3213-assignment:latest + imagePullPolicy: Always + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: grading +spec: + ports: + - name: http + port: 8088 + targetPort: 8088 + selector: + app: grading +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grading +spec: + replicas: 1 + selector: + matchLabels: + app: grading + template: + metadata: + labels: + app: grading + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: grading + image: fmdevsimages.azurecr.io/cs3213-grading:latest + imagePullPolicy: Always + ports: + - containerPort: 8088 \ No newline at end of file From a349b50bbfdeff1521f3abac8f9f3e26db7b7643 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Sun, 7 Apr 2024 18:04:25 +0800 Subject: [PATCH 04/37] Add middleware on frontend to redirect user without authentication accessing certain page, implement token authentication on user service --- .../controllers/user-controller.ts | 4 +- backend/user-service/routes/user-route.ts | 9 ++-- .../unit/controller/user-controller.test.ts | 8 ++-- frontend/src/middleware/middleware.ts | 43 +++++++++++++++++++ postman/user-microservice-postman-requests | 6 +-- 5 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 frontend/src/middleware/middleware.ts diff --git a/backend/user-service/controllers/user-controller.ts b/backend/user-service/controllers/user-controller.ts index 50ab2e6f..6a4b8251 100644 --- a/backend/user-service/controllers/user-controller.ts +++ b/backend/user-service/controllers/user-controller.ts @@ -106,7 +106,7 @@ async function loginUser(req: Request, res: Response) { } } -async function getUserByUserId(req: Request, res: Response) { +async function getUserInfo(req: Request, res: Response) { const { uid } = req.body; try { const userIdSearch = await db.getUserByUserId(uid); @@ -257,7 +257,7 @@ async function clearCookie(req: Request, res: Response) { export default { registerUser, loginUser, - getUserByUserId, + getUserInfo, getUserByEmail, getAllUsers, updateUserPassword, diff --git a/backend/user-service/routes/user-route.ts b/backend/user-service/routes/user-route.ts index 9acb38cf..c7083d4e 100644 --- a/backend/user-service/routes/user-route.ts +++ b/backend/user-service/routes/user-route.ts @@ -2,15 +2,16 @@ import express from 'express'; import userController from '../controllers/user-controller'; const router = express.Router(); +const auth = require("../middleware/auth.ts"); router.post("/register", userController.registerUser); router.post("/login", userController.loginUser); -router.delete("/deleteUser", userController.deleteUser); +router.delete("/deleteUser", auth, userController.deleteUser); router.delete("/clearCookie", userController.clearCookie); -router.put("/updateUserPassword", userController.updateUserPassword); -router.put("/updateUserInfo", userController.updateUserInfo); +router.put("/updateUserPassword", auth, userController.updateUserPassword); +router.put("/updateUserInfo", auth, userController.updateUserInfo); router.get("/getAllUsers", userController.getAllUsers); -router.get("/getUserByUserId", userController.getUserByUserId); +router.get("/getUserInfo", auth, userController.getUserInfo); router.get("/getUserByEmail", userController.getUserByEmail); export default router; \ No newline at end of file diff --git a/backend/user-service/tests/unit/controller/user-controller.test.ts b/backend/user-service/tests/unit/controller/user-controller.test.ts index f76f4475..da84c8a3 100644 --- a/backend/user-service/tests/unit/controller/user-controller.test.ts +++ b/backend/user-service/tests/unit/controller/user-controller.test.ts @@ -266,7 +266,7 @@ describe('Unit Tests for /user/login endpoint', () => { }); }); -describe('Unit Tests for /user/getUserByUserId endpoint', () => { +describe('Unit Tests for /user/getUserInfo endpoint', () => { const app = createUnitTestServer(); let reqBody: any; @@ -285,7 +285,7 @@ describe('Unit Tests for /user/getUserByUserId endpoint', () => { // Act const response = await supertest(app) - .get('/user/getUserByUserId') + .get('/user/getUserInfo') .send(reqBody); // Assert @@ -300,7 +300,7 @@ describe('Unit Tests for /user/getUserByUserId endpoint', () => { // Act const response = await supertest(app) - .get('/user/getUserByUserId') + .get('/user/getUserInfo') .send(reqBody); // Assert @@ -315,7 +315,7 @@ describe('Unit Tests for /user/getUserByUserId endpoint', () => { // Act const response = await supertest(app) - .get('/user/getUserByUserId') + .get('/user/getUserInfo') .send(reqBody); // Assert diff --git a/frontend/src/middleware/middleware.ts b/frontend/src/middleware/middleware.ts new file mode 100644 index 00000000..f09639dc --- /dev/null +++ b/frontend/src/middleware/middleware.ts @@ -0,0 +1,43 @@ +import { useUserContext } from "@/contexts/user-context"; +import userService from "@/helpers/user-service/api-wrapper"; +import { NextRequest, NextResponse } from "next/server"; +import { toast } from "react-toastify"; + +export const config = { + matchers: "/:path*", +}; + +export default function middleware(request: NextRequest) { + const publicRoutes = ["/_next", "/public"]; + const loggedInRequiredRoutes = ["/user", "/assignments", "/dashboard"]; + const redirectRoutes = ["/"]; + const { user } = useUserContext(); + + const path = request.nextUrl.pathname; + + // public routes do not need to be authenticated/reroute + if (publicRoutes.some((route) => path.startsWith(route))) { + return NextResponse.next(); + } + + // TODO: check if user is authenticated, and redirect to login page if not + if (loggedInRequiredRoutes.some((route) => path.startsWith(route))) { + if (user) { + const validated = await userService.validateUser(user.uid); + if (!validated) { + toast.error("Fail to validate your identity, you need to login again"); + return NextResponse.redirect(new URL("/login")); + } + } else { + toast.error("You must login first to access this page"); + return NextResponse.redirect(new URL("/login")); + } + return NextResponse.next(); + } + // redirect to dashboard page if home page is accessed + if (redirectRoutes.includes(path)) { + return NextResponse.redirect(new URL("/dashboard", request.nextUrl.origin)); + } + + return NextResponse.next(); +} \ No newline at end of file diff --git a/postman/user-microservice-postman-requests b/postman/user-microservice-postman-requests index 9d679ad7..33e6e041 100644 --- a/postman/user-microservice-postman-requests +++ b/postman/user-microservice-postman-requests @@ -185,7 +185,7 @@ "response": [] }, { - "name": "getUserByUserId", + "name": "getUserInfo", "protocolProfileBehavior": { "disableBodyPruning": true }, @@ -202,14 +202,14 @@ } }, "url": { - "raw": "localhost:3001/user/getUserByUserId", + "raw": "localhost:3001/user/getUserInfo", "host": [ "localhost" ], "port": "3001", "path": [ "user", - "getUserByUserId" + "getUserInfo" ] } }, From 3454cbcf693e99c63dc328dc57570b91989f2efe Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Sun, 7 Apr 2024 21:22:25 +0800 Subject: [PATCH 05/37] Fixing middleware.ts in frontend --- frontend/src/middleware/middleware.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/frontend/src/middleware/middleware.ts b/frontend/src/middleware/middleware.ts index f09639dc..e44042be 100644 --- a/frontend/src/middleware/middleware.ts +++ b/frontend/src/middleware/middleware.ts @@ -7,7 +7,7 @@ export const config = { matchers: "/:path*", }; -export default function middleware(request: NextRequest) { +export default async function middleware(request: NextRequest) { const publicRoutes = ["/_next", "/public"]; const loggedInRequiredRoutes = ["/user", "/assignments", "/dashboard"]; const redirectRoutes = ["/"]; @@ -20,20 +20,14 @@ export default function middleware(request: NextRequest) { return NextResponse.next(); } - // TODO: check if user is authenticated, and redirect to login page if not if (loggedInRequiredRoutes.some((route) => path.startsWith(route))) { - if (user) { - const validated = await userService.validateUser(user.uid); - if (!validated) { - toast.error("Fail to validate your identity, you need to login again"); - return NextResponse.redirect(new URL("/login")); - } - } else { + if (!user) { toast.error("You must login first to access this page"); return NextResponse.redirect(new URL("/login")); } return NextResponse.next(); } + // redirect to dashboard page if home page is accessed if (redirectRoutes.includes(path)) { return NextResponse.redirect(new URL("/dashboard", request.nextUrl.origin)); From 0bad0a1adce35c38f0337709b518a747d02a344e Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Mon, 8 Apr 2024 19:53:09 +0800 Subject: [PATCH 06/37] Add middleware for frontend to redierct user if they are not logging in, also fixing some issues on user-service --- .../controllers/user-controller.ts | 10 ++++++-- backend/user-service/middleware/auth.ts | 7 +++--- backend/user-service/routes/user-route.ts | 3 +-- frontend/src/app/login/page.tsx | 6 ++--- frontend/src/app/user/page.tsx | 11 +++++---- .../src/helpers/user-service/api-wrapper.ts | 24 ++++++++++--------- frontend/src/middleware/middleware.ts | 14 ++++++----- 7 files changed, 42 insertions(+), 33 deletions(-) diff --git a/backend/user-service/controllers/user-controller.ts b/backend/user-service/controllers/user-controller.ts index 6a4b8251..f0ee8b67 100644 --- a/backend/user-service/controllers/user-controller.ts +++ b/backend/user-service/controllers/user-controller.ts @@ -72,7 +72,7 @@ async function loginUser(req: Request, res: Response) { .then((result) => { if (!result) { console.log("Incorrect password."); - return res.json({ + return res.status(201).json({ error: "Incorrect password.", }); } else { @@ -107,8 +107,14 @@ async function loginUser(req: Request, res: Response) { } async function getUserInfo(req: Request, res: Response) { - const { uid } = req.body; + const queryUidString = req.query.uid; + console.log(queryUidString); + if (typeof queryUidString !== 'string') { + return res.status(400).json({ error: 'Invalid uid' }); +} try { + const uid = parseInt(queryUidString, 10); + console.log(uid); const userIdSearch = await db.getUserByUserId(uid); if (userIdSearch.rows.length == 0) { console.log("User does not exist."); diff --git a/backend/user-service/middleware/auth.ts b/backend/user-service/middleware/auth.ts index b0284b5e..b69e74f1 100644 --- a/backend/user-service/middleware/auth.ts +++ b/backend/user-service/middleware/auth.ts @@ -12,10 +12,11 @@ const verifyToken = (req: Request, res: Response, next: NextFunction): void | Re } if (token) { - const decode: any = jwt.verify(token, jwtSecretKey); - if (decode) { + const decoded: any = jwt.verify(token, jwtSecretKey); + if (decoded) { console.log("verified"); - next(); + // You can perform further validation or processing here if needed + return next(); } } else { console.log("Access Denied"); diff --git a/backend/user-service/routes/user-route.ts b/backend/user-service/routes/user-route.ts index c7083d4e..d50b5520 100644 --- a/backend/user-service/routes/user-route.ts +++ b/backend/user-service/routes/user-route.ts @@ -1,9 +1,8 @@ import express from 'express'; import userController from '../controllers/user-controller'; +import auth from "../middleware/auth"; const router = express.Router(); -const auth = require("../middleware/auth.ts"); - router.post("/register", userController.registerUser); router.post("/login", userController.loginUser); router.delete("/deleteUser", auth, userController.deleteUser); diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 1c9f35d8..be553f86 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -11,7 +11,6 @@ import userService from "@/helpers/user-service/api-wrapper"; import Link from "next/link"; import EmailInput from "@/components/forms/EmailInput"; import PasswordInput from "@/components/forms/PasswordInput"; -import Cookies from "js-cookie"; import 'react-toastify/dist/ReactToastify.css'; import { toast } from 'react-toastify'; import { useUserContext } from "@/contexts/user-context"; @@ -34,16 +33,15 @@ export default function Home() { setErrorMessage("Please correct the invalid fields"); return; } - // mock for backend + try { const user = await userService.login(email, password); if (!user) { throw new Error("Cannot logging in"); } - Cookies.set('user', JSON.stringify(user), {expires: 7}); setUserContext(user); toast.success("Log in successfully!"); - router.push('/dashboard'); + router.push('/user'); } catch (err) { if (err instanceof Error) { const errorMsg = err.message; diff --git a/frontend/src/app/user/page.tsx b/frontend/src/app/user/page.tsx index 9fd5618d..4b6e2704 100644 --- a/frontend/src/app/user/page.tsx +++ b/frontend/src/app/user/page.tsx @@ -21,12 +21,13 @@ export default function Page() { toast.error("You must login to view user page"); router.push("/"); } else { - const userInfo = await userService.getUserInfo(user.uid); - if (userInfo === null) { + const retrievedUserInfo = await userService.getUserInfo(user.uid); + console.log(retrievedUserInfo); + if (retrievedUserInfo === null) { toast.error("Unable to get user data"); router.push("/"); } else { - setUserInfo(userInfo); + setUserInfo(retrievedUserInfo); } } setIsLoading(false); @@ -41,9 +42,9 @@ export default function Page() { if (user) { fetchUserInfo().catch((err) => console.log(err)); } else { - return; + setIsLoading(false); } - }, [router]); + }, [user]); return (
diff --git a/frontend/src/helpers/user-service/api-wrapper.ts b/frontend/src/helpers/user-service/api-wrapper.ts index a6624d19..8e677a6c 100644 --- a/frontend/src/helpers/user-service/api-wrapper.ts +++ b/frontend/src/helpers/user-service/api-wrapper.ts @@ -20,8 +20,8 @@ const login = async (email: string, password: string): Promise => { { withCredentials: true} ).then((res) => { if (res.status === HttpStatusCode.OK.valueOf()) { - console.log(res); - const user = res.data as User; + console.log(res.data.user); + const user = res.data.user as User; console.log(user); return user; } else { @@ -46,7 +46,6 @@ const register = async (email: string, password: string) => { course: 'course placeholder', role: 'student' }, - { withCredentials: true} ).then((res) => { console.log(res); if (res.status !== HttpStatusCode.OK.valueOf()) { @@ -58,16 +57,19 @@ const register = async (email: string, password: string) => { }; const getUserInfo = async (uid: number): Promise => { - await api.post( - `/getUserInfo`, - { - uid: uid - }, + console.log("this is the uid"); + console.log(uid); + const response = await api.get( + `/getUserInfo?uid=${uid}`, { withCredentials: true} ).then((res) => { - console.log(res); if (res.status === HttpStatusCode.OK.valueOf()) { - const userInfo = res.data as UserInfo; + const userInfo : UserInfo = { + name: res.data.name, + email: res.data.email, + bio: res.data.bio || "This person doesn't have bio", + photo: res.data.photo + } return userInfo; } else { throw new Error("We are currently encountering some issues, please try again later"); @@ -75,7 +77,7 @@ const getUserInfo = async (uid: number): Promise => { }).catch((err: Error) => { throw err; }); - return null + return response; } const userService = { diff --git a/frontend/src/middleware/middleware.ts b/frontend/src/middleware/middleware.ts index e44042be..418f6f6c 100644 --- a/frontend/src/middleware/middleware.ts +++ b/frontend/src/middleware/middleware.ts @@ -1,5 +1,5 @@ import { useUserContext } from "@/contexts/user-context"; -import userService from "@/helpers/user-service/api-wrapper"; +import Cookies from 'js-cookie'; import { NextRequest, NextResponse } from "next/server"; import { toast } from "react-toastify"; @@ -7,12 +7,12 @@ export const config = { matchers: "/:path*", }; -export default async function middleware(request: NextRequest) { +export default function Middleware(request: NextRequest) { const publicRoutes = ["/_next", "/public"]; - const loggedInRequiredRoutes = ["/user", "/assignments", "/dashboard"]; + const loggedInRequiredRoutes = ["/user", "/assignments"]; const redirectRoutes = ["/"]; const { user } = useUserContext(); - + console.log(user); const path = request.nextUrl.pathname; // public routes do not need to be authenticated/reroute @@ -20,10 +20,12 @@ export default async function middleware(request: NextRequest) { return NextResponse.next(); } + // redirect to login page if no user context is found when accessing authentication-required route if (loggedInRequiredRoutes.some((route) => path.startsWith(route))) { - if (!user) { + console.log("u reached middleware"); + if (!user || !Cookies.get('token')) { toast.error("You must login first to access this page"); - return NextResponse.redirect(new URL("/login")); + return NextResponse.redirect(new URL("/login", request.url)); } return NextResponse.next(); } From 2e69079d9351c17951001dfa91122fea6fef7554 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Mon, 8 Apr 2024 20:00:17 +0800 Subject: [PATCH 07/37] Remove some console.log on frontend and fix build eslint error --- frontend/src/app/user/page.tsx | 1 - frontend/src/helpers/user-service/api-wrapper.ts | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/frontend/src/app/user/page.tsx b/frontend/src/app/user/page.tsx index 4b6e2704..9d2d19c3 100644 --- a/frontend/src/app/user/page.tsx +++ b/frontend/src/app/user/page.tsx @@ -22,7 +22,6 @@ export default function Page() { router.push("/"); } else { const retrievedUserInfo = await userService.getUserInfo(user.uid); - console.log(retrievedUserInfo); if (retrievedUserInfo === null) { toast.error("Unable to get user data"); router.push("/"); diff --git a/frontend/src/helpers/user-service/api-wrapper.ts b/frontend/src/helpers/user-service/api-wrapper.ts index 8e677a6c..3bc8b286 100644 --- a/frontend/src/helpers/user-service/api-wrapper.ts +++ b/frontend/src/helpers/user-service/api-wrapper.ts @@ -19,10 +19,8 @@ const login = async (email: string, password: string): Promise => { }, { withCredentials: true} ).then((res) => { - if (res.status === HttpStatusCode.OK.valueOf()) { - console.log(res.data.user); + if (res.status === HttpStatusCode.OK.valueOf() && res.data.user) { const user = res.data.user as User; - console.log(user); return user; } else { console.log("invaliad email/password"); @@ -47,7 +45,6 @@ const register = async (email: string, password: string) => { role: 'student' }, ).then((res) => { - console.log(res); if (res.status !== HttpStatusCode.OK.valueOf()) { throw new Error("We are currently encountering some issues, please try again later"); } @@ -57,8 +54,6 @@ const register = async (email: string, password: string) => { }; const getUserInfo = async (uid: number): Promise => { - console.log("this is the uid"); - console.log(uid); const response = await api.get( `/getUserInfo?uid=${uid}`, { withCredentials: true} From 0768612b15493411c04eeba8f479e9ed73fc175f Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Mon, 8 Apr 2024 20:18:42 +0800 Subject: [PATCH 08/37] Fix frontend build errors --- frontend/src/helpers/user-service/api-wrapper.ts | 14 ++++++++------ frontend/src/types/user-service.d.ts | 4 ++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/src/helpers/user-service/api-wrapper.ts b/frontend/src/helpers/user-service/api-wrapper.ts index 3bc8b286..7d1e466e 100644 --- a/frontend/src/helpers/user-service/api-wrapper.ts +++ b/frontend/src/helpers/user-service/api-wrapper.ts @@ -19,8 +19,9 @@ const login = async (email: string, password: string): Promise => { }, { withCredentials: true} ).then((res) => { - if (res.status === HttpStatusCode.OK.valueOf() && res.data.user) { - const user = res.data.user as User; + if (res.status === HttpStatusCode.OK.valueOf()) { + const responseData = res.data as LoginResponse; + const user = responseData.user; return user; } else { console.log("invaliad email/password"); @@ -59,11 +60,12 @@ const getUserInfo = async (uid: number): Promise => { { withCredentials: true} ).then((res) => { if (res.status === HttpStatusCode.OK.valueOf()) { + const responseData = res.data as UserInfo const userInfo : UserInfo = { - name: res.data.name, - email: res.data.email, - bio: res.data.bio || "This person doesn't have bio", - photo: res.data.photo + name: responseData.name, + email: responseData.email, + bio: responseData.bio || "This person doesn't have bio", + photo: responseData.photo } return userInfo; } else { diff --git a/frontend/src/types/user-service.d.ts b/frontend/src/types/user-service.d.ts index fb7bb710..a967c788 100644 --- a/frontend/src/types/user-service.d.ts +++ b/frontend/src/types/user-service.d.ts @@ -12,4 +12,8 @@ interface UserInfo { email: string; bio: string; photo?: string; +} + +interface LoginResponse { + user: User; } \ No newline at end of file From c544f5c97f4a770bfdcc8ead9e5df2a1d38f40a9 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Tue, 9 Apr 2024 16:34:37 +0800 Subject: [PATCH 09/37] Fix user-service unit tests, remove middleware from frontend --- .../controllers/user-controller.ts | 6 +-- backend/user-service/middleware/auth.ts | 23 +++++++---- .../unit/controller/user-controller.test.ts | 33 +++++++++------- frontend/src/middleware/middleware.ts | 39 ------------------- 4 files changed, 36 insertions(+), 65 deletions(-) delete mode 100644 frontend/src/middleware/middleware.ts diff --git a/backend/user-service/controllers/user-controller.ts b/backend/user-service/controllers/user-controller.ts index f0ee8b67..d94450aa 100644 --- a/backend/user-service/controllers/user-controller.ts +++ b/backend/user-service/controllers/user-controller.ts @@ -84,12 +84,12 @@ async function loginUser(req: Request, res: Response) { }); } - const data = { + const payload = { email: email, - password: hash, + uid: user.uid, }; - const token = jwt.sign(data, jwtSecretKey, { expiresIn: "5d" }); + const token = jwt.sign(payload, jwtSecretKey, { expiresIn: "5d" }); res .cookie("token", token, { path: "/", diff --git a/backend/user-service/middleware/auth.ts b/backend/user-service/middleware/auth.ts index b69e74f1..4693ae24 100644 --- a/backend/user-service/middleware/auth.ts +++ b/backend/user-service/middleware/auth.ts @@ -1,14 +1,14 @@ import { Request, Response, NextFunction } from 'express'; import jwt, { Secret } from 'jsonwebtoken'; -const verifyToken = (req: Request, res: Response, next: NextFunction): void | Response> => { +const verifyToken = async (req: Request, res: Response, next: NextFunction): Promise>> => { const jwtSecretKey: Secret | undefined = process.env.JWT_SECRET_KEY; try { - const token = req.cookies.token; + const token = await req.cookies.token; if (!jwtSecretKey) { - throw new Error('JWT secret key is not defined'); + return res.status(403).send({ error: "No defined JWT secret key" }); } if (token) { @@ -17,17 +17,24 @@ const verifyToken = (req: Request, res: Response, next: NextFunction): void | Re console.log("verified"); // You can perform further validation or processing here if needed return next(); + } else { + console.log("Unauthorized, invalid token"); + return res.status(401).json({ + login: false, + data: token + }); } } else { - console.log("Access Denied"); - return res.json({ + console.log("Unauthorized, no authentication token"); + return res.status(401).json({ login: false, - data: 'error' + data: "Unauthorized, no authentication token" }); } } catch (err) { - console.log("Invalid token"); - return res.send({ err: 'Invalid token' }); + console.log("Error verifying token"); + const token = req.cookies.token; + return res.status(402).send({ error: token }); } }; diff --git a/backend/user-service/tests/unit/controller/user-controller.test.ts b/backend/user-service/tests/unit/controller/user-controller.test.ts index da84c8a3..9499ed9b 100644 --- a/backend/user-service/tests/unit/controller/user-controller.test.ts +++ b/backend/user-service/tests/unit/controller/user-controller.test.ts @@ -16,6 +16,8 @@ import { getGetAllUsersResponseBody } from "../../payload/response/get-all-users import { getUpdateUserPasswordRequestBody } from "../../payload/request/update-user-password-request-body"; import { getUpdateUserInfoRequestBody } from "../../payload/request/update-user-info-request-body"; import { getDeleteUserRequestBody } from "../../payload/request/delete-user-request-body"; +import { NextFunction } from "express"; +import auth from '../../../middleware/auth'; jest.mock("../../../psql", () => { return { @@ -24,6 +26,13 @@ jest.mock("../../../psql", () => { }; }); +jest.mock('../../../middleware/auth', () => { + return jest.fn(async (req: Request, res: Response, next: NextFunction) => { + // Always call next() without performing any authentication checks + next(); + }); +}); + describe("Unit Tests for /user/register endpoint", () => { const app = createUnitTestServer(); let reqBody: any; @@ -268,11 +277,7 @@ describe('Unit Tests for /user/login endpoint', () => { describe('Unit Tests for /user/getUserInfo endpoint', () => { const app = createUnitTestServer(); - let reqBody: any; - - beforeEach(() => { - reqBody = getGetUserRequestBody(); - }); + let uid: any; afterEach(() => { jest.clearAllMocks(); @@ -282,11 +287,11 @@ describe('Unit Tests for /user/getUserInfo endpoint', () => { it('Should return the user object', async () => { // Arrange jest.spyOn(db, 'getUserByUserId').mockResolvedValue({ rows: [getGetUserResponseBody()] } as unknown as QueryResult); - + const existingUserId = 1; + // Act const response = await supertest(app) - .get('/user/getUserInfo') - .send(reqBody); + .get(`/user/getUserInfo?uid=${existingUserId}`); // Assert expect(response.body).toEqual(getGetUserResponseBody()); @@ -297,11 +302,10 @@ describe('Unit Tests for /user/getUserInfo endpoint', () => { it('Should return an error message', async () => { // Arrange jest.spyOn(db, 'getUserByUserId').mockResolvedValue({ rows: [] } as unknown as QueryResult); - + const unexistUserId = -2; // Act const response = await supertest(app) - .get('/user/getUserInfo') - .send(reqBody); + .get(`/user/getUserInfo?uid=${unexistUserId}`); // Assert expect(response.body).toEqual({ error: 'User does not exist.' }); @@ -315,11 +319,10 @@ describe('Unit Tests for /user/getUserInfo endpoint', () => { // Act const response = await supertest(app) - .get('/user/getUserInfo') - .send(reqBody); + .get('/user/getUserInfo'); // Assert - expect(response.body).toEqual({ message: 'Error getting user by uid.' }); + expect(response.body).toEqual({ error: 'Invalid uid' }); }); }); }); @@ -657,7 +660,7 @@ describe('Unit Tests for /user/deleteUser endpoint', () => { // Arrange const reqBody = {}; // Invalid request body jest.spyOn(db, 'deleteUser').mockRejectedValue(new Error('Failed to delete user.')); - + // Act const response = await supertest(app) .delete('/user/deleteUser') diff --git a/frontend/src/middleware/middleware.ts b/frontend/src/middleware/middleware.ts deleted file mode 100644 index 418f6f6c..00000000 --- a/frontend/src/middleware/middleware.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useUserContext } from "@/contexts/user-context"; -import Cookies from 'js-cookie'; -import { NextRequest, NextResponse } from "next/server"; -import { toast } from "react-toastify"; - -export const config = { - matchers: "/:path*", -}; - -export default function Middleware(request: NextRequest) { - const publicRoutes = ["/_next", "/public"]; - const loggedInRequiredRoutes = ["/user", "/assignments"]; - const redirectRoutes = ["/"]; - const { user } = useUserContext(); - console.log(user); - const path = request.nextUrl.pathname; - - // public routes do not need to be authenticated/reroute - if (publicRoutes.some((route) => path.startsWith(route))) { - return NextResponse.next(); - } - - // redirect to login page if no user context is found when accessing authentication-required route - if (loggedInRequiredRoutes.some((route) => path.startsWith(route))) { - console.log("u reached middleware"); - if (!user || !Cookies.get('token')) { - toast.error("You must login first to access this page"); - return NextResponse.redirect(new URL("/login", request.url)); - } - return NextResponse.next(); - } - - // redirect to dashboard page if home page is accessed - if (redirectRoutes.includes(path)) { - return NextResponse.redirect(new URL("/dashboard", request.nextUrl.origin)); - } - - return NextResponse.next(); -} \ No newline at end of file From 1ec00fede0d68c83f48e2c180c29ad97b95b4b5a Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Tue, 9 Apr 2024 16:45:42 +0800 Subject: [PATCH 10/37] Fix unit test --- .../tests/unit/controller/user-controller.test.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/user-service/tests/unit/controller/user-controller.test.ts b/backend/user-service/tests/unit/controller/user-controller.test.ts index 2edd08d8..3ecbcd0a 100644 --- a/backend/user-service/tests/unit/controller/user-controller.test.ts +++ b/backend/user-service/tests/unit/controller/user-controller.test.ts @@ -279,7 +279,8 @@ describe("Unit Tests for /user/login endpoint", () => { describe('Unit Tests for /user/getUserInfo endpoint', () => { const app = createUnitTestServer(); let uid: any; - + const existingUserId = 1; + const nonExistingUserId = -1; afterEach(() => { jest.clearAllMocks(); }); @@ -287,14 +288,14 @@ describe('Unit Tests for /user/getUserInfo endpoint', () => { describe("Given a valid user ID", () => { it("Should return the user object", async () => { // Arrange + uid = existingUserId jest.spyOn(db, "getUserByUserId").mockResolvedValue({ rows: [getGetUserResponseBody()], } as unknown as QueryResult); // Act const response = await supertest(app) - .get("/user/getUserUserInfo") - .send(reqBody); + .get(`/user/getUserInfo?uid=${uid}`) // Assert expect(response.body).toEqual(getGetUserResponseBody()); }); @@ -303,14 +304,14 @@ describe('Unit Tests for /user/getUserInfo endpoint', () => { describe("Given a non-existing user ID", () => { it("Should return an error message", async () => { // Arrange + uid = nonExistingUserId; jest .spyOn(db, "getUserByUserId") .mockResolvedValue({ rows: [] } as unknown as QueryResult); // Act const response = await supertest(app) - .get("/user/getUserUserInfo") - .send(reqBody); + .get(`/user/getUserInfo?uid=${uid}`) // Assert expect(response.body).toEqual({ error: "User does not exist." }); @@ -320,14 +321,14 @@ describe('Unit Tests for /user/getUserInfo endpoint', () => { describe("Given an error while fetching user", () => { it("Should return an error message", async () => { // Arrange + uid = existingUserId; jest .spyOn(db, "getUserByUserId") .mockRejectedValue(new Error("Database error")); // Act const response = await supertest(app) - .get("/user/getUserUserInfo") - .send(reqBody); + .get(`/user/getUserInfo?uid=${uid}`) // Assert expect(response.body).toEqual({ message: "Error getting user by uid." }); From 9e35011f9ce755bc39ab88051a7088871d0041dd Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Wed, 10 Apr 2024 00:50:18 +0800 Subject: [PATCH 11/37] Add homepage link to sidebar, sign in button if user is not logged in, intergrate userpage to allow user to change their passwrod --- .../controllers/user-controller.ts | 6 +- backend/user-service/middleware/auth.ts | 7 +- frontend/src/app/login/page.tsx | 3 +- frontend/src/app/user/page.tsx | 10 +- frontend/src/components/common/Providers.tsx | 9 +- frontend/src/components/common/SideBar.tsx | 132 ++++++++++++++---- .../src/components/forms/AccountEditor.tsx | 45 +++--- .../src/components/forms/ProfileEditor.tsx | 1 + .../forms/__tests__/AccountEditor.test.tsx | 2 + .../forms/__tests__/ProfileEditor.test.tsx | 2 + frontend/src/contexts/user-context.tsx | 21 ++- .../src/helpers/user-service/api-wrapper.ts | 46 +++++- frontend/src/types/user-service.d.ts | 1 + 13 files changed, 215 insertions(+), 70 deletions(-) diff --git a/backend/user-service/controllers/user-controller.ts b/backend/user-service/controllers/user-controller.ts index d94450aa..b161ac75 100644 --- a/backend/user-service/controllers/user-controller.ts +++ b/backend/user-service/controllers/user-controller.ts @@ -170,7 +170,7 @@ async function updateUserPassword(req: Request, res: Response) { const userIdSearch = await db.getUserByUserId(uid); if (userIdSearch.rows.length == 0) { console.log("User does not exist."); - return res.json({ + return res.status(403).json({ error: "User does not exist.", }); } else if (userIdSearch.rows.length > 0) { @@ -181,7 +181,7 @@ async function updateUserPassword(req: Request, res: Response) { .then((result) => { if (!result) { console.log("Incorrect password."); - return res.json({ + return res.status(403).json({ error: "Incorrect password.", }); } else { @@ -194,7 +194,7 @@ async function updateUserPassword(req: Request, res: Response) { message: "Update password successfully.", }); } catch (err) { - return res.json({ + return res.status(404).json({ error: "Failed to update user password.", }); } diff --git a/backend/user-service/middleware/auth.ts b/backend/user-service/middleware/auth.ts index 4693ae24..bfe0133c 100644 --- a/backend/user-service/middleware/auth.ts +++ b/backend/user-service/middleware/auth.ts @@ -32,9 +32,10 @@ const verifyToken = async (req: Request, res: Response, next: NextFunction): Pro }); } } catch (err) { - console.log("Error verifying token"); - const token = req.cookies.token; - return res.status(402).send({ error: token }); + return res.status(401).json({ + login: false, + data: "Unauthorize" + }); } }; diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index be553f86..4d8a504e 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -40,7 +40,8 @@ export default function Home() { throw new Error("Cannot logging in"); } setUserContext(user); - toast.success("Log in successfully!"); + console.log(user); + toast.error("Log in successfully!"); router.push('/user'); } catch (err) { if (err instanceof Error) { diff --git a/frontend/src/app/user/page.tsx b/frontend/src/app/user/page.tsx index 9d2d19c3..55999070 100644 --- a/frontend/src/app/user/page.tsx +++ b/frontend/src/app/user/page.tsx @@ -7,6 +7,7 @@ import LogoLoading from "@/components/common/LogoLoading"; import { useUserContext } from "@/contexts/user-context"; import { useRouter } from "next/navigation"; import { toast } from 'react-toastify'; +import Cookies from "js-cookie"; export default function Page() { const [userInfo, setUserInfo] = useState({} as UserInfo); @@ -17,14 +18,15 @@ export default function Page() { useEffect(() => { const fetchUserInfo = async () => { try { - if (user === null) { + if (user === null || Cookies.get('token')) { toast.error("You must login to view user page"); router.push("/"); } else { const retrievedUserInfo = await userService.getUserInfo(user.uid); + console.log("retrieved", retrievedUserInfo); if (retrievedUserInfo === null) { - toast.error("Unable to get user data"); - router.push("/"); + toast.error("Unauthorized, please log in again"); + router.push("/login"); } else { setUserInfo(retrievedUserInfo); } @@ -41,7 +43,9 @@ export default function Page() { if (user) { fetchUserInfo().catch((err) => console.log(err)); } else { + console.log("no user context"); setIsLoading(false); + router.push("/"); } }, [user]); diff --git a/frontend/src/components/common/Providers.tsx b/frontend/src/components/common/Providers.tsx index 01056080..955e4004 100644 --- a/frontend/src/components/common/Providers.tsx +++ b/frontend/src/components/common/Providers.tsx @@ -10,11 +10,12 @@ export default function Providers({ children }: { children: ReactNode }) { return ( - - + + + {children} - - + + ); } diff --git a/frontend/src/components/common/SideBar.tsx b/frontend/src/components/common/SideBar.tsx index cbe8bd68..df21741a 100644 --- a/frontend/src/components/common/SideBar.tsx +++ b/frontend/src/components/common/SideBar.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Avatar, Button, User, Spacer } from "@nextui-org/react"; import { HiOutlineChevronDoubleLeft, HiMenu } from "react-icons/hi"; import { useRouter } from "next/navigation"; @@ -8,8 +8,14 @@ import { MdOutlineAssignment, MdOutlineUploadFile, MdOutlineLogout, + MdOutlineLogin, + MdHome } from "react-icons/md"; import classNames from "classnames"; +import { useUserContext } from "@/contexts/user-context"; +import Cookies from "js-cookie"; +import { toast } from 'react-toastify'; +import userService from "@/helpers/user-service/api-wrapper"; interface MenuItem { id: number; @@ -31,14 +37,21 @@ const menuItems: MenuItem[] = [ icon: , link: "/assignments/submissions", }, + { + id: 3, + label: "Dashboard", + icon: , + link: "/dashboard", + }, ]; export default function SideBar() { const router = useRouter(); - const userName = "Jane Doe"; - const userEmail = "janedoe@u.nus.edu"; + const { user, setUserContext } = useUserContext(); const [isCollapsed, setIsCollapsed] = useState(false); const [isCollapsible, setIsCollapsible] = useState(false); + const [isLoggedIn, setLoggedIn] = useState(false); + const [userInfo, setUserInfo] = useState({} as UserInfo); const wrapperClasses = classNames( "h-screen px-4 pt-8 pb-4 bg-lightgrey text-black flex flex-col", @@ -60,6 +73,42 @@ export default function SideBar() { router.push(route); }; + const handleLoggingOut = () => { + localStorage.removeItem('userContext'); + setUserContext(null); + } + + const handleLoggingIn = () => { + handleNavigate('/login'); + } + + useEffect(() => { + const fetchUserInfo = async () => { + try { + if (user === null || Cookies.get('token')) { + toast.error("You must login to view user page"); + } else { + const retrievedUserInfo = await userService.getUserInfo(user.uid); + if (retrievedUserInfo !== null) { + setUserInfo(retrievedUserInfo); + } + } + setLoggedIn(true); + } catch (error) { + setLoggedIn(false); + console.error("Error fetching user info for sidebar:", error); + toast.error("An unexpected error occurred"); + } + }; + + if (user) { + fetchUserInfo().catch((err) => console.log(err)); + } else { + console.log("no user context"); + setLoggedIn(false); + } + }, [user]); + return (
- + { isLoggedIn ? + + :
+ } {menuItems.map((item: MenuItem) => ( + { isLoggedIn + ? + + : + }
) : (
@@ -113,15 +175,15 @@ export default function SideBar() { > - + /> :
} {menuItems.map((item: MenuItem) => ( + { isLoggedIn + ? + + : + }
)}
diff --git a/frontend/src/components/forms/AccountEditor.tsx b/frontend/src/components/forms/AccountEditor.tsx index 92cbb789..bd6b0802 100644 --- a/frontend/src/components/forms/AccountEditor.tsx +++ b/frontend/src/components/forms/AccountEditor.tsx @@ -10,14 +10,16 @@ import { } from "@nextui-org/react"; import PasswordInput from "./PasswordInput"; import ConfirmPasswordInput from "./ConfirmPasswordInput"; +import userService from "@/helpers/user-service/api-wrapper"; export default function AccountEditor({ userInfo }: { userInfo: UserInfo }) { + const [oldPassword, setOldPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [isInvalidPassword, setIsInvalidPassword] = useState(false); const [isInvalidConfirm, setIsInvalidConfirm] = useState(false); const [accountMessage, setAccountMessage] = useState(""); const [updateCount, setUpdateCount] = useState(2); - + const handleAccountSubmit = async () => { if (newPassword == "") { setAccountMessage("Please fill in the required fields"); @@ -28,32 +30,35 @@ export default function AccountEditor({ userInfo }: { userInfo: UserInfo }) { return; } - const res = await fetch("https://jsonplaceholder.typicode.com/users/1", { - method: "PATCH", - body: JSON.stringify({ - email: userInfo.email, - password: newPassword, - }), - }).catch((err) => { - console.log(err); - return { - status: 500, - ok: false, - }; - }); - - if (!res.ok) { - setAccountMessage("An error occured, please try again later"); - } else { - setAccountMessage("Password Updated!"); - setNewPassword(""); + try { + await userService.updateUserPassword( + userInfo.uid, + oldPassword, + newPassword, + ); setUpdateCount(updateCount + 1); + setAccountMessage("Update password successfully"); + } catch (error) { + if (error instanceof Error) { + const errorMessage = error.message; + setAccountMessage(errorMessage); + } else { + setAccountMessage("Unknown error updating password, please try again"); + } + } finally { + setNewPassword(""); + setOldPassword(""); } }; return (
+ { describe("Account Editor", () => { const userInfo: UserInfo = { + uid: 1, email: "email@email.com", name: "Abc", bio: "Hello!", }; const errorInfo: UserInfo = { + uid: 2, email: "bad@email.com", name: "Abc", bio: "Hello!", diff --git a/frontend/src/components/forms/__tests__/ProfileEditor.test.tsx b/frontend/src/components/forms/__tests__/ProfileEditor.test.tsx index 6cecb73c..e876cac7 100644 --- a/frontend/src/components/forms/__tests__/ProfileEditor.test.tsx +++ b/frontend/src/components/forms/__tests__/ProfileEditor.test.tsx @@ -21,11 +21,13 @@ jest.mock("next/navigation", () => { describe("Profile Editor", () => { const userInfo: UserInfo = { + uid: 1, email: "email@email.com", name: "Abc", bio: "Hello!", }; const errorInfo: UserInfo = { + uid: 2, email: "bad@email.com", name: "Abc", bio: "Hello!", diff --git a/frontend/src/contexts/user-context.tsx b/frontend/src/contexts/user-context.tsx index 2e59b157..cc3b2864 100644 --- a/frontend/src/contexts/user-context.tsx +++ b/frontend/src/contexts/user-context.tsx @@ -1,6 +1,6 @@ "use client"; -import { createContext, useContext, ReactNode, useState } from "react"; +import { createContext, useContext, ReactNode, useState, useEffect } from "react"; interface UserContextType { user: User | null; @@ -10,18 +10,33 @@ interface UserContextType { const initialUser: User | null = null; const UserContext = createContext({ - user: initialUser, + user: null, setUserContext: () => { throw new Error("Not implemented"); } }); function UserProvider({ children }: { children: ReactNode }) { - const [user, setUser] = useState(initialUser); + const getLocalState = () : User | null => { + if (typeof window !== "undefined") { + const localUserContext = localStorage.getItem("userContext"); + if (localUserContext) { + return JSON.parse(localUserContext) as User; + } + } + return initialUser; + }; + + const [user, setUser] = useState(getLocalState() ?? initialUser); + const setUserContext = (user: User | null) => { setUser(user); } + useEffect(() => { + localStorage.setItem("userContext", JSON.stringify(user)); + }, [user]); + return ( {children} diff --git a/frontend/src/helpers/user-service/api-wrapper.ts b/frontend/src/helpers/user-service/api-wrapper.ts index 7d1e466e..88b679e9 100644 --- a/frontend/src/helpers/user-service/api-wrapper.ts +++ b/frontend/src/helpers/user-service/api-wrapper.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios, { isAxiosError } from "axios"; import HttpStatusCode from "@/types/HttpStatusCode"; const api = axios.create({ @@ -59,9 +59,11 @@ const getUserInfo = async (uid: number): Promise => { `/getUserInfo?uid=${uid}`, { withCredentials: true} ).then((res) => { + console.log("response", res); if (res.status === HttpStatusCode.OK.valueOf()) { const responseData = res.data as UserInfo const userInfo : UserInfo = { + uid: responseData.uid, name: responseData.name, email: responseData.email, bio: responseData.bio || "This person doesn't have bio", @@ -69,18 +71,52 @@ const getUserInfo = async (uid: number): Promise => { } return userInfo; } else { - throw new Error("We are currently encountering some issues, please try again later"); + return null; } - }).catch((err: Error) => { - throw err; + }).catch((_err: Error) => { + return null; }); return response; } +const updateUserPassword = async (uid: number, oldPassword: string, newPassword: string) => { + try { + const response = await api.put( + `/updateUserPassword`, + { + uid: uid, + old_password: oldPassword, + new_password: newPassword, + }, + { withCredentials: true} + ); + + console.log(response.status); + if (response.status === HttpStatusCode.OK.valueOf()) { + return; + } else { + return new Error("Unknown error updating password, please try again"); + } + } catch (error) { + console.log(error); + if (isAxiosError(error)) { + if (error.response?.status === HttpStatusCode.UNAUTHORIZED.valueOf()) { + throw new Error("Unauthorize"); + } else if (error.response?.status === HttpStatusCode.FORBIDDEN.valueOf()) { + throw new Error("Incorrect password"); + } else { + throw new Error(error.message); + } + } + throw new Error("Unknown error updating password, please try again"); + }; +}; + const userService = { login, register, - getUserInfo + getUserInfo, + updateUserPassword }; export default userService; diff --git a/frontend/src/types/user-service.d.ts b/frontend/src/types/user-service.d.ts index a967c788..5fb29145 100644 --- a/frontend/src/types/user-service.d.ts +++ b/frontend/src/types/user-service.d.ts @@ -8,6 +8,7 @@ interface User { } interface UserInfo { + uid: number; name: string; email: string; bio: string; From 29e57b9e8dc0811fe7a268c9cd9dfdb75b51a502 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Wed, 10 Apr 2024 01:04:53 +0800 Subject: [PATCH 12/37] Update user service to use HttpsStatusCode enum --- .../controllers/user-controller.ts | 13 +- backend/user-service/index.ts | 1 + .../user-service/libs/enums/HttpStatusCode.ts | 384 ++++++++++++++++++ backend/user-service/middleware/auth.ts | 8 +- 4 files changed, 396 insertions(+), 10 deletions(-) create mode 100644 backend/user-service/libs/enums/HttpStatusCode.ts diff --git a/backend/user-service/controllers/user-controller.ts b/backend/user-service/controllers/user-controller.ts index b161ac75..e4ed4f54 100644 --- a/backend/user-service/controllers/user-controller.ts +++ b/backend/user-service/controllers/user-controller.ts @@ -2,6 +2,7 @@ import { Request, Response } from "express"; import jwt, { Secret } from "jsonwebtoken"; import bcrypt from "bcrypt"; import db from "../models/user-model"; +import HttpStatusCode from "../libs/enums/HttpStatusCode"; async function registerUser(req: Request, res: Response) { const { email, password, name, major, course, role } = req.body; @@ -72,14 +73,14 @@ async function loginUser(req: Request, res: Response) { .then((result) => { if (!result) { console.log("Incorrect password."); - return res.status(201).json({ + return res.status(HttpStatusCode.FORBIDDEN.valueOf()).json({ error: "Incorrect password.", }); } else { const jwtSecretKey: Secret | undefined = process.env.JWT_SECRET_KEY; if (!jwtSecretKey) { console.error("JWT secret key is not defined."); - return res.status(500).json({ + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).json({ error: "Internal server error.", }); } @@ -110,7 +111,7 @@ async function getUserInfo(req: Request, res: Response) { const queryUidString = req.query.uid; console.log(queryUidString); if (typeof queryUidString !== 'string') { - return res.status(400).json({ error: 'Invalid uid' }); + return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ error: 'Invalid uid' }); } try { const uid = parseInt(queryUidString, 10); @@ -170,7 +171,7 @@ async function updateUserPassword(req: Request, res: Response) { const userIdSearch = await db.getUserByUserId(uid); if (userIdSearch.rows.length == 0) { console.log("User does not exist."); - return res.status(403).json({ + return res.status(HttpStatusCode.FORBIDDEN.valueOf()).json({ error: "User does not exist.", }); } else if (userIdSearch.rows.length > 0) { @@ -181,7 +182,7 @@ async function updateUserPassword(req: Request, res: Response) { .then((result) => { if (!result) { console.log("Incorrect password."); - return res.status(403).json({ + return res.status(HttpStatusCode.FORBIDDEN.valueOf()).json({ error: "Incorrect password.", }); } else { @@ -194,7 +195,7 @@ async function updateUserPassword(req: Request, res: Response) { message: "Update password successfully.", }); } catch (err) { - return res.status(404).json({ + return res.status(HttpStatusCode.NOT_FOUND.valueOf()).json({ error: "Failed to update user password.", }); } diff --git a/backend/user-service/index.ts b/backend/user-service/index.ts index f0bd9be1..e708a136 100644 --- a/backend/user-service/index.ts +++ b/backend/user-service/index.ts @@ -2,6 +2,7 @@ import express from 'express'; import cookieParser from 'cookie-parser'; import cors from 'cors'; import userRoute from './routes/user-route'; +import HttpStatusCode from "./libs/enums/HttpStatusCode"; const app = express(); diff --git a/backend/user-service/libs/enums/HttpStatusCode.ts b/backend/user-service/libs/enums/HttpStatusCode.ts new file mode 100644 index 00000000..134bb669 --- /dev/null +++ b/backend/user-service/libs/enums/HttpStatusCode.ts @@ -0,0 +1,384 @@ +"use strict"; + +/** + * Hypertext Transfer Protocol (HTTP) response status codes. + * Copied from {@link https://gist.github.com/scokmen/f813c904ef79022e84ab2409574d1b45}. + * + * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes} + */ +enum HttpStatusCode { + /** + * The server has received the request headers and the client should proceed to send the request body + * (in the case of a request for which a body needs to be sent; for example, a POST request). + * Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient. + * To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request + * and receive a 100 Continue status code in response before sending the body. The response 417 Expectation Failed indicates the request should not be continued. + */ + CONTINUE = 100, + + /** + * The requester has asked the server to switch protocols and the server has agreed to do so. + */ + SWITCHING_PROTOCOLS = 101, + + /** + * A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request. + * This code indicates that the server has received and is processing the request, but no response is available yet. + * This prevents the client from timing out and assuming the request was lost. + */ + PROCESSING = 102, + + /** + * Standard response for successful HTTP requests. + * The actual response will depend on the request method used. + * In a GET request, the response will contain an entity corresponding to the requested resource. + * In a POST request, the response will contain an entity describing or containing the result of the action. + */ + OK = 200, + + /** + * The request has been fulfilled, resulting in the creation of a new resource. + */ + CREATED = 201, + + /** + * The request has been accepted for processing, but the processing has not been completed. + * The request might or might not be eventually acted upon, and may be disallowed when processing occurs. + */ + ACCEPTED = 202, + + /** + * SINCE HTTP/1.1 + * The server is a transforming proxy that received a 200 OK from its origin, + * but is returning a modified version of the origin's response. + */ + NON_AUTHORITATIVE_INFORMATION = 203, + + /** + * The server successfully processed the request and is not returning any content. + */ + NO_CONTENT = 204, + + /** + * The server successfully processed the request, but is not returning any content. + * Unlike a 204 response, this response requires that the requester reset the document view. + */ + RESET_CONTENT = 205, + + /** + * The server is delivering only part of the resource (byte serving) due to a range header sent by the client. + * The range header is used by HTTP clients to enable resuming of interrupted downloads, + * or split a download into multiple simultaneous streams. + */ + PARTIAL_CONTENT = 206, + + /** + * The message body that follows is an XML message and can contain a number of separate response codes, + * depending on how many sub-requests were made. + */ + MULTI_STATUS = 207, + + /** + * The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response, + * and are not being included again. + */ + ALREADY_REPORTED = 208, + + /** + * The server has fulfilled a request for the resource, + * and the response is a representation of the result of one or more instance-manipulations applied to the current instance. + */ + IM_USED = 226, + + /** + * Indicates multiple options for the resource from which the client may choose (via agent-driven content negotiation). + * For example, this code could be used to present multiple video format options, + * to list files with different filename extensions, or to suggest word-sense disambiguation. + */ + MULTIPLE_CHOICES = 300, + + /** + * This and all future requests should be directed to the given URI. + */ + MOVED_PERMANENTLY = 301, + + /** + * This is an example of industry practice contradicting the standard. + * The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect + * (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302 + * with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 + * to distinguish between the two behaviours. However, some Web applications and frameworks + * use the 302 status code as if it were the 303. + */ + FOUND = 302, + + /** + * SINCE HTTP/1.1 + * The response to the request can be found under another URI using a GET method. + * When received in response to a POST (or PUT/DELETE), the client should presume that + * the server has received the data and should issue a redirect with a separate GET message. + */ + SEE_OTHER = 303, + + /** + * Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match. + * In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy. + */ + NOT_MODIFIED = 304, + + /** + * SINCE HTTP/1.1 + * The requested resource is available only through a proxy, the address for which is provided in the response. + * Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle responses with this status code, primarily for security reasons. + */ + USE_PROXY = 305, + + /** + * No longer used. Originally meant "Subsequent requests should use the specified proxy." + */ + SWITCH_PROXY = 306, + + /** + * SINCE HTTP/1.1 + * In this case, the request should be repeated with another URI; however, future requests should still use the original URI. + * In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. + * For example, a POST request should be repeated using another POST request. + */ + TEMPORARY_REDIRECT = 307, + + /** + * The request and all future requests should be repeated using another URI. + * 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change. + * So, for example, submitting a form to a permanently redirected resource may continue smoothly. + */ + PERMANENT_REDIRECT = 308, + + /** + * The server cannot or will not process the request due to an apparent client error + * (e.g., malformed request syntax, too large size, invalid request message framing, or deceptive request routing). + */ + BAD_REQUEST = 400, + + /** + * Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet + * been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the + * requested resource. See Basic access authentication and Digest access authentication. 401 semantically means + * "unauthenticated",i.e. the user does not have the necessary credentials. + */ + UNAUTHORIZED = 401, + + /** + * Reserved for future use. The original intention was that this code might be used as part of some form of digital + * cash or micro payment scheme, but that has not happened, and this code is not usually used. + * Google Developers API uses this status if a particular developer has exceeded the daily limit on requests. + */ + PAYMENT_REQUIRED = 402, + + /** + * The request was valid, but the server is refusing action. + * The user might not have the necessary permissions for a resource. + */ + FORBIDDEN = 403, + + /** + * The requested resource could not be found but may be available in the future. + * Subsequent requests by the client are permissible. + */ + NOT_FOUND = 404, + + /** + * A request method is not supported for the requested resource; + * for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource. + */ + METHOD_NOT_ALLOWED = 405, + + /** + * The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request. + */ + NOT_ACCEPTABLE = 406, + + /** + * The client must first authenticate itself with the proxy. + */ + PROXY_AUTHENTICATION_REQUIRED = 407, + + /** + * The server timed out waiting for the request. + * According to HTTP specifications: + * "The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time." + */ + REQUEST_TIMEOUT = 408, + + /** + * Indicates that the request could not be processed because of conflict in the request, + * such as an edit conflict between multiple simultaneous updates. + */ + CONFLICT = 409, + + /** + * Indicates that the resource requested is no longer available and will not be available again. + * This should be used when a resource has been intentionally removed and the resource should be purged. + * Upon receiving a 410 status code, the client should not request the resource in the future. + * Clients such as search engines should remove the resource from their indices. + * Most use cases do not require clients and search engines to purge the resource, and a "404 Not Found" may be used instead. + */ + GONE = 410, + + /** + * The request did not specify the length of its content, which is required by the requested resource. + */ + LENGTH_REQUIRED = 411, + + /** + * The server does not meet one of the preconditions that the requester put on the request. + */ + PRECONDITION_FAILED = 412, + + /** + * The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large". + */ + PAYLOAD_TOO_LARGE = 413, + + /** + * The URI provided was too long for the server to process. Often the result of too much data being encoded as a query-string of a GET request, + * in which case it should be converted to a POST request. + * Called "Request-URI Too Long" previously. + */ + URI_TOO_LONG = 414, + + /** + * The request entity has a media type which the server or resource does not support. + * For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format. + */ + UNSUPPORTED_MEDIA_TYPE = 415, + + /** + * The client has asked for a portion of the file (byte serving), but the server cannot supply that portion. + * For example, if the client asked for a part of the file that lies beyond the end of the file. + * Called "Requested Range Not Satisfiable" previously. + */ + RANGE_NOT_SATISFIABLE = 416, + + /** + * The server cannot meet the requirements of the Expect request-header field. + */ + EXPECTATION_FAILED = 417, + + /** + * This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol, + * and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by + * teapots requested to brew coffee. This HTTP status is used as an Easter egg in some websites, including Google.com. + */ + I_AM_A_TEAPOT = 418, + + /** + * The request was directed at a server that is not able to produce a response (for example because a connection reuse). + */ + MISDIRECTED_REQUEST = 421, + + /** + * The request was well-formed but was unable to be followed due to semantic errors. + */ + UNPROCESSABLE_ENTITY = 422, + + /** + * The resource that is being accessed is locked. + */ + LOCKED = 423, + + /** + * The request failed due to failure of a previous request (e.g., a PROPPATCH). + */ + FAILED_DEPENDENCY = 424, + + /** + * The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. + */ + UPGRADE_REQUIRED = 426, + + /** + * The origin server requires the request to be conditional. + * Intended to prevent "the 'lost update' problem, where a client + * GETs a resource's state, modifies it, and PUTs it back to the server, + * when meanwhile a third party has modified the state on the server, leading to a conflict." + */ + PRECONDITION_REQUIRED = 428, + + /** + * The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes. + */ + TOO_MANY_REQUESTS = 429, + + /** + * The server is unwilling to process the request because either an individual header field, + * or all the header fields collectively, are too large. + */ + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, + + /** + * A server operator has received a legal demand to deny access to a resource or to a set of resources + * that includes the requested resource. The code 451 was chosen as a reference to the novel Fahrenheit 451. + */ + UNAVAILABLE_FOR_LEGAL_REASONS = 451, + + /** + * A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. + */ + INTERNAL_SERVER_ERROR = 500, + + /** + * The server either does not recognize the request method, or it lacks the ability to fulfill the request. + * Usually this implies future availability (e.g., a new feature of a web-service API). + */ + NOT_IMPLEMENTED = 501, + + /** + * The server was acting as a gateway or proxy and received an invalid response from the upstream server. + */ + BAD_GATEWAY = 502, + + /** + * The server is currently unavailable (because it is overloaded or down for maintenance). + * Generally, this is a temporary state. + */ + SERVICE_UNAVAILABLE = 503, + + /** + * The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. + */ + GATEWAY_TIMEOUT = 504, + + /** + * The server does not support the HTTP protocol version used in the request + */ + HTTP_VERSION_NOT_SUPPORTED = 505, + + /** + * Transparent content negotiation for the request results in a circular reference. + */ + VARIANT_ALSO_NEGOTIATES = 506, + + /** + * The server is unable to store the representation needed to complete the request. + */ + INSUFFICIENT_STORAGE = 507, + + /** + * The server detected an infinite loop while processing the request. + */ + LOOP_DETECTED = 508, + + /** + * Further extensions to the request are required for the server to fulfill it. + */ + NOT_EXTENDED = 510, + + /** + * The client needs to authenticate to gain network access. + * Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used + * to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot). + */ + NETWORK_AUTHENTICATION_REQUIRED = 511, +} + +export default HttpStatusCode; diff --git a/backend/user-service/middleware/auth.ts b/backend/user-service/middleware/auth.ts index bfe0133c..3785e7e0 100644 --- a/backend/user-service/middleware/auth.ts +++ b/backend/user-service/middleware/auth.ts @@ -8,7 +8,7 @@ const verifyToken = async (req: Request, res: Response, next: NextFunction): Pro const token = await req.cookies.token; if (!jwtSecretKey) { - return res.status(403).send({ error: "No defined JWT secret key" }); + return res.status(HttpStatusCode.FORBIDDEN.valueOf()).send({ error: "No defined JWT secret key" }); } if (token) { @@ -19,20 +19,20 @@ const verifyToken = async (req: Request, res: Response, next: NextFunction): Pro return next(); } else { console.log("Unauthorized, invalid token"); - return res.status(401).json({ + return res.status(HttpStatusCode.UNAUTHORIZED.valueOf()).json({ login: false, data: token }); } } else { console.log("Unauthorized, no authentication token"); - return res.status(401).json({ + return res.status(HttpStatusCode.UNAUTHORIZED.valueOf()).json({ login: false, data: "Unauthorized, no authentication token" }); } } catch (err) { - return res.status(401).json({ + return res.status(HttpStatusCode.UNAUTHORIZED.valueOf()).json({ login: false, data: "Unauthorize" }); From a9a81f23ac34d7a641942b6e0791876d85c06067 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Wed, 10 Apr 2024 01:06:13 +0800 Subject: [PATCH 13/37] Fix yarn build error for user service --- backend/user-service/middleware/auth.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/user-service/middleware/auth.ts b/backend/user-service/middleware/auth.ts index 3785e7e0..6b363642 100644 --- a/backend/user-service/middleware/auth.ts +++ b/backend/user-service/middleware/auth.ts @@ -1,5 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import jwt, { Secret } from 'jsonwebtoken'; +import HttpStatusCode from '../libs/enums/HttpStatusCode'; const verifyToken = async (req: Request, res: Response, next: NextFunction): Promise>> => { const jwtSecretKey: Secret | undefined = process.env.JWT_SECRET_KEY; From d4ac36cfcb3eca5bf4bd46f9257edc81c6fc69f8 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Wed, 10 Apr 2024 17:14:24 +0800 Subject: [PATCH 14/37] Resolve conflict --- frontend/src/app/dashboard/page.tsx | 39 ++++++++-- frontend/src/app/login/page.tsx | 75 ++++++------------- frontend/src/app/user/page.tsx | 10 ++- frontend/src/components/common/SideBar.tsx | 57 +++++++++++++- .../src/components/forms/AccountEditor.tsx | 6 +- .../src/components/forms/ProfileEditor.tsx | 1 - .../src/helpers/user-service/api-wrapper.ts | 50 +++++++------ 7 files changed, 143 insertions(+), 95 deletions(-) diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 1069d8ea..f0b25060 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -5,23 +5,48 @@ import LogoLoading from "@/components/common/LogoLoading"; import AssignmentList from "@/components/assignment/AssignmentList"; import { useUserContext } from "@/contexts/user-context"; import { useQuery } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; +import Cookies from "js-cookie"; +import { useToast } from "@/components/ui/use-toast"; +import { useRouter } from "next/navigation"; export default function DashBoard() { const { user } = useUserContext(); + const { toast } = useToast(); + const route = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const [assignments, setAssignments] = useState(); + useEffect(() => { + if (!(user && Cookies.get('token'))) { + toast({ + title: "You must login to view dashboard", + description: "Please login and try again", + variant: "destructive", + }); + route.push('/login'); + } else { + const { data: assignments, isLoading } = useQuery({ + queryKey: ["get-assignments", user.uid], + queryFn: async () => { + return await AssignmentService.getAssignmentsByUserId(user.uid); + }, + }); + setIsLoading(isLoading); + setAssignments(assignments); + } + }, [user]); - const { data: assignments, isLoading } = useQuery({ - queryKey: ["get-assignments", user.uid], - queryFn: async () => { - return await AssignmentService.getAssignmentsByUserId(user.uid); - }, - }); return (
{isLoading ? ( ) : ( - + user ? ( + + ) : ( + <> + ) )}
); diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 88e678f5..6088730b 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -6,12 +6,7 @@ import userService from "@/helpers/user-service/api-wrapper"; import Link from "next/link"; import EmailInput from "@/components/forms/EmailInput"; import PasswordInput from "@/components/forms/PasswordInput"; -<<<<<<< HEAD import 'react-toastify/dist/ReactToastify.css'; -import { toast } from 'react-toastify'; -======= -import Cookies from "js-cookie"; ->>>>>>> master import { useUserContext } from "@/contexts/user-context"; import { useRouter } from "next/navigation"; import { useToast } from "@/components/ui/use-toast"; @@ -20,19 +15,18 @@ export default function Home() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [isInvalid, setIsInvalid] = useState(false); - - const { setUser } = useUserContext(); + const { toast } = useToast(); + const { setUserContext } = useUserContext(); const router = useRouter(); -<<<<<<< HEAD const handleSubmit = async () => { - if (email == "" || password == "") { - setErrorMessage("Please enter the required fields"); - return; - } - if (isInvalid) { - setErrorMessage("Please correct the invalid fields"); + if (email == "" || password == "" || isInvalid) { + toast({ + title: "Invalid input", + description: "Please check your input and try again", + variant: "destructive", + }); return; } @@ -43,53 +37,28 @@ export default function Home() { } setUserContext(user); console.log(user); - toast.error("Log in successfully!"); - router.push('/user'); + toast({ + title: "Login successfully", + description: "Welcome back to ITS, " + user.name, + variant: "success", + }); + router.push('/dashboard'); } catch (err) { if (err instanceof Error) { const errorMsg = err.message; - setErrorMessage(errorMsg); - } else { - setErrorMessage("We are currently encountering some issues, please try again later"); - } -======= - const { toast } = useToast(); - - const handleSubmit = () => { - if (email === "" || password === "" || isInvalid) { - toast({ - title: "Invalid input", - description: "Please check your input and try again", - variant: "destructive", - }); ->>>>>>> master - } - - userService - .login(email, password) - .then((user) => { - if (!user) { - throw new Error("Cannot logging in"); - } - - Cookies.set("user", JSON.stringify(user), { expires: 7 }); - setUser(user); - toast({ - title: "Login successfully", - description: "Welcome back to ITS, " + user.name, - variant: "success", + title: "Logging in unsucessfully", + description: errorMsg, + variant: "destructive", }); - - router.push("/dashboard"); - }) - .catch((_err) => { + } else { toast({ - title: "Login failed", - description: "Please check your email and password", + title: "Logging in unsucessfully", + description: "We are currently encountering some issues, please try again later", variant: "destructive", }); - }); + } + } }; return ( diff --git a/frontend/src/app/user/page.tsx b/frontend/src/app/user/page.tsx index 816dfd12..e06657ee 100644 --- a/frontend/src/app/user/page.tsx +++ b/frontend/src/app/user/page.tsx @@ -12,9 +12,10 @@ import Cookies from "js-cookie"; export default function Page() { const { user } = useUserContext(); const [isLoading, setIsLoading] = useState(false); - const [ userInfo, setUserInfo ] = useState(); + const [ userInfo, setUserInfo ] = useState({} as UserInfo); const router = useRouter(); const { toast } = useToast(); + useEffect(() => { const fetchUserInfo = async () => { try { @@ -48,12 +49,13 @@ export default function Page() { variant: "destructive", }); // Handle the error based on its type + router.push("/"); setIsLoading(false); } }; - if (user) { fetchUserInfo().catch((err) => console.log(err)); + setIsLoading(true); } else { console.log("no user context"); setIsLoading(false); @@ -69,11 +71,11 @@ export default function Page() {
Your Account
- +
Your Profile
- +
)} diff --git a/frontend/src/components/common/SideBar.tsx b/frontend/src/components/common/SideBar.tsx index 594b9122..c0ca5150 100644 --- a/frontend/src/components/common/SideBar.tsx +++ b/frontend/src/components/common/SideBar.tsx @@ -9,6 +9,8 @@ import UserDropdown from "./UserDropdown"; import { useUserContext } from "@/contexts/user-context"; import userService from "@/helpers/user-service/api-wrapper"; import Cookies from "js-cookie"; +import { MdOutlineLogin, MdOutlineLogout } from "react-icons/md"; +import { useToast } from "@/components/ui/use-toast"; interface MenuItem { id: number; @@ -35,8 +37,8 @@ const menuItems: MenuItem[] = [ label: "View Submissions", icon: , link: "/assignments/submissions", - }, -]; + } +] export default function SideBar() { const router = useRouter(); @@ -45,7 +47,7 @@ export default function SideBar() { const [isCollapsible, setIsCollapsible] = useState(false); const [isLoggedIn, setLoggedIn] = useState(false); const [userInfo, setUserInfo] = useState({} as UserInfo); - + const { toast } = useToast(); const wrapperClasses = classNames( "h-screen px-4 pt-8 pb-4 bg-lightgrey text-black flex flex-col", { @@ -69,6 +71,12 @@ export default function SideBar() { const handleLoggingOut = () => { localStorage.removeItem('userContext'); setUserContext(null); + toast({ + title: "Log out succesfully", + description: "see you later!", + variant: "success", + }); + router.push('/login'); } const handleLoggingIn = () => { @@ -148,6 +156,24 @@ export default function SideBar() { {item.icon} ))} + + + { isLoggedIn ? + + : + } ) : (
@@ -187,6 +213,31 @@ export default function SideBar() { {item.label} ))} + + + + { isLoggedIn ? + + : + }
)} diff --git a/frontend/src/components/forms/AccountEditor.tsx b/frontend/src/components/forms/AccountEditor.tsx index 757b10fb..475bda71 100644 --- a/frontend/src/components/forms/AccountEditor.tsx +++ b/frontend/src/components/forms/AccountEditor.tsx @@ -12,7 +12,7 @@ import PasswordInput from "./PasswordInput"; import ConfirmPasswordInput from "./ConfirmPasswordInput"; import userService from "@/helpers/user-service/api-wrapper"; -export default function AccountEditor({ userInfo }: { userInfo: UserInfo }) { +export default function AccountEditor({ uid, userInfo }: { uid: number, userInfo: UserInfo }) { const [oldPassword, setOldPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [isInvalidPassword, setIsInvalidPassword] = useState(false); @@ -21,7 +21,7 @@ export default function AccountEditor({ userInfo }: { userInfo: UserInfo }) { const [updateCount, setUpdateCount] = useState(2); const handleAccountSubmit = async () => { - if (newPassword == "") { + if (newPassword == "" || oldPassword == "") { setAccountMessage("Please fill in the required fields"); return; } @@ -32,7 +32,7 @@ export default function AccountEditor({ userInfo }: { userInfo: UserInfo }) { try { await userService.updateUserPassword( - userInfo.uid, + uid, oldPassword, newPassword, ); diff --git a/frontend/src/components/forms/ProfileEditor.tsx b/frontend/src/components/forms/ProfileEditor.tsx index c222db64..833d331e 100644 --- a/frontend/src/components/forms/ProfileEditor.tsx +++ b/frontend/src/components/forms/ProfileEditor.tsx @@ -83,7 +83,6 @@ export default function ProfileEditor({ userInfo }: { userInfo: UserInfo }) { } else { setMessage("Profile saved!"); setInfo({ - uid: info.uid, email: info.email, name: name, bio: bio, diff --git a/frontend/src/helpers/user-service/api-wrapper.ts b/frontend/src/helpers/user-service/api-wrapper.ts index 9345989b..060273e6 100644 --- a/frontend/src/helpers/user-service/api-wrapper.ts +++ b/frontend/src/helpers/user-service/api-wrapper.ts @@ -10,28 +10,34 @@ const api = axios.create({ }); const login = async (email: string, password: string): Promise => { - const response = await api.post( - `/login`, - { - email: email, - password: password - }, - { withCredentials: true} - ).then((res) => { - if (res.status === HttpStatusCode.OK.valueOf()) { - const responseData = res.data as LoginResponse; - const user = responseData.user; - return user; + try { + const response = await api.post( + `/login`, + { + email: email, + password: password + }, + { withCredentials: true} + ) + console.log(response.status); + if (response.status === HttpStatusCode.OK.valueOf()) { + return response.data.user as User; } else { - console.log("invalid email/password"); - throw new Error("Invalid Email/Password"); + throw new Error("Unknown error updating password, please try again"); } - }).catch((err: Error) => { - console.log(err); - throw err; - }); - - return response; + } catch (error) { + if (isAxiosError(error)) { + console.log(error.response?.status); + if (error.response?.status === HttpStatusCode.UNAUTHORIZED.valueOf()) { + throw new Error("Unauthorize"); + } else if (error.response?.status === HttpStatusCode.FORBIDDEN.valueOf()) { + throw new Error("Incorrect password"); + } else { + throw new Error(error.message); + } + } + throw new Error("Unknown error updating password, please try again"); + }; }; const register = async (email: string, password: string) => { @@ -59,11 +65,9 @@ const getUserInfo = async (uid: number): Promise => { `/getUserInfo?uid=${uid}`, { withCredentials: true} ).then((res) => { - console.log("response", res); if (res.status === HttpStatusCode.OK.valueOf()) { const responseData = res.data as UserInfo const userInfo : UserInfo = { - uid: responseData.uid, name: responseData.name, email: responseData.email, bio: responseData.bio || "This person doesn't have bio", @@ -90,8 +94,6 @@ const updateUserPassword = async (uid: number, oldPassword: string, newPassword: }, { withCredentials: true} ); - - console.log(response.status); if (response.status === HttpStatusCode.OK.valueOf()) { return; } else { From 7c921d786ea4520eb2c98e33c55f543e1155ca1f Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Wed, 10 Apr 2024 17:20:24 +0800 Subject: [PATCH 15/37] Ensure assignment page/component behave correctly after change --- frontend/src/app/assignments/[id]/page.tsx | 10 ++++++++++ .../assignment/create/AssignmentEditor.tsx | 14 +++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/assignments/[id]/page.tsx b/frontend/src/app/assignments/[id]/page.tsx index b6f9157b..eea6eb2f 100644 --- a/frontend/src/app/assignments/[id]/page.tsx +++ b/frontend/src/app/assignments/[id]/page.tsx @@ -19,6 +19,7 @@ import { useDisclosure, } from "@nextui-org/react"; import { useQuery } from "@tanstack/react-query"; +import Cookies from "js-cookie"; import { notFound, useRouter } from "next/navigation"; interface Props { @@ -38,6 +39,15 @@ export default function Page({ params }: Props) { // TODO: replace below code with actual user context to check for user role const { user } = useUserContext(); + if (!(user && Cookies.get('token'))) { + toast({ + title: "You must login to see Assignment page", + description: "Please login first", + variant: "destructive", + }); + router.push('/login'); + return; + } const userRole = user.role; const { diff --git a/frontend/src/components/assignment/create/AssignmentEditor.tsx b/frontend/src/components/assignment/create/AssignmentEditor.tsx index e36b7484..62cc546e 100644 --- a/frontend/src/components/assignment/create/AssignmentEditor.tsx +++ b/frontend/src/components/assignment/create/AssignmentEditor.tsx @@ -11,6 +11,7 @@ import Icons from "@/components/common/Icons"; import { useToast } from "@/components/ui/use-toast"; import { useAssignmentContext } from "@/contexts/assignment-context"; import { useUserContext } from "@/contexts/user-context"; +import Cookies from "js-cookie"; interface Props { isEditing?: boolean; @@ -49,10 +50,17 @@ export default function AssignmentEditor({ isEditing = false }: Props) { setIsPublished(assignment.isPublished); } }, []); - - const { user } = useUserContext(); - const { toast } = useToast(); + const { user } = useUserContext(); + if (!(user && Cookies.get('token'))) { + toast({ + title: "You must login to see Assignment page", + description: "Please login first", + variant: "destructive", + }); + router.push('/login'); + return
; + } const checkFormValidity = useCallback( (field: string, value: string) => { From b87858a0b45b45e3b7f967babb53df39f69f7d29 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Wed, 10 Apr 2024 17:22:46 +0800 Subject: [PATCH 16/37] Fix build error --- frontend/src/helpers/user-service/api-wrapper.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/helpers/user-service/api-wrapper.ts b/frontend/src/helpers/user-service/api-wrapper.ts index 060273e6..8fdf1b48 100644 --- a/frontend/src/helpers/user-service/api-wrapper.ts +++ b/frontend/src/helpers/user-service/api-wrapper.ts @@ -21,7 +21,9 @@ const login = async (email: string, password: string): Promise => { ) console.log(response.status); if (response.status === HttpStatusCode.OK.valueOf()) { - return response.data.user as User; + const responseData = response.data as LoginResponse; + const user = responseData.user; + return user; } else { throw new Error("Unknown error updating password, please try again"); } From 532642f8cf126b25bf5e4291198be16f905edc09 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Wed, 10 Apr 2024 17:24:05 +0800 Subject: [PATCH 17/37] Fix build error --- frontend/src/components/common/SideBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/common/SideBar.tsx b/frontend/src/components/common/SideBar.tsx index c0ca5150..93f3aed3 100644 --- a/frontend/src/components/common/SideBar.tsx +++ b/frontend/src/components/common/SideBar.tsx @@ -95,7 +95,7 @@ export default function SideBar() { setLoggedIn(true); } } - } catch (error) { + } catch (_error) { setLoggedIn(false); } }; From 2bcccb6578e35e1be4f36c1fa04ea04dd9fb0eba Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Wed, 10 Apr 2024 18:17:06 +0800 Subject: [PATCH 18/37] Fix build error --- frontend/src/app/assignments/[id]/page.tsx | 3 +- frontend/src/app/dashboard/page.tsx | 33 ++++--------------- frontend/src/app/login/page.tsx | 9 +++-- frontend/src/app/user/page.tsx | 19 ++++++----- .../assignment/create/AssignmentEditor.tsx | 7 ++-- 5 files changed, 27 insertions(+), 44 deletions(-) diff --git a/frontend/src/app/assignments/[id]/page.tsx b/frontend/src/app/assignments/[id]/page.tsx index eea6eb2f..742d767b 100644 --- a/frontend/src/app/assignments/[id]/page.tsx +++ b/frontend/src/app/assignments/[id]/page.tsx @@ -46,9 +46,8 @@ export default function Page({ params }: Props) { variant: "destructive", }); router.push('/login'); - return; } - const userRole = user.role; + const userRole = user?.role; const { data: assignment, diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index f0b25060..8f203c12 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -5,37 +5,16 @@ import LogoLoading from "@/components/common/LogoLoading"; import AssignmentList from "@/components/assignment/AssignmentList"; import { useUserContext } from "@/contexts/user-context"; import { useQuery } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; -import Cookies from "js-cookie"; -import { useToast } from "@/components/ui/use-toast"; -import { useRouter } from "next/navigation"; export default function DashBoard() { const { user } = useUserContext(); - const { toast } = useToast(); - const route = useRouter(); - const [isLoading, setIsLoading] = useState(false); - const [assignments, setAssignments] = useState(); - useEffect(() => { - if (!(user && Cookies.get('token'))) { - toast({ - title: "You must login to view dashboard", - description: "Please login and try again", - variant: "destructive", - }); - route.push('/login'); - } else { - const { data: assignments, isLoading } = useQuery({ - queryKey: ["get-assignments", user.uid], - queryFn: async () => { - return await AssignmentService.getAssignmentsByUserId(user.uid); - }, - }); - setIsLoading(isLoading); - setAssignments(assignments); - } - }, [user]); + const { data: assignments, isLoading } = useQuery({ + queryKey: ["get-assignments", user?.uid], + queryFn: async () => { + return await AssignmentService.getAssignmentsByUserId(user?.uid ?? 0); + }, + }); return (
diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 6088730b..72f80414 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -27,7 +27,6 @@ export default function Home() { description: "Please check your input and try again", variant: "destructive", }); - return; } try { @@ -74,7 +73,13 @@ export default function Home() {
)} diff --git a/frontend/src/components/assignment/create/AssignmentEditor.tsx b/frontend/src/components/assignment/create/AssignmentEditor.tsx index 62cc546e..51fb7a66 100644 --- a/frontend/src/components/assignment/create/AssignmentEditor.tsx +++ b/frontend/src/components/assignment/create/AssignmentEditor.tsx @@ -59,7 +59,6 @@ export default function AssignmentEditor({ isEditing = false }: Props) { variant: "destructive", }); router.push('/login'); - return
; } const checkFormValidity = useCallback( @@ -92,9 +91,9 @@ export default function AssignmentEditor({ isEditing = false }: Props) { description, isPublished, // if uid is alr in authors, don't add it again - authors: assignment!.authors.includes(user.uid) + authors: assignment!.authors.includes(user?.uid ?? 0) ? assignment!.authors - : [...assignment!.authors, user.uid], + : [...assignment!.authors, user?.uid ?? 0], }) .then((updatedAssignment) => { if (!updatedAssignment) { @@ -122,7 +121,7 @@ export default function AssignmentEditor({ isEditing = false }: Props) { deadline, description, isPublished, - authors: [user.uid], + authors: [user?.uid ?? 0], }) .then((createdAssignment) => { if (!createdAssignment) { From 0a9a337667b14604d9b758e25f31a2c4cf17eca8 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Wed, 10 Apr 2024 19:06:24 +0800 Subject: [PATCH 19/37] Fix ESlint rule violations --- frontend/package.json | 4 +- frontend/src/app/assignments/[id]/page.tsx | 2 +- frontend/src/app/dashboard/page.tsx | 37 ++++++++----------- frontend/src/app/login/page.tsx | 8 +++- frontend/src/app/user/page.tsx | 2 +- .../assignment/create/AssignmentEditor.tsx | 6 +-- frontend/src/components/common/SideBar.tsx | 2 +- .../src/helpers/user-service/api-wrapper.ts | 3 +- frontend/yarn.lock | 4 +- 9 files changed, 34 insertions(+), 34 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index ff5bd896..27a5b55f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,11 +28,11 @@ "clsx": "^2.1.0", "framer-motion": "^11.0.22", "html-react-parser": "^5.1.8", - "lucide-react": "^0.363.0", "js-cookie": "^3.0.5", + "lucide-react": "^0.363.0", "monaco-editor": "^0.47.0", "next": "14.1.0", - "react": "^18", + "react": "^18.2.0", "react-dom": "^18", "react-icons": "^5.0.1", "react-quill": "^2.0.0", diff --git a/frontend/src/app/assignments/[id]/page.tsx b/frontend/src/app/assignments/[id]/page.tsx index b6f9157b..c447be36 100644 --- a/frontend/src/app/assignments/[id]/page.tsx +++ b/frontend/src/app/assignments/[id]/page.tsx @@ -38,7 +38,7 @@ export default function Page({ params }: Props) { // TODO: replace below code with actual user context to check for user role const { user } = useUserContext(); - const userRole = user.role; + const userRole = user?.role ?? "student"; const { data: assignment, diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index f0b25060..030281d1 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -5,7 +5,6 @@ import LogoLoading from "@/components/common/LogoLoading"; import AssignmentList from "@/components/assignment/AssignmentList"; import { useUserContext } from "@/contexts/user-context"; import { useQuery } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; import Cookies from "js-cookie"; import { useToast } from "@/components/ui/use-toast"; import { useRouter } from "next/navigation"; @@ -14,28 +13,22 @@ export default function DashBoard() { const { user } = useUserContext(); const { toast } = useToast(); const route = useRouter(); - const [isLoading, setIsLoading] = useState(false); - const [assignments, setAssignments] = useState(); - useEffect(() => { - if (!(user && Cookies.get('token'))) { - toast({ - title: "You must login to view dashboard", - description: "Please login and try again", - variant: "destructive", - }); - route.push('/login'); - } else { - const { data: assignments, isLoading } = useQuery({ - queryKey: ["get-assignments", user.uid], - queryFn: async () => { - return await AssignmentService.getAssignmentsByUserId(user.uid); - }, - }); - setIsLoading(isLoading); - setAssignments(assignments); - } - }, [user]); + if (!(user && Cookies.get('token'))) { + toast({ + title: "You must login to view dashboard", + description: "Please login and try again", + variant: "destructive", + }); + route.push('/login'); + } + + const { data: assignments, isLoading } = useQuery({ + queryKey: ["get-assignments", user?.uid ?? 0], + queryFn: async () => { + return await AssignmentService.getAssignmentsByUserId(user?.uid ?? 0); + } + }); return (
diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 6088730b..f1478e74 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -11,6 +11,7 @@ import { useUserContext } from "@/contexts/user-context"; import { useRouter } from "next/navigation"; import { useToast } from "@/components/ui/use-toast"; +// eslint-disable-next-line @typescript-eslint/no-misused-promises export default function Home() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -74,7 +75,11 @@ export default function Home() {
); } +/* eslint-enable @typescript-eslint/no-misused-promises */ \ No newline at end of file diff --git a/frontend/src/app/user/page.tsx b/frontend/src/app/user/page.tsx index e06657ee..5421c0f2 100644 --- a/frontend/src/app/user/page.tsx +++ b/frontend/src/app/user/page.tsx @@ -75,7 +75,7 @@ export default function Page() {
Your Profile
- +
)} diff --git a/frontend/src/components/assignment/create/AssignmentEditor.tsx b/frontend/src/components/assignment/create/AssignmentEditor.tsx index e36b7484..78ddd7ef 100644 --- a/frontend/src/components/assignment/create/AssignmentEditor.tsx +++ b/frontend/src/components/assignment/create/AssignmentEditor.tsx @@ -84,9 +84,9 @@ export default function AssignmentEditor({ isEditing = false }: Props) { description, isPublished, // if uid is alr in authors, don't add it again - authors: assignment!.authors.includes(user.uid) + authors: assignment!.authors.includes(user?.uid ?? 0) ? assignment!.authors - : [...assignment!.authors, user.uid], + : [...assignment!.authors, user?.uid ?? 0], }) .then((updatedAssignment) => { if (!updatedAssignment) { @@ -114,7 +114,7 @@ export default function AssignmentEditor({ isEditing = false }: Props) { deadline, description, isPublished, - authors: [user.uid], + authors: [user?.uid?? 0], }) .then((createdAssignment) => { if (!createdAssignment) { diff --git a/frontend/src/components/common/SideBar.tsx b/frontend/src/components/common/SideBar.tsx index c0ca5150..93f3aed3 100644 --- a/frontend/src/components/common/SideBar.tsx +++ b/frontend/src/components/common/SideBar.tsx @@ -95,7 +95,7 @@ export default function SideBar() { setLoggedIn(true); } } - } catch (error) { + } catch (_error) { setLoggedIn(false); } }; diff --git a/frontend/src/helpers/user-service/api-wrapper.ts b/frontend/src/helpers/user-service/api-wrapper.ts index 060273e6..2ba914cc 100644 --- a/frontend/src/helpers/user-service/api-wrapper.ts +++ b/frontend/src/helpers/user-service/api-wrapper.ts @@ -21,7 +21,8 @@ const login = async (email: string, password: string): Promise => { ) console.log(response.status); if (response.status === HttpStatusCode.OK.valueOf()) { - return response.data.user as User; + const responseData = response.data as LoginResponse + return responseData.user; } else { throw new Error("Unknown error updating password, please try again"); } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6b6ac438..67583f76 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -7135,9 +7135,9 @@ react-toastify@^10.0.5: dependencies: clsx "^2.1.0" -react@^18: +react@^18.2.0: version "18.2.0" - resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== dependencies: loose-envify "^1.1.0" From e7d1331a34be0e825e4e27048a720ba6abc34ca0 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Wed, 10 Apr 2024 19:39:06 +0800 Subject: [PATCH 20/37] Fix build error, middleware since I'm not storing user in cookies --- frontend/src/app/dashboard/page.tsx | 20 +------------------- frontend/src/components/common/Icons.tsx | 2 ++ frontend/src/components/common/SideBar.tsx | 10 +++++----- frontend/src/middleware.ts | 3 +-- 4 files changed, 9 insertions(+), 26 deletions(-) diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 030281d1..ceefab7c 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -5,23 +5,9 @@ import LogoLoading from "@/components/common/LogoLoading"; import AssignmentList from "@/components/assignment/AssignmentList"; import { useUserContext } from "@/contexts/user-context"; import { useQuery } from "@tanstack/react-query"; -import Cookies from "js-cookie"; -import { useToast } from "@/components/ui/use-toast"; -import { useRouter } from "next/navigation"; export default function DashBoard() { const { user } = useUserContext(); - const { toast } = useToast(); - const route = useRouter(); - - if (!(user && Cookies.get('token'))) { - toast({ - title: "You must login to view dashboard", - description: "Please login and try again", - variant: "destructive", - }); - route.push('/login'); - } const { data: assignments, isLoading } = useQuery({ queryKey: ["get-assignments", user?.uid ?? 0], @@ -35,11 +21,7 @@ export default function DashBoard() { {isLoading ? ( ) : ( - user ? ( - - ) : ( - <> - ) + )} ); diff --git a/frontend/src/components/common/Icons.tsx b/frontend/src/components/common/Icons.tsx index 759777a5..b60e0940 100644 --- a/frontend/src/components/common/Icons.tsx +++ b/frontend/src/components/common/Icons.tsx @@ -4,6 +4,7 @@ import { MdOutlineAssignment, MdOutlineLogout, MdOutlineUploadFile, + MdOutlineLogin } from "react-icons/md"; import { MdCreateNewFolder } from "react-icons/md"; import { HiMenu, HiOutlineChevronDoubleLeft } from "react-icons/hi"; @@ -18,6 +19,7 @@ const Icons = { ViewAssignment: MdOutlineAssignment, ViewSubmissions: MdOutlineUploadFile, Logout: MdOutlineLogout, + Login: MdOutlineLogin, Collapse: HiOutlineChevronDoubleLeft, Expand: HiMenu, Edit: FaRegEdit, diff --git a/frontend/src/components/common/SideBar.tsx b/frontend/src/components/common/SideBar.tsx index 93f3aed3..d3d626be 100644 --- a/frontend/src/components/common/SideBar.tsx +++ b/frontend/src/components/common/SideBar.tsx @@ -9,7 +9,6 @@ import UserDropdown from "./UserDropdown"; import { useUserContext } from "@/contexts/user-context"; import userService from "@/helpers/user-service/api-wrapper"; import Cookies from "js-cookie"; -import { MdOutlineLogin, MdOutlineLogout } from "react-icons/md"; import { useToast } from "@/components/ui/use-toast"; interface MenuItem { @@ -70,6 +69,7 @@ export default function SideBar() { const handleLoggingOut = () => { localStorage.removeItem('userContext'); + Cookies.remove('token'); setUserContext(null); toast({ title: "Log out succesfully", @@ -164,14 +164,14 @@ export default function SideBar() { className="text-black" onPress={() => handleLoggingOut()} > - + : } @@ -223,7 +223,7 @@ export default function SideBar() { className="flex text-black w-full text-left items-center justify-start p-2" fullWidth={true} onPress={() => handleLoggingOut()} - startContent={} + startContent={} > Log Out @@ -233,7 +233,7 @@ export default function SideBar() { className="flex text-black w-full text-left items-center justify-start p-2" fullWidth={true} onPress={() => handleLoggingIn()} - startContent={} + startContent={} > Sign in diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index dea9af79..b079feb6 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -15,8 +15,7 @@ export default function middleware(request: NextRequest) { return NextResponse.next(); } - const userCookie = - request.cookies.get("user") && request.cookies.get("token"); + const userCookie = request.cookies.get("token"); if (!userCookie) { return NextResponse.redirect(new URL("/login", request.nextUrl.origin)); From ca9c80ed84cc2b4fc5b165be311cb607d3f14c53 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Wed, 10 Apr 2024 19:43:03 +0800 Subject: [PATCH 21/37] Resolve conflict --- frontend/src/app/login/page.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index fa711cea..889cf7c8 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -74,13 +74,6 @@ export default function Home() { ))} - - - { isLoggedIn ? - - : - } ) : (
@@ -190,7 +150,7 @@ export default function SideBar() { ))} - - - - { isLoggedIn ? - - : - }
)} diff --git a/frontend/src/components/common/UserDropdown.tsx b/frontend/src/components/common/UserDropdown.tsx index 3534fb42..eefea635 100644 --- a/frontend/src/components/common/UserDropdown.tsx +++ b/frontend/src/components/common/UserDropdown.tsx @@ -8,14 +8,30 @@ import { } from "@nextui-org/react"; import { useRouter } from "next/navigation"; import { ReactNode } from "react"; +import { useToast } from "@/components/ui/use-toast"; +import Cookies from "js-cookie"; +import { useUserContext } from "@/contexts/user-context"; export default function UserDropdown({ children }: { children: ReactNode }) { const router = useRouter(); - + const { toast } = useToast(); + const { setUserContext } = useUserContext(); const redirectToUserProfile = () => { router.push("/user"); }; + const handleLoggingOut = () => { + localStorage.removeItem('userContext'); + Cookies.remove('token'); + setUserContext(null); + toast({ + title: "Log out succesfully", + description: "see you later!", + variant: "success", + }); + router.push('/login'); + } + return ( @@ -25,7 +41,7 @@ export default function UserDropdown({ children }: { children: ReactNode }) { User Profile - + Log Out From fb065189c33939b7bf99885090117d26a9f378a6 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Thu, 11 Apr 2024 14:07:11 +0800 Subject: [PATCH 28/37] Fix InputPassword label is not set appropriately on /user/page.tsx --- .../src/components/forms/AccountEditor.tsx | 4 ++-- .../forms/__tests__/AccountEditor.test.tsx | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/forms/AccountEditor.tsx b/frontend/src/components/forms/AccountEditor.tsx index ff1cd51b..36ed574b 100644 --- a/frontend/src/components/forms/AccountEditor.tsx +++ b/frontend/src/components/forms/AccountEditor.tsx @@ -55,13 +55,13 @@ export default function AccountEditor({ uid, userInfo }: { uid: number, userInfo { it("should have an error popover given empty confirmation", () => { render(); - const passwordInput = screen.getByLabelText("oldPassword"); + const passwordInput = screen.getByLabelText("Old Password"); fireEvent.change(passwordInput, { target: { value: "12345678" } }); const updateButton = screen.getByRole("button", { expanded: false }); fireEvent.click(updateButton); @@ -118,7 +118,7 @@ describe("Account Editor", () => { it("should have an error popover given confirmation different", () => { render(); - const passwordInput = screen.getByLabelText("newPassword"); + const passwordInput = screen.getByLabelText("New Password"); fireEvent.change(passwordInput, { target: { value: "12345678" } }); const confirmInput = screen.getByLabelText("Confirm Password"); fireEvent.change(confirmInput, { target: { value: "different" } }); @@ -135,9 +135,9 @@ describe("Account Editor", () => { it("should have error popover when server goes down", async () => { render(); - const oldPassword = screen.getByLabelText("oldPassword"); + const oldPassword = screen.getByLabelText("Old Password"); fireEvent.change(oldPassword, { target: { value: "12345678" } }); - const newPassword = screen.getByLabelText("newPassword"); + const newPassword = screen.getByLabelText("New Password"); fireEvent.change(newPassword, { target: { value: "abcdeftghj" } }); const confirmInput = screen.getByLabelText("Confirm Password"); fireEvent.change(confirmInput, { target: { value: "12345678" } }); @@ -155,9 +155,9 @@ describe("Account Editor", () => { hasFetchError = true; render(); - const oldPassword = screen.getByLabelText("oldPassword"); + const oldPassword = screen.getByLabelText("Old Password"); fireEvent.change(oldPassword, { target: { value: "12345678" } }); - const newPassword = screen.getByLabelText("newPassword"); + const newPassword = screen.getByLabelText("New Password"); fireEvent.change(newPassword, { target: { value: "12345678" } }); const confirmInput = screen.getByLabelText("Confirm Password"); fireEvent.change(confirmInput, { target: { value: "12345678" } }); @@ -175,9 +175,9 @@ describe("Account Editor", () => { it("should have not have error popover", async () => { render(); - const oldPassword = screen.getByLabelText("oldPassword"); + const oldPassword = screen.getByLabelText("Old Password"); fireEvent.change(oldPassword, { target: { value: "12345678" } }); - const newPassword = screen.getByLabelText("newPassword"); + const newPassword = screen.getByLabelText("New Password"); fireEvent.change(newPassword, { target: { value: "12345678910" } }); const confirmInput = screen.getByLabelText("Confirm Password"); fireEvent.change(confirmInput, { target: { value: "12345678910" } }); @@ -189,7 +189,7 @@ describe("Account Editor", () => { expanded: true, }); expect(updateButtonWithSuccess).toBeInTheDocument(); - const rePassword: HTMLInputElement = screen.getByLabelText("oldPassword"); + const rePassword: HTMLInputElement = screen.getByLabelText("Old Password"); expect(rePassword.value).toBe(""); }); }); From 12c8e1400039844b903873f5176561197d62e888 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Thu, 11 Apr 2024 14:28:19 +0800 Subject: [PATCH 29/37] Fix user table field to store avatar url to be named as "avatarUrl" in camelCase --- backend/user-service/psql.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/user-service/psql.ts b/backend/user-service/psql.ts index bee3702c..fbece2b7 100644 --- a/backend/user-service/psql.ts +++ b/backend/user-service/psql.ts @@ -23,7 +23,7 @@ const createUserTableQueryIfNotExist = ` major VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, bio TEXT DEFAULT '', - avatarUrl VARCHAR(255) DEFAULT '', + "avatarUrl" VARCHAR(255) DEFAULT '', role VARCHAR(60) NOT NULL ); From ddc4551af8b07ff0f81006b11e3b3a4985b208f2 Mon Sep 17 00:00:00 2001 From: tryyang2001 Date: Thu, 11 Apr 2024 14:43:02 +0800 Subject: [PATCH 30/37] fix assignment table authors being changed from number[] to text[], and also fix the wrong expectation of response for getAssignmentsByUserId --- .../assignment-service/src/models/prisma/schema.prisma | 10 ++++++---- frontend/src/app/dashboard/page.tsx | 5 ++++- frontend/src/helpers/assignment-service/api-wrapper.ts | 6 ++---- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/backend/assignment-service/src/models/prisma/schema.prisma b/backend/assignment-service/src/models/prisma/schema.prisma index 5e1e9009..bdca1f83 100644 --- a/backend/assignment-service/src/models/prisma/schema.prisma +++ b/backend/assignment-service/src/models/prisma/schema.prisma @@ -82,10 +82,12 @@ model ReferenceSolution { model User { uid Int @id @default(autoincrement()) - name String - email String @unique - major String - course String? + name String + email String @unique + major String + course String? + avatarUrl String? + bio String? password String diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 1069d8ea..e45eca58 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -12,7 +12,10 @@ export default function DashBoard() { const { data: assignments, isLoading } = useQuery({ queryKey: ["get-assignments", user.uid], queryFn: async () => { - return await AssignmentService.getAssignmentsByUserId(user.uid); + const assignments = await AssignmentService.getAssignmentsByUserId( + user.uid + ); + return assignments; }, }); diff --git a/frontend/src/helpers/assignment-service/api-wrapper.ts b/frontend/src/helpers/assignment-service/api-wrapper.ts index 56d9786e..95b78356 100644 --- a/frontend/src/helpers/assignment-service/api-wrapper.ts +++ b/frontend/src/helpers/assignment-service/api-wrapper.ts @@ -45,11 +45,9 @@ const getAssignmentsByUserId = async (userId: number | string) => { return []; } - const response = await api.get( - `/assignments?userId=${userId}` - ); + const response = await api.get(`/assignments?userId=${userId}`); - const assignments = response.data.assignments; + const assignments = response.data as Assignment[]; return assignments; } catch (error) { From 636f10c6fc45caadf0813f73eb37a5b26c3619df Mon Sep 17 00:00:00 2001 From: tryyang2001 Date: Thu, 11 Apr 2024 14:44:40 +0800 Subject: [PATCH 31/37] delete unused interface definition --- frontend/src/helpers/assignment-service/api-wrapper.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/helpers/assignment-service/api-wrapper.ts b/frontend/src/helpers/assignment-service/api-wrapper.ts index 95b78356..0bc99c70 100644 --- a/frontend/src/helpers/assignment-service/api-wrapper.ts +++ b/frontend/src/helpers/assignment-service/api-wrapper.ts @@ -10,10 +10,6 @@ const api = axios.create({ }, }); -interface GetAssignmentsResponse { - assignments: Assignment[]; -} - const getAssignmentById = async (assignmentId: string) => { try { const response = await api.get(`/assignments/${assignmentId}`); From 130013b42b59a6098bfa4caed8a7dcf941d8857b Mon Sep 17 00:00:00 2001 From: tryyang2001 Date: Thu, 11 Apr 2024 14:49:26 +0800 Subject: [PATCH 32/37] remove course attribute from the user schema --- backend/assignment-service/src/models/prisma/schema.prisma | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/assignment-service/src/models/prisma/schema.prisma b/backend/assignment-service/src/models/prisma/schema.prisma index bdca1f83..630e289e 100644 --- a/backend/assignment-service/src/models/prisma/schema.prisma +++ b/backend/assignment-service/src/models/prisma/schema.prisma @@ -85,7 +85,6 @@ model User { name String email String @unique major String - course String? avatarUrl String? bio String? From fecdba2cf7766f9a26478f6e94d17656a6b74e15 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Thu, 11 Apr 2024 18:46:54 +0800 Subject: [PATCH 33/37] Fix SideBar behaviours, logging in, signing up, profile update error message handling. --- .../controllers/user-controller.ts | 73 ++++++++-------- backend/user-service/models/user-model.ts | 2 +- .../response/login-user-response-body.ts | 6 +- .../unit/controller/user-controller.test.ts | 45 ++++++---- frontend/.dockerignore | 17 ++-- frontend/src/app/login/page.tsx | 2 +- frontend/src/app/sign-up/page.tsx | 56 ++++++++---- frontend/src/app/user/page.tsx | 5 +- .../src/components/forms/ProfileEditor.tsx | 11 ++- .../src/helpers/user-service/api-wrapper.ts | 87 +++++++++++-------- frontend/src/types/user-service.d.ts | 11 +-- 11 files changed, 176 insertions(+), 139 deletions(-) diff --git a/backend/user-service/controllers/user-controller.ts b/backend/user-service/controllers/user-controller.ts index 34c2db7c..6f24f19b 100644 --- a/backend/user-service/controllers/user-controller.ts +++ b/backend/user-service/controllers/user-controller.ts @@ -13,13 +13,13 @@ async function registerUser(req: Request, res: Response) { if (emailSearch.rows.length > 0) { console.log("Email already exists."); - return res.json({ - error: "Email already exists.", + return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ + message: "Email already exists.", }); } else if (password.length < 10) { console.log("Password not long enough."); - return res.json({ - error: "Password not long enough.", + return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ + message: "Password not long enough.", }); } bcrypt @@ -37,31 +37,30 @@ async function registerUser(req: Request, res: Response) { return res.json({ uid }); } catch (err) { console.log(err); - return res.json({ - error: "Failed to create user.", + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).json({ + message: "Failed to create user.", }); } }) .catch((err) => { console.log(err); - return res.send({ message: "Error crypting password." }); + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).json({ message: "Error crypting password." }); }); } catch (err) { console.log(err); - return res.json({ - error: "Undefined error creating users.", + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).json({ + message: "Undefined error creating users.", }); } } async function loginUser(req: Request, res: Response) { const { email, password } = req.body; - const emailSearch = await db.getUserByEmail(email); if (emailSearch.rows.length == 0) { console.log("User does not exist."); - return res.json({ - error: "User does not exist.", + return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ + message: "User does not exist.", }); } else if (emailSearch.rows.length > 0) { const user = emailSearch.rows[0]; @@ -73,14 +72,14 @@ async function loginUser(req: Request, res: Response) { if (!result) { console.log("Incorrect password."); return res.status(HttpStatusCode.FORBIDDEN.valueOf()).json({ - error: "Incorrect password.", + message: "Incorrect password.", }); } else { const jwtSecretKey: Secret | undefined = process.env.JWT_SECRET_KEY; if (!jwtSecretKey) { console.error("JWT secret key is not defined."); return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).json({ - error: "Internal server error.", + message: "Internal server error.", }); } @@ -90,13 +89,17 @@ async function loginUser(req: Request, res: Response) { }; const token = jwt.sign(payload, jwtSecretKey, { expiresIn: "5d" }); + const responseData = { + uid: user.uid, + role: user.role, + } res .cookie("token", token, { path: "/", httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days expiry }) - .json({ user }); + .json(responseData); } }) .catch((err) => { @@ -110,7 +113,7 @@ async function getUserInfo(req: Request, res: Response) { const queryUidString = req.query.uid; console.log(queryUidString); if (typeof queryUidString !== 'string') { - return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ error: 'Invalid uid.' }); + return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ message: 'Invalid uid.' }); } try { @@ -119,8 +122,8 @@ async function getUserInfo(req: Request, res: Response) { const userIdSearch = await db.getUserByUserId(uid); if (userIdSearch.rows.length == 0) { console.log("User does not exist."); - return res.json({ - error: "User does not exist.", + return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ + message: "User does not exist.", }); } else if (userIdSearch.rows.length > 0) { const user = userIdSearch.rows[0]; @@ -128,7 +131,7 @@ async function getUserInfo(req: Request, res: Response) { } } catch (err) { console.log(err); - return res.send({ + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).json({ message: "Error getting user by uid.", }); } @@ -140,8 +143,8 @@ async function getUserByEmail(req: Request, res: Response) { const emailSearch = await db.getUserByEmail(email); if (emailSearch.rows.length == 0) { console.log("User does not exist."); - return res.json({ - error: "User does not exist.", + return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ + message: "User does not exist.", }); } else if (emailSearch.rows.length > 0) { const user = emailSearch.rows[0]; @@ -149,7 +152,7 @@ async function getUserByEmail(req: Request, res: Response) { } } catch (err) { console.log(err); - return res.send({ + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).json({ message: "Error getting user by email.", }); } @@ -161,7 +164,7 @@ async function getAllUsers(req: Request, res: Response) { return res.json(allUsers); } catch (err) { console.log(err); - return res.send({ message: "Error getting all users." }); + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).send({ message: "Error getting all users." }); } } @@ -172,7 +175,7 @@ async function updateUserPassword(req: Request, res: Response) { if (userIdSearch.rows.length == 0) { console.log("User does not exist."); return res.status(HttpStatusCode.FORBIDDEN.valueOf()).json({ - error: "User does not exist.", + message: "User does not exist.", }); } else if (userIdSearch.rows.length > 0) { const hash = userIdSearch.rows[0].password; @@ -182,8 +185,8 @@ async function updateUserPassword(req: Request, res: Response) { .then((result) => { if (!result) { console.log("Incorrect password."); - return res.status(HttpStatusCode.FORBIDDEN.valueOf()).json({ - error: "Incorrect password.", + return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ + message: "Incorrect password.", }); } else { bcrypt @@ -195,14 +198,14 @@ async function updateUserPassword(req: Request, res: Response) { message: "Update password successfully.", }); } catch (err) { - return res.status(HttpStatusCode.NOT_FOUND.valueOf()).json({ - error: "Failed to update user password.", + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).json({ + message: "Failed to update user password.", }); } }) .catch((err) => { console.log(err); - return res.send({ + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).send({ message: "Error crypting password.", }); }); @@ -210,14 +213,14 @@ async function updateUserPassword(req: Request, res: Response) { }) .catch((err) => { console.log(err); - return res.send({ + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).send({ message: "Error checking password.", }); }); } } catch (err) { console.log(err); - return res.send({ + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).send({ message: "Error getting user by uid.", }); } @@ -226,14 +229,14 @@ async function updateUserPassword(req: Request, res: Response) { async function updateUserInfo(req: Request, res: Response) { const queryUidString = req.query.uid; if (typeof queryUidString !== 'string') { - return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ error: 'Invalid uid.' }); + return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ message: 'Invalid uid.' }); } const uid = parseInt(queryUidString); const updateFields = req.body; try { if (Object.keys(updateFields).length === 0) { - return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ error: 'No fields provided for update.' }); + return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ message: 'No fields provided for update.' }); } await db.updateUserInfo(uid, updateFields); @@ -243,7 +246,7 @@ async function updateUserInfo(req: Request, res: Response) { }); } catch (err) { console.error('Error updating user info:', err); - return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).json({ error: 'Failed to update user info.' }); + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).json({ message: 'Failed to update user info.' }); } } @@ -259,7 +262,7 @@ async function deleteUser(req: Request, res: Response) { } catch (err) { console.log(err); return res.send({ - error: "Undefined error deleting account.", + message: "Undefined error deleting account.", }); } } diff --git a/backend/user-service/models/user-model.ts b/backend/user-service/models/user-model.ts index 0bcb1c00..3b82cf61 100644 --- a/backend/user-service/models/user-model.ts +++ b/backend/user-service/models/user-model.ts @@ -43,7 +43,7 @@ async function createNewUser( ): Promise { try { const result: QueryResult = await pool.query( - `INSERT INTO users."User" (name, major, email, password, bio, avatarUrl, role) + `INSERT INTO users."User" (name, major, email, password, bio, "avatarUrl", role) VALUES ($1, $2, $3, $4, '', '', $5) RETURNING uid;`, [name, major, email, hash, role] diff --git a/backend/user-service/tests/payload/response/login-user-response-body.ts b/backend/user-service/tests/payload/response/login-user-response-body.ts index 71ac51d7..f0aa5fdc 100644 --- a/backend/user-service/tests/payload/response/login-user-response-body.ts +++ b/backend/user-service/tests/payload/response/login-user-response-body.ts @@ -1,11 +1,7 @@ export const getLoginUserResponseBody = () => { return { uid: 1, - email: 'test@example.com', - password: 'password12345', - name: 'Test', - major: 'Computer Science', - role: 'student', + role: "student", }; }; \ No newline at end of file diff --git a/backend/user-service/tests/unit/controller/user-controller.test.ts b/backend/user-service/tests/unit/controller/user-controller.test.ts index 585f6045..11c9012f 100644 --- a/backend/user-service/tests/unit/controller/user-controller.test.ts +++ b/backend/user-service/tests/unit/controller/user-controller.test.ts @@ -79,7 +79,7 @@ describe("Unit Tests for /user/register endpoint", () => { .send(reqBody); // Assert - expect(response.body).toEqual({ error: "Email already exists." }); + expect(response.body).toEqual({ message: "Email already exists." }); // expect(response.status).toBe(400); }); }); @@ -101,7 +101,7 @@ describe("Unit Tests for /user/register endpoint", () => { .send(reqBody); // Assert - expect(response.body).toEqual({ error: "Password not long enough." }); + expect(response.body).toEqual({ message: "Password not long enough." }); // expect(response.status).toBe(400); }); }); @@ -123,7 +123,7 @@ describe("Unit Tests for /user/register endpoint", () => { .send(reqBody); // Assert - expect(response.body).toEqual({ error: "Failed to create user." }); + expect(response.body).toEqual({ message: "Failed to create user." }); // expect(response.status).toBe(400); }); }); @@ -166,7 +166,7 @@ describe("Unit Tests for /user/register endpoint", () => { // Assert expect(response.body).toEqual({ - error: "Undefined error creating users.", + message: "Undefined error creating users.", }); // expect(response.status).toBe(400); }); @@ -189,17 +189,24 @@ describe("Unit Tests for /user/login endpoint", () => { it("Should return 200 and a token", async () => { // Arrange jest.spyOn(db, "getUserByEmail").mockResolvedValue({ - rows: [getLoginUserResponseBody()], + rows: [{ + uid: 1, + email: 'test@example.com', + password: 'password12345', + name: 'Test', + major: 'Computer Science', + role: 'student'} + ], } as unknown as QueryResult); bcrypt.compare = jest.fn().mockResolvedValue(true); jwt.sign = jest.fn().mockResolvedValue("fake_token"); process.env.JWT_SECRET_KEY = "secretkey"; - // Act - const response = await supertest(app).post("/user/login").send(reqBody); + const response = await supertest(app).post("/user/login").send({email: 'test@example.com', password: 'password12345'}); + console.log("Response:", response.body); // Assert - expect(response.body).toEqual({ user: getLoginUserResponseBody() }); + expect(response.body).toEqual(getLoginUserResponseBody()); expect(response.status).toBe(200); }); }); @@ -215,7 +222,7 @@ describe("Unit Tests for /user/login endpoint", () => { const response = await supertest(app).post("/user/login").send(reqBody); // Assert - expect(response.body).toEqual({ error: "User does not exist." }); + expect(response.body).toEqual({ message: "User does not exist." }); // expect(response.status).toBe(400); }); }); @@ -233,7 +240,7 @@ describe("Unit Tests for /user/login endpoint", () => { const response = await supertest(app).post("/user/login").send(reqBody); // Assert - expect(response.body).toEqual({ error: "Incorrect password." }); + expect(response.body).toEqual({ message: "Incorrect password." }); // expect(response.status).toBe(400); }); }); @@ -251,7 +258,7 @@ describe("Unit Tests for /user/login endpoint", () => { const response = await supertest(app).post("/user/login").send(reqBody); // Assert - expect(response.body).toEqual({ error: "Internal server error." }); + expect(response.body).toEqual({ message: "Internal server error." }); // expect(response.status).toBe(500); }); }); @@ -314,7 +321,7 @@ describe('Unit Tests for /user/getUserInfo endpoint', () => { .get(`/user/getUserInfo?uid=${uid}`) // Assert - expect(response.body).toEqual({ error: "User does not exist." }); + expect(response.body).toEqual({ message: "User does not exist." }); }); }); @@ -372,7 +379,7 @@ describe("Unit Tests for /user/getUserByEmail endpoint", () => { const response = await supertest(app).get(`/user/getUserByEmail`); // Assert - expect(response.body).toEqual({ error: "User does not exist." }); + expect(response.body).toEqual({ message: "User does not exist." }); // expect(response.status).toBe(404); }); @@ -497,7 +504,7 @@ describe("Unit Tests for /user/updateUserPassword endpoint", () => { .send(reqBody); // Assert - expect(response.body).toEqual({ error: "User does not exist." }); + expect(response.body).toEqual({ message: "User does not exist." }); // expect(response.status).toBe(400); }); }); @@ -516,7 +523,7 @@ describe("Unit Tests for /user/updateUserPassword endpoint", () => { .send(reqBody); // Assert - expect(response.body).toEqual({ error: "Incorrect password." }); + expect(response.body).toEqual({ message: "Incorrect password." }); // expect(response.status).toBe(400); }); }); @@ -540,7 +547,7 @@ describe("Unit Tests for /user/updateUserPassword endpoint", () => { // Assert expect(response.body).toEqual({ - error: "Failed to update user password.", + message: "Failed to update user password.", }); // expect(response.status).toBe(500); }); @@ -656,7 +663,7 @@ describe("Unit Tests for /user/updateUserInfo endpoint", () => { .send(reqBody); // Assert - expect(response.body).toEqual({ error: "No fields provided for update." }); + expect(response.body).toEqual({ message: "No fields provided for update." }); // expect(response.status).toBe(400); }); @@ -673,7 +680,7 @@ describe("Unit Tests for /user/updateUserInfo endpoint", () => { .send(reqBody); // Assert - expect(response.body).toEqual({ error: "Invalid uid." }); + expect(response.body).toEqual({ message: "Invalid uid." }); // expect(response.status).toBe(400); }); }); @@ -732,7 +739,7 @@ describe("Unit Tests for /user/deleteUser endpoint", () => { // Assert expect(response.body).toEqual({ - error: "Undefined error deleting account.", + message: "Undefined error deleting account.", }); // expect(response.status).toBe(400); }); diff --git a/frontend/.dockerignore b/frontend/.dockerignore index a08a3b3b..db60711c 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -1,20 +1,13 @@ # .dockerignore /node_modules +/.next # testing /coverage # production -/build +src/app/__tests__ -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# vercel -.vercel +.eslintrc.json +.prettierrc +jest.config.ts \ No newline at end of file diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index a201ea4f..0ae6b4a8 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -39,7 +39,7 @@ export default function Home() { console.log(user); toast({ title: "Login successfully", - description: "Welcome back to ITS, " + user.name, + description: "Welcome back to ITS", variant: "success", }); router.push('/dashboard'); diff --git a/frontend/src/app/sign-up/page.tsx b/frontend/src/app/sign-up/page.tsx index 495e1843..da2fcb94 100644 --- a/frontend/src/app/sign-up/page.tsx +++ b/frontend/src/app/sign-up/page.tsx @@ -39,7 +39,7 @@ export default function Home() { const { toast } = useToast(); - const handleSubmit = () => { + const handleSubmit = async () => { if (isInvalidEmail || isInvalidConfirmation || isInvalidPassword) { return toast({ title: "Invalid input", @@ -48,28 +48,35 @@ export default function Home() { }); } - userService - .register(email, password) - .then(() => { + try { + await userService.register(email, password); + + toast({ + title: "Sign Up successfully", + description: + "Welcome to ITS, you may proceed to login with your registered email and password.", + variant: "success", + }); + + // push to login page since we haven't set up the cookie yet + router.push("/login"); + } catch (error) { + console.log(error); + if (error instanceof Error) { + const errorMsg = error.message; toast({ - title: "Sign Up successfully", - description: - "Welcome to ITS, you may proceed to login with your registered email and password.", - variant: "success", + title: "Signing up unsucessfully", + description: errorMsg, + variant: "destructive", }); - - // push to login page since we haven't set up the cookie yet - router.push("/login"); - }) - .catch((_err) => { - console.log("Sign up failed,", _err); + } else { toast({ - title: "Sign Up failed", - description: - "We are currently encountering some issues, please try again later", + title: "Signing up unsucessfully", + description: "We are currently encountering some issues, please try again later", variant: "destructive", }); - }); + } + }; }; function Eye() { @@ -126,7 +133,18 @@ export default function Home() { } /> - diff --git a/frontend/src/app/user/page.tsx b/frontend/src/app/user/page.tsx index ad3b3b3b..c5186d06 100644 --- a/frontend/src/app/user/page.tsx +++ b/frontend/src/app/user/page.tsx @@ -7,7 +7,6 @@ import { useUserContext } from "@/contexts/user-context"; import { useRouter } from "next/navigation"; import { useToast } from "@/components/ui/use-toast"; import { useState, useEffect } from "react"; -import Cookies from "js-cookie"; export default function Page() { const { user } = useUserContext(); @@ -18,8 +17,6 @@ export default function Page() { useEffect(() => { const fetchUserInfo = async () => { - const token = Cookies.get('token'); - console.log(token); try { if (!user) { toast({ @@ -71,7 +68,7 @@ export default function Page() {
Your Account
- +
Your Profile
diff --git a/frontend/src/components/forms/ProfileEditor.tsx b/frontend/src/components/forms/ProfileEditor.tsx index 359ccfb5..3ed58cef 100644 --- a/frontend/src/components/forms/ProfileEditor.tsx +++ b/frontend/src/components/forms/ProfileEditor.tsx @@ -14,8 +14,10 @@ import { } from "@nextui-org/react"; import FileInput from "./FileInput"; import userService from "@/helpers/user-service/api-wrapper"; +import { useUserContext } from "@/contexts/user-context"; -export default function ProfileEditor({ uid, userInfo }: { uid: number, userInfo: UserInfo }) { +export default function ProfileEditor({ userInfo }: { userInfo: UserInfo }) { + const { user, setUserContext } = useUserContext(); const [info, setInfo] = useState(userInfo); const [name, setName] = useState(info.name); const isInvalidName = useMemo(() => { @@ -64,7 +66,7 @@ export default function ProfileEditor({ uid, userInfo }: { uid: number, userInfo try { await userService.updateUserInfo( - uid, + user?.uid ?? 0, { name: name, bio: bio @@ -77,6 +79,11 @@ export default function ProfileEditor({ uid, userInfo }: { uid: number, userInfo bio: bio, photo: photo!, }) + + setUserContext({ + uid: user?.uid ?? 0, + role: user?.role ?? "student", + }); } catch (error) { if (error instanceof Error) { const errorMessage = error.message; diff --git a/frontend/src/helpers/user-service/api-wrapper.ts b/frontend/src/helpers/user-service/api-wrapper.ts index d6401913..5b560296 100644 --- a/frontend/src/helpers/user-service/api-wrapper.ts +++ b/frontend/src/helpers/user-service/api-wrapper.ts @@ -21,54 +21,58 @@ const login = async (email: string, password: string): Promise => { ) console.log(response.status); if (response.status === HttpStatusCode.OK.valueOf()) { - const responseData = response.data as LoginResponse; - const user = responseData.user; + const user = response.data as User; return user; } else { - throw new Error("Unknown error updating password, please try again"); + throw new Error("Unknown error logging in, please try again"); } } catch (error) { + console.log(error); if (isAxiosError(error)) { console.log(error.response?.status); if (error.response?.status === HttpStatusCode.UNAUTHORIZED.valueOf()) { throw new Error("Unauthorize"); } else if (error.response?.status === HttpStatusCode.FORBIDDEN.valueOf()) { throw new Error("Incorrect password"); - } else { - throw new Error(error.message); + } else if (error?.response?.data) { + const responseData = error.response as ErrorResponse; + throw new Error(responseData.data.message); } } - throw new Error("Unknown error updating password, please try again"); + throw new Error("Unknown error logging in, please try again"); }; }; const register = async (email: string, password: string) => { - await api.post( - `/register`, - { - email: email, - password: password, - name: 'name placeholder', - major: 'major placeholder', - course: 'course placeholder', - role: 'student' - }, - ).then((res) => { - if (res.status !== HttpStatusCode.OK.valueOf()) { - throw new Error("We are currently encountering some issues, please try again later"); + try { + await api.post( + `/register`, + { + email: email, + password: password, + name: 'name placeholder', + major: 'major placeholder', + role: 'student' + }, + ); + } catch(error) { + console.log(error); + if (isAxiosError(error) && error?.response?.data) { + const responseData = error.response as ErrorResponse; + throw new Error(responseData.data.message); } - }).catch((err: Error) => { - throw err; - }); + throw new Error("Unknown error signing up, please try again"); + }; }; const getUserInfo = async (uid: number): Promise => { - const response = await api.get( - `/getUserInfo?uid=${uid}`, - { withCredentials: true} - ).then((res) => { - if (res.status === HttpStatusCode.OK.valueOf()) { - const responseData = res.data as UserInfo + try { + const response = await api.get( + `/getUserInfo?uid=${uid}`, + { withCredentials: true} + ) + if (response.status === HttpStatusCode.OK.valueOf()) { + const responseData = response.data as UserInfo const userInfo : UserInfo = { name: responseData.name, email: responseData.email, @@ -79,10 +83,17 @@ const getUserInfo = async (uid: number): Promise => { } else { return null; } - }).catch((_err: Error) => { - return null; - }); - return response; + } catch(error) { + if (isAxiosError(error)) { + if (error.response?.status === HttpStatusCode.UNAUTHORIZED.valueOf()) { + throw new Error("Unauthorize"); + } else if (error?.response?.data) { + const responseData = error.response as ErrorResponse; + throw new Error(responseData.data.message); + } + } + throw new Error("Unknown getting user information, please try again"); + } } const updateUserPassword = async (uid: number, oldPassword: string, newPassword: string) => { @@ -108,10 +119,13 @@ const updateUserPassword = async (uid: number, oldPassword: string, newPassword: throw new Error("Unauthorize"); } else if (error.response?.status === HttpStatusCode.FORBIDDEN.valueOf()) { throw new Error("Incorrect password"); - } else { - throw new Error(error.message); + } else if (error?.response?.data) { + const responseData = error.response as ErrorResponse; + console.log(responseData); + throw new Error(responseData.data.message); } } + throw new Error("Unknown error updating password, please try again"); }; }; @@ -137,8 +151,9 @@ const updateUserInfo = async ( if (isAxiosError(error)) { if (error.response?.status === HttpStatusCode.UNAUTHORIZED.valueOf()) { throw new Error("Unauthorized action, please login again"); - } else { - throw new Error(error.message); + } else if (error?.response?.data) { + const responseData = error.response as ErrorResponse; + throw new Error(responseData.data.message); } } throw new Error("Unknown error updating user info, please try again"); diff --git a/frontend/src/types/user-service.d.ts b/frontend/src/types/user-service.d.ts index f4b60603..3aa21fb0 100644 --- a/frontend/src/types/user-service.d.ts +++ b/frontend/src/types/user-service.d.ts @@ -1,8 +1,5 @@ interface User { uid: number; - email: string; - name: string; - major: string; role: string; } @@ -13,6 +10,10 @@ interface UserInfo { photo?: string; } -interface LoginResponse { - user: User; +interface ErrorResponse { + data: ErrorData; } + +interface ErrorData { + message: string; +} \ No newline at end of file From eb038bfa92373940c7ce64717d8114c803ad718f Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Thu, 11 Apr 2024 18:51:28 +0800 Subject: [PATCH 34/37] Fix user package misdeleting dependencies --- backend/user-service/package.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/user-service/package.json b/backend/user-service/package.json index b15fda19..9d29b2a3 100644 --- a/backend/user-service/package.json +++ b/backend/user-service/package.json @@ -12,6 +12,15 @@ "lint:autofix": "eslint ./**/*.ts ./*.ts --ext .ts --fix --ignore-path .gitignore . && prettier --write --ignore-path .gitignore .", "test": "jest --coverage --verbose" }, + "dependencies": { + "bcrypt": "^5.1.1", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.18.3", + "jsonwebtoken": "^9.0.2", + "pg": "^8.11.3" + }, "devDependencies": { "@types/bcrypt": "^5.0.2", "@types/cookie-parser": "^1.4.7", From bc73f612e0f6adc6f281dbea724a72a268a4cb3b Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Thu, 11 Apr 2024 18:54:45 +0800 Subject: [PATCH 35/37] Remove console.log in frontend --- frontend/src/app/user/page.tsx | 4 +--- frontend/src/components/common/SideBar.tsx | 7 +++---- frontend/src/helpers/user-service/api-wrapper.ts | 7 ------- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/user/page.tsx b/frontend/src/app/user/page.tsx index c5186d06..606cb5b7 100644 --- a/frontend/src/app/user/page.tsx +++ b/frontend/src/app/user/page.tsx @@ -27,7 +27,6 @@ export default function Page() { router.push("/"); } else { const retrievedUserInfo = await userService.getUserInfo(user.uid); - console.log("retrieved", retrievedUserInfo); if (retrievedUserInfo === null) { toast({ title: "Cannot fetch userpage", @@ -52,9 +51,8 @@ export default function Page() { } }; if (user) { - fetchUserInfo().catch((err) => console.log(err)); + fetchUserInfo().catch((_err) => {}); } else { - console.log("no user context"); setIsLoading(true); router.push("/"); } diff --git a/frontend/src/components/common/SideBar.tsx b/frontend/src/components/common/SideBar.tsx index 6159d81d..630501ea 100644 --- a/frontend/src/components/common/SideBar.tsx +++ b/frontend/src/components/common/SideBar.tsx @@ -74,15 +74,14 @@ export default function SideBar() { setUserInfo(retrievedUserInfo); } } - } catch (error) { - console.log(error); + } catch (_error) { } }; if (user) { - fetchUserInfo().catch((err) => console.log(err)); + fetchUserInfo().catch((_err) => {}); } else { - console.log("no user context"); + //should never reach here since if there's no user context, middleware should redirect to login page } }, [user]); diff --git a/frontend/src/helpers/user-service/api-wrapper.ts b/frontend/src/helpers/user-service/api-wrapper.ts index 5b560296..b263a9ee 100644 --- a/frontend/src/helpers/user-service/api-wrapper.ts +++ b/frontend/src/helpers/user-service/api-wrapper.ts @@ -19,7 +19,6 @@ const login = async (email: string, password: string): Promise => { }, { withCredentials: true} ) - console.log(response.status); if (response.status === HttpStatusCode.OK.valueOf()) { const user = response.data as User; return user; @@ -27,9 +26,7 @@ const login = async (email: string, password: string): Promise => { throw new Error("Unknown error logging in, please try again"); } } catch (error) { - console.log(error); if (isAxiosError(error)) { - console.log(error.response?.status); if (error.response?.status === HttpStatusCode.UNAUTHORIZED.valueOf()) { throw new Error("Unauthorize"); } else if (error.response?.status === HttpStatusCode.FORBIDDEN.valueOf()) { @@ -56,7 +53,6 @@ const register = async (email: string, password: string) => { }, ); } catch(error) { - console.log(error); if (isAxiosError(error) && error?.response?.data) { const responseData = error.response as ErrorResponse; throw new Error(responseData.data.message); @@ -113,7 +109,6 @@ const updateUserPassword = async (uid: number, oldPassword: string, newPassword: return new Error("Unknown error updating password, please try again"); } } catch (error) { - console.log(error); if (isAxiosError(error)) { if (error.response?.status === HttpStatusCode.UNAUTHORIZED.valueOf()) { throw new Error("Unauthorize"); @@ -121,7 +116,6 @@ const updateUserPassword = async (uid: number, oldPassword: string, newPassword: throw new Error("Incorrect password"); } else if (error?.response?.data) { const responseData = error.response as ErrorResponse; - console.log(responseData); throw new Error(responseData.data.message); } } @@ -147,7 +141,6 @@ const updateUserInfo = async ( throw new Error("Unknown error updating user info, please try again"); } } catch (error) { - console.log(error); if (isAxiosError(error)) { if (error.response?.status === HttpStatusCode.UNAUTHORIZED.valueOf()) { throw new Error("Unauthorized action, please login again"); From 6d90dfe3641386486314b75db8ee69255cc8bf77 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Thu, 11 Apr 2024 18:55:50 +0800 Subject: [PATCH 36/37] remove console.log in frontend (2) --- frontend/src/app/login/page.tsx | 1 - frontend/src/app/sign-up/page.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 0ae6b4a8..b04e8bdf 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -36,7 +36,6 @@ export default function Home() { throw new Error("Cannot logging in"); } setUserContext(user); - console.log(user); toast({ title: "Login successfully", description: "Welcome back to ITS", diff --git a/frontend/src/app/sign-up/page.tsx b/frontend/src/app/sign-up/page.tsx index da2fcb94..c2c3fbd1 100644 --- a/frontend/src/app/sign-up/page.tsx +++ b/frontend/src/app/sign-up/page.tsx @@ -61,7 +61,6 @@ export default function Home() { // push to login page since we haven't set up the cookie yet router.push("/login"); } catch (error) { - console.log(error); if (error instanceof Error) { const errorMsg = error.message; toast({ From 5a34543b6cdf8690de884a10215f80042aec8472 Mon Sep 17 00:00:00 2001 From: hhchinh2002 Date: Thu, 11 Apr 2024 19:01:28 +0800 Subject: [PATCH 37/37] Fix build error --- frontend/src/app/user/page.tsx | 2 +- frontend/src/components/common/SideBar.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/user/page.tsx b/frontend/src/app/user/page.tsx index 606cb5b7..ed57cf35 100644 --- a/frontend/src/app/user/page.tsx +++ b/frontend/src/app/user/page.tsx @@ -51,7 +51,7 @@ export default function Page() { } }; if (user) { - fetchUserInfo().catch((_err) => {}); + fetchUserInfo().catch((_err) => {return;}); } else { setIsLoading(true); router.push("/"); diff --git a/frontend/src/components/common/SideBar.tsx b/frontend/src/components/common/SideBar.tsx index 630501ea..55364807 100644 --- a/frontend/src/components/common/SideBar.tsx +++ b/frontend/src/components/common/SideBar.tsx @@ -79,7 +79,7 @@ export default function SideBar() { }; if (user) { - fetchUserInfo().catch((_err) => {}); + fetchUserInfo().catch((_err) => {return;}); } else { //should never reach here since if there's no user context, middleware should redirect to login page }