diff --git a/backend/user-service/.env.sample b/backend/user-service/.env.sample index 936b68ea62..20d0f8ef74 100644 --- a/backend/user-service/.env.sample +++ b/backend/user-service/.env.sample @@ -8,3 +8,6 @@ JWT_SECRET=you-can-replace-this-with-your-own-secret ADMIN_USERNAME=administrator ADMIN_EMAIL=admin@gmail.com ADMIN_PASSWORD=Admin@123 + +# origins for cors +ORIGINS=["http://localhost:5173", "http://127.0.0.1:5173"] \ No newline at end of file diff --git a/backend/user-service/app.ts b/backend/user-service/app.ts index c3099e32de..7fa1ae776e 100644 --- a/backend/user-service/app.ts +++ b/backend/user-service/app.ts @@ -1,19 +1,42 @@ import express, { Request, Response, NextFunction } from "express"; import cors from "cors"; +import dotenv from "dotenv"; +import fs from "fs"; +import yaml from "yaml"; +import swaggerUi from "swagger-ui-express"; import userRoutes from "./routes/user-routes.js"; import authRoutes from "./routes/auth-routes.js"; +dotenv.config(); + +const file = fs.readFileSync("./swagger.yml", "utf-8"); +const swaggerDocument = yaml.parse(file); +const origin = process.env.ORIGINS + ? process.env.ORIGINS.split(",") + : ["http://localhost:5173", "http://127.0.0.1:5173"]; + const app = express(); app.use(express.urlencoded({ extended: true })); app.use(express.json()); -app.use(cors()); // config cors so that front-end can use -app.options("*", cors()); +app.use( + cors({ + origin: origin, + credentials: true, + }) +); // config cors so that front-end can use +app.options( + "*", + cors({ + origin: ["http://localhost:5173", "http://127.0.0.1:5173"], + credentials: true, + }) +); // To handle CORS Errors app.use((req: Request, res: Response, next: NextFunction) => { - res.header("Access-Control-Allow-Origin", "*"); // "*" -> Allow all links to access + res.header("Access-Control-Allow-Origin", req.headers.origin); // "*" -> Allow all links to access res.header( "Access-Control-Allow-Headers", @@ -30,9 +53,9 @@ app.use((req: Request, res: Response, next: NextFunction) => { next(); }); -app.use("/users", userRoutes); -app.use("/auth", authRoutes); - +app.use("/api/users", userRoutes); +app.use("/api/auth", authRoutes); +app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); app.get("/", (req: Request, res: Response, next: NextFunction) => { console.log("Sending Greetings!"); res.json({ diff --git a/backend/user-service/controller/auth-controller.ts b/backend/user-service/controller/auth-controller.ts index 2a6672a0df..e96c75281e 100644 --- a/backend/user-service/controller/auth-controller.ts +++ b/backend/user-service/controller/auth-controller.ts @@ -5,7 +5,10 @@ import { findUserByEmail as _findUserByEmail } from "../model/repository.js"; import { formatUserResponse } from "./user-controller.js"; import { AuthenticatedRequest } from "../types/request.js"; -export async function handleLogin(req: AuthenticatedRequest, res: Response): Promise { +export async function handleLogin( + req: AuthenticatedRequest, + res: Response +): Promise { const { email, password } = req.body; if (email && password) { try { @@ -28,9 +31,10 @@ export async function handleLogin(req: AuthenticatedRequest, res: Response): Pro expiresIn: "7d", } ); - return res - .status(200) - .json({ message: "User logged in", data: { accessToken, ...formatUserResponse(user) } }); + return res.status(200).json({ + message: "User logged in", + data: { accessToken, user: formatUserResponse(user) }, + }); } catch (err) { return res.status(500).json({ message: "Server error", err }); } @@ -45,7 +49,9 @@ export async function handleVerifyToken( ): Promise { try { const verifiedUser = req.user; - return res.status(200).json({ message: "Token verified", data: verifiedUser }); + return res + .status(200) + .json({ message: "Token verified", data: verifiedUser }); } catch (err) { return res.status(500).json({ message: "Server error", err }); } diff --git a/backend/user-service/controller/user-controller.ts b/backend/user-service/controller/user-controller.ts index 196eba90d3..3e06d53e66 100644 --- a/backend/user-service/controller/user-controller.ts +++ b/backend/user-service/controller/user-controller.ts @@ -21,26 +21,34 @@ import { } from "../utils/validators"; import { IUser } from "../model/user-model"; -export async function createUser(req: Request, res: Response): Promise { +export async function createUser( + req: Request, + res: Response +): Promise { try { const { username, email, password } = req.body; const existingUser = await _findUserByUsernameOrEmail(username, email); if (existingUser) { - return res.status(409).json({ message: "username or email already exists" }); + return res + .status(409) + .json({ message: "username or email already exists" }); } if (username && email && password) { - const { isValid: isValidUsername, message: usernameMessage } = validateUsername(username); + const { isValid: isValidUsername, message: usernameMessage } = + validateUsername(username); if (!isValidUsername) { return res.status(400).json({ message: usernameMessage }); } - const { isValid: isValidEmail, message: emailMessage } = validateEmail(email); + const { isValid: isValidEmail, message: emailMessage } = + validateEmail(email); if (!isValidEmail) { return res.status(400).json({ message: emailMessage }); } - const { isValid: isValidPassword, message: passwordMessage } = validatePassword(password); + const { isValid: isValidPassword, message: passwordMessage } = + validatePassword(password); if (!isValidPassword) { return res.status(400).json({ message: passwordMessage }); } @@ -53,11 +61,15 @@ export async function createUser(req: Request, res: Response): Promise data: formatUserResponse(createdUser), }); } else { - return res.status(400).json({ message: "username and/or email and/or password are missing" }); + return res + .status(400) + .json({ message: "username and/or email and/or password are missing" }); } } catch (err) { console.error(err); - return res.status(500).json({ message: "Unknown error when creating new user!" }); + return res + .status(500) + .json({ message: "Unknown error when creating new user!" }); } } @@ -72,30 +84,59 @@ export async function getUser(req: Request, res: Response): Promise { if (!user) { return res.status(404).json({ message: `User ${userId} not found` }); } else { - return res.status(200).json({ message: `Found user`, data: formatUserResponse(user) }); + return res + .status(200) + .json({ message: `Found user`, data: formatUserResponse(user) }); } } catch (err) { console.error(err); - return res.status(500).json({ message: "Unknown error when getting user!" }); + return res + .status(500) + .json({ message: "Unknown error when getting user!" }); } } -export async function getAllUsers(req: Request, res: Response): Promise { +export async function getAllUsers( + req: Request, + res: Response +): Promise { try { const users = await _findAllUsers(); - return res.status(200).json({ message: `Found users`, data: users.map(formatUserResponse) }); + return res + .status(200) + .json({ message: `Found users`, data: users.map(formatUserResponse) }); } catch (err) { console.error(err); - return res.status(500).json({ message: "Unknown error when getting all users!" }); + return res + .status(500) + .json({ message: "Unknown error when getting all users!" }); } } -export async function updateUser(req: Request, res: Response): Promise { +export async function updateUser( + req: Request, + res: Response +): Promise { try { - const { username, email, password, profilePictureUrl, firstName, lastName, biography } = - req.body; - if (username || email || password || profilePictureUrl || firstName || lastName || biography) { + const { + username, + email, + password, + profilePictureUrl, + firstName, + lastName, + biography, + } = req.body; + if ( + username || + email || + password || + profilePictureUrl || + firstName || + lastName || + biography + ) { const userId = req.params.id; if (!isValidObjectId(userId)) { @@ -108,7 +149,8 @@ export async function updateUser(req: Request, res: Response): Promise } if (username) { - const { isValid: isValidUsername, message: usernameMessage } = validateUsername(username); + const { isValid: isValidUsername, message: usernameMessage } = + validateUsername(username); if (!isValidUsername) { return res.status(400).json({ message: usernameMessage }); } @@ -120,7 +162,8 @@ export async function updateUser(req: Request, res: Response): Promise } if (email) { - const { isValid: isValidEmail, message: emailMessage } = validateEmail(email); + const { isValid: isValidEmail, message: emailMessage } = + validateEmail(email); if (!isValidEmail) { return res.status(400).json({ message: emailMessage }); } @@ -133,7 +176,8 @@ export async function updateUser(req: Request, res: Response): Promise let hashedPassword: string | undefined; if (password) { - const { isValid: isValidPassword, message: passwordMessage } = validatePassword(password); + const { isValid: isValidPassword, message: passwordMessage } = + validatePassword(password); if (!isValidPassword) { return res.status(400).json({ message: passwordMessage }); } @@ -143,20 +187,16 @@ export async function updateUser(req: Request, res: Response): Promise } if (firstName) { - const { isValid: isValidFirstName, message: firstNameMessage } = validateName( - firstName, - "first name" - ); + const { isValid: isValidFirstName, message: firstNameMessage } = + validateName(firstName, "first name"); if (!isValidFirstName) { return res.status(400).json({ message: firstNameMessage }); } } if (lastName) { - const { isValid: isValidLastName, message: lastNameMessage } = validateName( - lastName, - "last name" - ); + const { isValid: isValidLastName, message: lastNameMessage } = + validateName(lastName, "last name"); if (!isValidLastName) { return res.status(400).json({ message: lastNameMessage }); } @@ -192,11 +232,16 @@ export async function updateUser(req: Request, res: Response): Promise } } catch (err) { console.error(err); - return res.status(500).json({ message: "Unknown error when updating user!" }); + return res + .status(500) + .json({ message: "Unknown error when updating user!" }); } } -export async function updateUserPrivilege(req: Request, res: Response): Promise { +export async function updateUserPrivilege( + req: Request, + res: Response +): Promise { try { const { isAdmin } = req.body; @@ -211,7 +256,10 @@ export async function updateUserPrivilege(req: Request, res: Response): Promise< return res.status(404).json({ message: `User ${userId} not found` }); } - const updatedUser = await _updateUserPrivilegeById(userId, isAdmin === true); + const updatedUser = await _updateUserPrivilegeById( + userId, + isAdmin === true + ); return res.status(200).json({ message: `Updated privilege for user ${userId}`, data: formatUserResponse(updatedUser as IUser), @@ -221,11 +269,16 @@ export async function updateUserPrivilege(req: Request, res: Response): Promise< } } catch (err) { console.error(err); - return res.status(500).json({ message: "Unknown error when updating user privilege!" }); + return res + .status(500) + .json({ message: "Unknown error when updating user privilege!" }); } } -export async function deleteUser(req: Request, res: Response): Promise { +export async function deleteUser( + req: Request, + res: Response +): Promise { try { const userId = req.params.id; if (!isValidObjectId(userId)) { @@ -237,10 +290,14 @@ export async function deleteUser(req: Request, res: Response): Promise } await _deleteUserById(userId); - return res.status(200).json({ message: `Deleted user ${userId} successfully` }); + return res + .status(200) + .json({ message: `Deleted user ${userId} successfully` }); } catch (err) { console.error(err); - return res.status(500).json({ message: "Unknown error when deleting user!" }); + return res + .status(500) + .json({ message: "Unknown error when deleting user!" }); } } diff --git a/backend/user-service/model/repository.ts b/backend/user-service/model/repository.ts index b39c4178d3..ab8a86dca6 100644 --- a/backend/user-service/model/repository.ts +++ b/backend/user-service/model/repository.ts @@ -37,7 +37,9 @@ export async function findUserById(userId: string): Promise { return UserModel.findById(userId); } -export async function findUserByUsername(username: string): Promise { +export async function findUserByUsername( + username: string +): Promise { return UserModel.findOne({ username }); } diff --git a/backend/user-service/package-lock.json b/backend/user-service/package-lock.json index 0d4af77da2..e2dcadedda 100644 --- a/backend/user-service/package-lock.json +++ b/backend/user-service/package-lock.json @@ -16,7 +16,9 @@ "express": "^4.19.2", "jsonwebtoken": "^9.0.2", "mongoose": "^8.5.4", - "validator": "^13.12.0" + "swagger-ui-express": "^5.0.1", + "validator": "^13.12.0", + "yaml": "^2.5.1" }, "devDependencies": { "@types/bcrypt": "^5.0.2", @@ -24,6 +26,7 @@ "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.7", "@types/node": "^22.5.5", + "@types/swagger-ui-express": "^4.1.6", "@types/validator": "^13.12.2", "nodemon": "^3.1.4", "tsx": "^4.19.1", @@ -580,6 +583,17 @@ "@types/send": "*" } }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@types/validator": { "version": "13.12.2", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", @@ -2350,6 +2364,27 @@ "node": ">=4" } }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", + "license": "Apache-2.0" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -2536,6 +2571,18 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } } } } diff --git a/backend/user-service/package.json b/backend/user-service/package.json index 4b6a372eb2..b7527906bb 100644 --- a/backend/user-service/package.json +++ b/backend/user-service/package.json @@ -19,6 +19,7 @@ "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.7", "@types/node": "^22.5.5", + "@types/swagger-ui-express": "^4.1.6", "@types/validator": "^13.12.2", "nodemon": "^3.1.4", "tsx": "^4.19.1", @@ -32,6 +33,8 @@ "express": "^4.19.2", "jsonwebtoken": "^9.0.2", "mongoose": "^8.5.4", - "validator": "^13.12.0" + "swagger-ui-express": "^5.0.1", + "validator": "^13.12.0", + "yaml": "^2.5.1" } } diff --git a/backend/user-service/routes/user-routes.ts b/backend/user-service/routes/user-routes.ts index acc8cb5b27..843cc1c622 100644 --- a/backend/user-service/routes/user-routes.ts +++ b/backend/user-service/routes/user-routes.ts @@ -18,11 +18,16 @@ const router = express.Router(); router.get("/", verifyAccessToken, verifyIsAdmin, getAllUsers); -router.patch("/:id/privilege", verifyAccessToken, verifyIsAdmin, updateUserPrivilege); +router.patch( + "/:id/privilege", + verifyAccessToken, + verifyIsAdmin, + updateUserPrivilege +); router.post("/", createUser); -router.get("/:id", verifyAccessToken, getUser); +router.get("/:id", getUser); router.patch("/:id", verifyAccessToken, verifyIsOwnerOrAdmin, updateUser); diff --git a/backend/user-service/scripts/seed.ts b/backend/user-service/scripts/seed.ts index b7f4b7c114..12497d8caa 100644 --- a/backend/user-service/scripts/seed.ts +++ b/backend/user-service/scripts/seed.ts @@ -8,7 +8,11 @@ export async function seedAdminAccount() { const adminEmail = process.env.ADMIN_EMAIL || "admin@gmail.com"; const adminPassword = process.env.ADMIN_PASSWORD || "Admin@123"; - if (!process.env.ADMIN_USERNAME || !process.env.ADMIN_EMAIL || !process.env.ADMIN_PASSWORD) { + if ( + !process.env.ADMIN_USERNAME || + !process.env.ADMIN_EMAIL || + !process.env.ADMIN_PASSWORD + ) { console.error( "Admin account not seeded in .env. Using default admin account credentials (username: administrator, email: admin@gmail.com, password: Admin@123)" ); diff --git a/backend/user-service/swagger.yml b/backend/user-service/swagger.yml new file mode 100644 index 0000000000..e4f98abe4b --- /dev/null +++ b/backend/user-service/swagger.yml @@ -0,0 +1,226 @@ +openapi: 3.0.0 + +info: + title: User Service + version: 1.0.0 + +definitions: + UserOutput: + properties: + id: + type: string + required: true + username: + type: string + required: true + firstName: + type: string + required: true + lastName: + type: string + required: true + email: + type: string + required: true + isAdmin: + type: boolean + required: true + default: false + biography: + type: string + required: false + profilePictureUrl: + type: string + required: false + createdAt: + type: string + required: true + +components: + schemas: + User: + properties: + username: + type: string + required: true + firstName: + type: string + required: true + lastName: + type: string + required: true + email: + type: string + required: true + isAdmin: + type: boolean + required: true + default: false + biography: + type: string + required: false + profilePictureUrl: + type: string + required: false + password: + type: string + required: true + UserResponse: + properties: + message: + type: string + data: + $ref: "#/definitions/UserOutput" + ErrorResponse: + properties: + message: + type: string + +paths: + /: + get: + tags: + - root + summary: Ping the server + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message + /api/users: + post: + tags: + - users + summary: Creates a new user + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + responses: + 201: + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/UserResponse" + 400: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + 409: + description: Conflict + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + get: + tags: + - users + summary: Get all users + responses: + 200: + description: Successful Response + content: + application/json: + schema: + properties: + message: + type: string + data: + type: array + items: + $ref: "#/definitions/UserOutput" + /api/users/{id}: + get: + summary: Get a user by id + tags: + - users + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/UserResponse" + 404: + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + patch: + summary: Update a user's particulars + tags: + - users + parameters: + - in: path + name: id + required: true + schema: + type: string + delete: + summary: Delete a user account + tags: + - users + parameters: + - in: path + name: id + required: true + schema: + type: string + /api/auth/login: + post: + summary: Login + tags: + - auth + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + type: string + required: true + password: + type: string + required: true + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string + data: + type: object + properties: + accessToken: + type: string + user: + type: object + $ref: "#/definitions/UserOutput" + + /api/auth/verify-token: + get: + summary: Verify token + tags: + - auth diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 740c8d7410..f9372e37dd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,22 +4,27 @@ import NewQuestion from "./pages/NewQuestion"; import QuestionDetail from "./pages/QuestionDetail"; import QuestionEdit from "./pages/QuestionEdit"; import PageNotFound from "./pages/PageNotFound"; +import ProfilePage from "./pages/Profile"; +import AuthProvider from "./contexts/AuthContext"; function App() { return ( - - - }> - - question page list} /> - } /> - } /> - } /> + + + + }> + + question page list} /> + } /> + } /> + } /> + + } /> + } /> - } /> - - - + + + ); } diff --git a/frontend/src/components/Navbar/index.tsx b/frontend/src/components/Navbar/index.tsx index 7ab83b4e13..ebbdf0ff4f 100644 --- a/frontend/src/components/Navbar/index.tsx +++ b/frontend/src/components/Navbar/index.tsx @@ -1,7 +1,22 @@ -import { AppBar, Box, Link, Toolbar, Typography } from "@mui/material"; +import { + AppBar, + Avatar, + Box, + Button, + IconButton, + Link, + Menu, + MenuItem, + Stack, + Toolbar, + Tooltip, + Typography, +} from "@mui/material"; import { grey } from "@mui/material/colors"; import AppMargin from "../AppMargin"; import { useNavigate } from "react-router-dom"; +import { useAuth } from "../../contexts/AuthContext"; +import { useState } from "react"; type NavbarItem = { label: string; link: string }; @@ -10,6 +25,19 @@ type NavbarProps = { navbarItems?: Array }; const Navbar: React.FC = (props) => { const { navbarItems = [{ label: "Questions", link: "/questions" }] } = props; const navigate = useNavigate(); + const auth = useAuth(); + const [anchorEl, setAnchorEl] = useState(null); + + if (!auth) { + throw new Error("useAuth() must be used within AuthProvider"); + } + + const { user } = auth; + + const handleClick = (event: React.MouseEvent) => + setAnchorEl(event.currentTarget); + + const handleClose = () => setAnchorEl(null); return ( = (props) => { }} > - + = (props) => { > PeerPrep - + {navbarItems.map((item) => ( = (props) => { {item.label} ))} - + {user ? ( + <> + + + + + + + { + handleClose(); + navigate(`/profile/${user.id}}`); + }} + > + Profile + + Logout + + + ) : ( + <> + + + + )} + diff --git a/frontend/src/components/ProfileSection/index.tsx b/frontend/src/components/ProfileSection/index.tsx new file mode 100644 index 0000000000..4bd7147e41 --- /dev/null +++ b/frontend/src/components/ProfileSection/index.tsx @@ -0,0 +1,65 @@ +import { Avatar, Box, Button, Divider, Stack, Typography } from "@mui/material"; +import React from "react"; + +type ProfileSectionProps = { + firstName: string; + lastName: string; + username: string; + biography?: string; + isCurrentUser: boolean; +}; + +const ProfileSection: React.FC = (props) => { + const { firstName, lastName, username, biography, isCurrentUser } = props; + + return ( + + + ({ + display: "flex", + flexDirection: "row", + alignItems: "center", + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + })} + > + + ({ marginLeft: theme.spacing(2) })}> + + {firstName} {lastName} + + @{username} + + + ({ + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + })} + > + {biography} + + + {isCurrentUser && ( + <> + + ({ + marginTop: theme.spacing(4), + marginBottom: theme.spacing(4), + })} + > + + + + + )} + + ); +}; + +export default ProfileSection; diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx new file mode 100644 index 0000000000..becd57a65a --- /dev/null +++ b/frontend/src/contexts/AuthContext.tsx @@ -0,0 +1,46 @@ +/* eslint-disable react-refresh/only-export-components */ + +import { createContext, useContext, useState } from "react"; + +type User = { + id: string; + username: string; + firstName: string; + lastName: string; + email: string; + biography: string; + profilePictureUrl: string; + createdAt: Date; +}; + +type AuthContextType = { + signup: () => void; + login: () => void; + logout: () => void; + user: User | null; + setUser: React.Dispatch>; +}; + +const AuthContext = createContext(null); + +const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => { + const { children } = props; + const [user, setUser] = useState(null); + + // TODO + const signup = () => {}; + + const login = () => {}; + + const logout = () => {}; + + return ( + + {children} + + ); +}; + +export const useAuth = () => useContext(AuthContext); + +export default AuthProvider; diff --git a/frontend/src/pages/Profile/index.module.css b/frontend/src/pages/Profile/index.module.css new file mode 100644 index 0000000000..806d506cc5 --- /dev/null +++ b/frontend/src/pages/Profile/index.module.css @@ -0,0 +1,10 @@ +.fullheight { + flex: 1; +} + +.center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/frontend/src/pages/Profile/index.tsx b/frontend/src/pages/Profile/index.tsx new file mode 100644 index 0000000000..048f465afa --- /dev/null +++ b/frontend/src/pages/Profile/index.tsx @@ -0,0 +1,88 @@ +import { useParams } from "react-router-dom"; +import AppMargin from "../../components/AppMargin"; +import ProfileSection from "../../components/ProfileSection"; +import { Box, Typography } from "@mui/material"; +import classes from "./index.module.css"; +import { useEffect, useState } from "react"; +import { userClient } from "../../utils/api"; +import { useAuth } from "../../contexts/AuthContext"; + +type UserProfile = { + id: string; + username: string; + firstName: string; + lastName: string; + email: string; + isAdmin: boolean; + biography?: string; + profilePictureUrl?: string; + createdAt: string; +}; + +const ProfilePage: React.FC = () => { + const { userId } = useParams<{ userId: string }>(); + const [userProfile, setUserProfile] = useState(null); + const auth = useAuth(); + + if (!auth) { + throw new Error("useAuth() must be used within AuthProvider"); + } + + const { user } = auth; + + useEffect(() => { + userClient + .get(`/users/${userId}`) + .then((res) => { + setUserProfile(res.data.data); + }) + .catch(() => setUserProfile(null)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!userProfile) { + return ( + + + ({ marginBottom: theme.spacing(4) })} + > + Oops, user not found... + + + Unfortunately, we can't find who you're looking for 😥 + + + + ); + } + + return ( + + ({ + marginTop: theme.spacing(4), + display: "flex", + })} + > + ({ flex: 1, paddingRight: theme.spacing(4) })}> + + + ({ flex: 3, paddingLeft: theme.spacing(4) })}> + Questions attempted + + + + ); +}; + +export default ProfilePage; diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index 7ee97d42ce..cf9afd34b9 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -1,8 +1,14 @@ import axios from "axios"; +const usersUrl = "http://localhost:3001/api/"; const questionsUrl = "http://localhost:3000/api/questions"; export const questionClient = axios.create({ baseURL: questionsUrl, withCredentials: true, }); + +export const userClient = axios.create({ + baseURL: usersUrl, + withCredentials: true, +});