diff --git a/backend/user-service/controllers/user-controller.ts b/backend/user-service/controllers/user-controller.ts index 50ab2e6f..6f24f19b 100644 --- a/backend/user-service/controllers/user-controller.ts +++ b/backend/user-service/controllers/user-controller.ts @@ -2,9 +2,10 @@ 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; + const { email, password, name, major, role } = req.body; console.log("registering new user", req.body); try { @@ -12,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 @@ -29,7 +30,6 @@ async function registerUser(req: Request, res: Response) { const uid = await db.createNewUser( name, major, - course, email, hash, role @@ -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]; @@ -72,31 +71,35 @@ async function loginUser(req: Request, res: Response) { .then((result) => { if (!result) { console.log("Incorrect password."); - return res.json({ - error: "Incorrect password.", + return res.status(HttpStatusCode.FORBIDDEN.valueOf()).json({ + 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(500).json({ - error: "Internal server error.", + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).json({ + message: "Internal server error.", }); } - 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" }); + 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) => { @@ -106,14 +109,21 @@ async function loginUser(req: Request, res: Response) { } } -async function getUserByUserId(req: Request, res: Response) { - const { uid } = req.body; +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({ message: '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."); - 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]; @@ -121,7 +131,7 @@ async function getUserByUserId(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.", }); } @@ -133,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]; @@ -142,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.", }); } @@ -154,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." }); } } @@ -164,8 +174,8 @@ 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({ - error: "User does not exist.", + return res.status(HttpStatusCode.FORBIDDEN.valueOf()).json({ + message: "User does not exist.", }); } else if (userIdSearch.rows.length > 0) { const hash = userIdSearch.rows[0].password; @@ -175,8 +185,8 @@ async function updateUserPassword(req: Request, res: Response) { .then((result) => { if (!result) { console.log("Incorrect password."); - return res.json({ - error: "Incorrect password.", + return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ + message: "Incorrect password.", }); } else { bcrypt @@ -188,14 +198,14 @@ async function updateUserPassword(req: Request, res: Response) { message: "Update password successfully.", }); } catch (err) { - return res.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.", }); }); @@ -203,30 +213,40 @@ 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.", }); } } async function updateUserInfo(req: Request, res: Response) { - const { uid, email, name, major, course, role } = req.body; + const queryUidString = req.query.uid; + if (typeof queryUidString !== 'string') { + return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ message: 'Invalid uid.' }); + } + const uid = parseInt(queryUidString); + const updateFields = req.body; + try { - await db.updateUserInfo(uid, email, name, major, course, role); + if (Object.keys(updateFields).length === 0) { + return res.status(HttpStatusCode.BAD_REQUEST.valueOf()).json({ message: 'No fields provided for update.' }); + } + + await db.updateUserInfo(uid, updateFields); + return res.json({ - message: "User info updated.", + message: 'User info updated.', }); } catch (err) { - return res.json({ - error: "Failed to update user info.", - }); + console.error('Error updating user info:', err); + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR.valueOf()).json({ message: 'Failed to update user info.' }); } } @@ -242,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.", }); } } @@ -257,7 +277,7 @@ async function clearCookie(req: Request, res: Response) { export default { registerUser, loginUser, - getUserByUserId, + getUserInfo, getUserByEmail, getAllUsers, updateUserPassword, 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 b0284b5e..6b363642 100644 --- a/backend/user-service/middleware/auth.ts +++ b/backend/user-service/middleware/auth.ts @@ -1,32 +1,42 @@ import { Request, Response, NextFunction } from 'express'; import jwt, { Secret } from 'jsonwebtoken'; +import HttpStatusCode from '../libs/enums/HttpStatusCode'; -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(HttpStatusCode.FORBIDDEN.valueOf()).send({ error: "No defined JWT secret key" }); } 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("Unauthorized, invalid token"); + return res.status(HttpStatusCode.UNAUTHORIZED.valueOf()).json({ + login: false, + data: token + }); } } else { - console.log("Access Denied"); - return res.json({ + console.log("Unauthorized, no authentication token"); + return res.status(HttpStatusCode.UNAUTHORIZED.valueOf()).json({ login: false, - data: 'error' + data: "Unauthorized, no authentication token" }); } } catch (err) { - console.log("Invalid token"); - return res.send({ err: 'Invalid token' }); + return res.status(HttpStatusCode.UNAUTHORIZED.valueOf()).json({ + login: false, + data: "Unauthorize" + }); } }; diff --git a/backend/user-service/models/user-model.ts b/backend/user-service/models/user-model.ts index f6f3bdab..3b82cf61 100644 --- a/backend/user-service/models/user-model.ts +++ b/backend/user-service/models/user-model.ts @@ -34,40 +34,19 @@ async function getUserByEmail(email: string): Promise { } } -async function updateUser( - uid: number, - name: string, - major: string, - course: string, - email: string, - hash: string, - role: string -): Promise { - try { - await pool.query( - `UPDATE users."User" SET name = $2, major = $3, course = $4, email = $5, password = $6, role = $7 - WHERE uid = $1`, - [uid, name, major, course, email, hash, role] - ); - } catch (error) { - throw error; - } -} - async function createNewUser( name: string, major: string, - course: string, email: string, hash: string, role: string ): Promise { try { const result: QueryResult = await pool.query( - `INSERT INTO users."User" (name, major, course, email, password, role) - VALUES ($1, $2, $3, $4, $5, $6) + `INSERT INTO users."User" (name, major, email, password, bio, "avatarUrl", role) + VALUES ($1, $2, $3, $4, '', '', $5) RETURNING uid;`, - [name, major, course, email, hash, role] + [name, major, email, hash, role] ); const uid: number = result.rows[0].uid; console.log(uid); @@ -89,20 +68,16 @@ async function updateUserPassword(uid: number, hash: string): Promise { } } -async function updateUserInfo( - uid: number, - email: string, - name: string, - major: string, - course: string, - role: string -): Promise { +async function updateUserInfo(uid: number, updateFields: any): Promise { + const fieldsToUpdate = Object.keys(updateFields).map((key, index) => `${key} = $${index + 2}`).join(', '); + + const query = { + text: `UPDATE users."User" SET ${fieldsToUpdate} WHERE uid = $1`, + values: [uid, ...Object.values(updateFields)], + }; + try { - await pool.query( - `UPDATE users."User" SET email = $2, name = $3, major = $4, course = $5, role = $6 - WHERE uid = $1`, - [uid, email, name, major, course, role] - ); + await pool.query(query); } catch (error) { throw error; } @@ -124,7 +99,6 @@ const db = { getAllUsers, getUserByUserId, getUserByEmail, - updateUser, createNewUser, updateUserPassword, updateUserInfo, diff --git a/backend/user-service/psql.ts b/backend/user-service/psql.ts index 27f466e3..fbece2b7 100644 --- a/backend/user-service/psql.ts +++ b/backend/user-service/psql.ts @@ -18,13 +18,20 @@ const createUserTableQueryIfNotExist = ` CREATE SCHEMA IF NOT EXISTS users; CREATE TABLE IF NOT EXISTS users."User" ( uid SERIAL PRIMARY KEY, - email VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(255) NOT NULL, major VARCHAR(255) NOT NULL, - course VARCHAR(255), password VARCHAR(255) NOT NULL, + bio TEXT DEFAULT '', + "avatarUrl" VARCHAR(255) DEFAULT '', role VARCHAR(60) NOT NULL ); + + CREATE TABLE IF NOT EXISTS users."UserAssignmentRelation" ( + userId SERIAL REFERENCES users."User" (uid), + assignmentId TEXT REFERENCES assignments."Assignment" (id), + primary key (userId, assignmentId) + ); `; pool.query(createUserTableQueryIfNotExist).catch((err) => { diff --git a/backend/user-service/routes/user-route.ts b/backend/user-service/routes/user-route.ts index 9acb38cf..d50b5520 100644 --- a/backend/user-service/routes/user-route.ts +++ b/backend/user-service/routes/user-route.ts @@ -1,16 +1,16 @@ import express from 'express'; import userController from '../controllers/user-controller'; +import auth from "../middleware/auth"; const router = express.Router(); - 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/payload/request/create-user-request-body.ts b/backend/user-service/tests/payload/request/create-user-request-body.ts index cc8b8e7d..7afe1d30 100644 --- a/backend/user-service/tests/payload/request/create-user-request-body.ts +++ b/backend/user-service/tests/payload/request/create-user-request-body.ts @@ -4,7 +4,6 @@ export const getCreateUserRequestBody = () => { password: 'password12345', name: 'Test', major: 'Computer Science', - course: 'CS1101S', role: 'student', }; }; diff --git a/backend/user-service/tests/payload/request/update-user-info-request-body.ts b/backend/user-service/tests/payload/request/update-user-info-request-body.ts index 1e8a016b..f841f3f0 100644 --- a/backend/user-service/tests/payload/request/update-user-info-request-body.ts +++ b/backend/user-service/tests/payload/request/update-user-info-request-body.ts @@ -4,7 +4,6 @@ export const getUpdateUserInfoRequestBody = () => { email: 'test@example.com', name: 'Test', major: 'Computer Science', - course: 'CS1101S', role: 'student', }; }; diff --git a/backend/user-service/tests/payload/response/get-all-users-response-body.ts b/backend/user-service/tests/payload/response/get-all-users-response-body.ts index 70a9a5e7..126c0c8b 100644 --- a/backend/user-service/tests/payload/response/get-all-users-response-body.ts +++ b/backend/user-service/tests/payload/response/get-all-users-response-body.ts @@ -6,7 +6,6 @@ export const getGetAllUsersResponseBody = () => { password: 'password12345', name: 'Test', major: 'Computer Science', - course: 'CS1101S', role: 'student', }, { @@ -15,7 +14,6 @@ export const getGetAllUsersResponseBody = () => { password: 'password12345', name: 'Test2', major: 'Computer Science', - course: 'CS1101S', role: 'student', } ]; diff --git a/backend/user-service/tests/payload/response/get-user-by-email-response-body.ts b/backend/user-service/tests/payload/response/get-user-by-email-response-body.ts index ff064bdf..3830832e 100644 --- a/backend/user-service/tests/payload/response/get-user-by-email-response-body.ts +++ b/backend/user-service/tests/payload/response/get-user-by-email-response-body.ts @@ -5,7 +5,6 @@ export const getGetUserByEmailResponseBody = () => { password: 'password12345', name: 'Test', major: 'Computer Science', - course: 'CS1101S', role: 'student', }; }; \ No newline at end of file diff --git a/backend/user-service/tests/payload/response/get-user-response-body.ts b/backend/user-service/tests/payload/response/get-user-response-body.ts index 16a76597..817dd385 100644 --- a/backend/user-service/tests/payload/response/get-user-response-body.ts +++ b/backend/user-service/tests/payload/response/get-user-response-body.ts @@ -5,7 +5,6 @@ export const getGetUserResponseBody = () => { password: 'password12345', name: 'Test', major: 'Computer Science', - course: 'CS1101S', role: 'student', }; }; \ No newline at end of file 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 508f41d9..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,12 +1,7 @@ export const getLoginUserResponseBody = () => { return { uid: 1, - email: 'test@example.com', - password: 'password12345', - name: 'Test', - major: 'Computer Science', - course: 'CS1101S', - 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 feca6697..11c9012f 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'; process.env.NODE_ENV = "test"; @@ -24,6 +26,13 @@ jest.mock("../../../psql", () => ({ connect: jest.fn(), })); +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; @@ -70,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); }); }); @@ -92,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); }); }); @@ -114,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); }); }); @@ -157,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); }); @@ -180,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); }); }); @@ -206,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); }); }); @@ -224,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); }); }); @@ -242,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); }); }); @@ -267,14 +283,11 @@ 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; - - beforeEach(() => { - reqBody = getGetUserRequestBody(); - }); - + let uid: any; + const existingUserId = 1; + const nonExistingUserId = -1; afterEach(() => { jest.clearAllMocks(); }); @@ -282,15 +295,14 @@ describe("Unit Tests for /user/getUserByUserId 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/getUserByUserId") - .send(reqBody); - + .get(`/user/getUserInfo?uid=${uid}`) // Assert expect(response.body).toEqual(getGetUserResponseBody()); }); @@ -299,31 +311,31 @@ describe("Unit Tests for /user/getUserByUserId 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/getUserByUserId") - .send(reqBody); - + .get(`/user/getUserInfo?uid=${uid}`) + // Assert - expect(response.body).toEqual({ error: "User does not exist." }); + expect(response.body).toEqual({ message: "User does not exist." }); }); }); 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/getUserByUserId") - .send(reqBody); + .get(`/user/getUserInfo?uid=${uid}`) // Assert expect(response.body).toEqual({ message: "Error getting user by uid." }); @@ -367,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); }); @@ -445,7 +457,6 @@ describe("Unit Tests for /user/updateUserPassword endpoint", () => { password: "password12345", name: "Test", major: "Computer Science", - course: "CS1101S", role: "student", }; @@ -493,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); }); }); @@ -512,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); }); }); @@ -536,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); }); @@ -619,12 +630,18 @@ describe("Unit Tests for /user/updateUserInfo endpoint", () => { describe("Given a valid request body to update user info", () => { it("Should return a success message", async () => { // Arrange + const updateFields = { + name: "Updated Name", + major: "Updated Major", + email: "updated@example.com", + role: "updatedRole" + }; jest.spyOn(db, "updateUserInfo").mockResolvedValue(); // Act const response = await supertest(app) - .put("/user/updateUserInfo") - .send(reqBody); + .put(`/user/updateUserInfo?uid=${1}`) + .send(updateFields); // Assert expect(response.status).toBe(200); @@ -633,20 +650,37 @@ describe("Unit Tests for /user/updateUserInfo endpoint", () => { }); describe("Given an invalid request body to update user info", () => { - it("Should return an error message", async () => { + it("Should return an error message saying no update field given valid uid", async () => { + // Arrange + const reqBody = {}; + jest + .spyOn(db, "updateUserInfo") + .mockRejectedValue(new Error("No fields provided for update.")); + + // Act + const response = await supertest(app) + .put(`/user/updateUserInfo?uid=${1}`) + .send(reqBody); + + // Assert + expect(response.body).toEqual({ message: "No fields provided for update." }); + // expect(response.status).toBe(400); + }); + + it("Should return an error message saying Invalid uid.", async () => { // Arrange const reqBody = {}; jest .spyOn(db, "updateUserInfo") - .mockRejectedValue(new Error("Failed to update user info.")); + .mockRejectedValue(new Error("Invalid uid.")); // Act const response = await supertest(app) - .put("/user/updateUserInfo") + .put(`/user/updateUserInfo`) .send(reqBody); // Assert - expect(response.body).toEqual({ error: "Failed to update user info." }); + expect(response.body).toEqual({ message: "Invalid uid." }); // expect(response.status).toBe(400); }); }); @@ -661,7 +695,6 @@ describe("Unit Tests for /user/deleteUser endpoint", () => { password: "password12345", name: "Test", major: "Computer Science", - course: "CS1101S", role: "student", }; @@ -698,7 +731,7 @@ describe("Unit Tests for /user/deleteUser endpoint", () => { jest .spyOn(db, "deleteUser") .mockRejectedValue(new Error("Failed to delete user.")); - + // Act const response = await supertest(app) .delete("/user/deleteUser") @@ -706,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/backend/user-service/tests/unit/model/user-model.test.ts b/backend/user-service/tests/unit/model/user-model.test.ts index a5d8f8fa..d9789587 100644 --- a/backend/user-service/tests/unit/model/user-model.test.ts +++ b/backend/user-service/tests/unit/model/user-model.test.ts @@ -24,7 +24,6 @@ describe("getAllUsers function", () => { password: "password12345", name: "Test", major: "Computer Science", - course: "CS1101S", role: "student", }, ], @@ -64,7 +63,6 @@ describe("getUserByUserId function", () => { password: "password12345", name: "Test", major: "Computer Science", - course: "CS1101S", role: "student", }, ], @@ -105,7 +103,6 @@ describe("getUserByEmail function", () => { password: "password12345", name: "Test", major: "Computer Science", - course: "CS1101S", role: "student", }, ], @@ -130,59 +127,6 @@ describe("getUserByEmail function", () => { }); }); -describe("updateUser function", () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it("should update a user", async () => { - // Arrange - const uid = 1; - const name = "Updated Name"; - const major = "Updated Major"; - const course = "Updated Course"; - const email = "updated@example.com"; - const hash = "updatedHash"; - const role = "updatedRole"; - - const mockResult = { - rowCount: 1, - }; - (pool.query as jest.Mock).mockResolvedValue(mockResult as QueryResult); - // Act - const result = await model.updateUser( - uid, - name, - major, - course, - email, - hash, - role - ); - - // Assert - expect(result).toBeUndefined(); - }); - - it("should throw an error if query fails", async () => { - // Arrange - const uid = 1; - const name = "Updated Name"; - const major = "Updated Major"; - const course = "Updated Course"; - const email = "updated@example.com"; - const hash = "updatedHash"; - const role = "updatedRole"; - const errorMessage = "Failed to update user"; - (pool.query as jest.Mock).mockRejectedValue(new Error(errorMessage)); - - // Act and Assert - await expect( - model.updateUser(uid, name, major, course, email, hash, role) - ).rejects.toThrow(errorMessage); - }); -}); - describe("createNewUser function", () => { afterEach(() => { jest.clearAllMocks(); @@ -192,7 +136,6 @@ describe("createNewUser function", () => { // Arrange const name = "New User"; const major = "Computer Science"; - const course = "CS1101S"; const email = "newuser@example.com"; const hash = "newHash"; const role = "student"; @@ -206,7 +149,6 @@ describe("createNewUser function", () => { const result = await model.createNewUser( name, major, - course, email, hash, role @@ -220,7 +162,6 @@ describe("createNewUser function", () => { // Arrange const name = "New User"; const major = "Computer Science"; - const course = "CS1101S"; const email = "newuser@example.com"; const hash = "newHash"; const role = "student"; @@ -229,7 +170,7 @@ describe("createNewUser function", () => { // Act and Assert await expect( - model.createNewUser(name, major, course, email, hash, role) + model.createNewUser(name, major, email, hash, role) ).rejects.toThrow(errorMessage); }); }); @@ -278,11 +219,12 @@ describe("updateUserInfo function", () => { it("should update user information", async () => { // Arrange const uid = 1; - const email = "updated@example.com"; - const name = "Updated Name"; - const major = "Updated Major"; - const course = "Updated Course"; - const role = "Updated Role"; + const updateFields = { + name: "Updated Name", + major: "Updated Major", + email: "updated@example.com", + role: "updatedRole" + }; const mockResult = { rowCount: 1, @@ -292,11 +234,7 @@ describe("updateUserInfo function", () => { // Act const result = await model.updateUserInfo( uid, - email, - name, - major, - course, - role + updateFields ); // Assert @@ -306,17 +244,18 @@ describe("updateUserInfo function", () => { it("should throw an error if update fails", async () => { // Arrange const uid = 1; - const email = "updated@example.com"; - const name = "Updated Name"; - const major = "Updated Major"; - const course = "Updated Course"; - const role = "Updated Role"; + const updateFields = { + name: "Updated Name", + major: "Updated Major", + email: "updated@example.com", + role: "updatedRole" + }; const errorMessage = "Failed to update user"; (pool.query as jest.Mock).mockRejectedValue(new Error(errorMessage)); // Act and Assert await expect( - model.updateUserInfo(uid, email, name, major, course, role) + model.updateUserInfo(uid, updateFields) ).rejects.toThrow(errorMessage); }); }); 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 e45eca58..ceefab7c 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -10,13 +10,10 @@ export default function DashBoard() { const { user } = useUserContext(); const { data: assignments, isLoading } = useQuery({ - queryKey: ["get-assignments", user.uid], + queryKey: ["get-assignments", user?.uid ?? 0], queryFn: async () => { - const assignments = await AssignmentService.getAssignmentsByUserId( - user.uid - ); - return assignments; - }, + return await AssignmentService.getAssignmentsByUserId(user?.uid ?? 0); + } }); return ( @@ -24,7 +21,7 @@ export default function DashBoard() { {isLoading ? ( ) : ( - + )} ); diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index eb4135ae..b04e8bdf 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -6,24 +6,23 @@ 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 { 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(""); const [isInvalid, setIsInvalid] = useState(false); - - const { setUser } = useUserContext(); + const { toast } = useToast(); + const { setUserContext } = useUserContext(); const router = useRouter(); - const { toast } = useToast(); - - const handleSubmit = () => { - if (email === "" || password === "" || isInvalid) { + const handleSubmit = async () => { + if (email == "" || password == "" || isInvalid) { toast({ title: "Invalid input", description: "Please check your input and try again", @@ -31,31 +30,34 @@ export default function Home() { }); } - userService - .login(email, password) - .then((user) => { - if (!user) { - throw new Error("Cannot logging in"); - } - - Cookies.set("user", JSON.stringify(user), { expires: 7 }); - setUser(user); - + try { + const user = await userService.login(email, password); + if (!user) { + throw new Error("Cannot logging in"); + } + setUserContext(user); + toast({ + title: "Login successfully", + description: "Welcome back to ITS", + variant: "success", + }); + router.push('/dashboard'); + } catch (err) { + if (err instanceof Error) { + const errorMsg = err.message; 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 ( @@ -67,11 +69,17 @@ export default function Home() { setIsInvalid={setIsInvalid} /> - + diff --git a/frontend/src/app/user/page.tsx b/frontend/src/app/user/page.tsx index 743e7847..ed57cf35 100644 --- a/frontend/src/app/user/page.tsx +++ b/frontend/src/app/user/page.tsx @@ -6,34 +6,57 @@ import LogoLoading from "@/components/common/LogoLoading"; import { useUserContext } from "@/contexts/user-context"; import { useRouter } from "next/navigation"; import { useToast } from "@/components/ui/use-toast"; -import { useQuery } from "@tanstack/react-query"; +import { useState, useEffect } from "react"; export default function Page() { const { user } = useUserContext(); - + const [isLoading, setIsLoading] = useState(true); + const [ userInfo, setUserInfo ] = useState({} as UserInfo); const router = useRouter(); - const { toast } = useToast(); - const { data: userInfo, isLoading } = useQuery({ - queryKey: ["get-user-info", user.uid], - queryFn: async () => { - const userInfo = await userService.getUserInfo(user.uid); - - if (userInfo === null) { + useEffect(() => { + const fetchUserInfo = async () => { + try { + if (!user) { + toast({ + title: "You must login to see Userpage", + description: "Please login first", + variant: "destructive", + }); + router.push("/"); + } else { + const retrievedUserInfo = await userService.getUserInfo(user.uid); + if (retrievedUserInfo === null) { + toast({ + title: "Cannot fetch userpage", + description: "Please try again later", + variant: "destructive", + }); + router.push("/"); + } else { + setUserInfo(retrievedUserInfo); + setIsLoading(false); + } + } + } catch (error) { + console.error("Error fetching user info:", error); toast({ - title: "User not found", - description: "Please login again", + title: "Cannot fetch userpaage", + description: "Please try again later", variant: "destructive", }); - - router.push("/dashboard"); - return null; + // Handle the error based on its type + router.push("/"); } - - return userInfo; - }, - }); + }; + if (user) { + fetchUserInfo().catch((_err) => {return;}); + } else { + setIsLoading(true); + router.push("/"); + } + }, []); return (
@@ -43,11 +66,11 @@ export default function Page() {
Your Account
- +
Your Profile
- +
)} diff --git a/frontend/src/components/assignment/create/AssignmentEditor.tsx b/frontend/src/components/assignment/create/AssignmentEditor.tsx index e36b7484..fa15ad31 100644 --- a/frontend/src/components/assignment/create/AssignmentEditor.tsx +++ b/frontend/src/components/assignment/create/AssignmentEditor.tsx @@ -49,10 +49,8 @@ export default function AssignmentEditor({ isEditing = false }: Props) { setIsPublished(assignment.isPublished); } }, []); - - const { user } = useUserContext(); - const { toast } = useToast(); + const { user } = useUserContext(); const checkFormValidity = useCallback( (field: string, value: string) => { @@ -84,9 +82,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 +112,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/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 ce06a157..55364807 100644 --- a/frontend/src/components/common/SideBar.tsx +++ b/frontend/src/components/common/SideBar.tsx @@ -1,11 +1,13 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Avatar, Button, User, Spacer } from "@nextui-org/react"; import { usePathname, useRouter } from "next/navigation"; import classNames from "classnames"; import Icons from "./Icons"; import UserDropdown from "./UserDropdown"; +import { useUserContext } from "@/contexts/user-context"; +import userService from "@/helpers/user-service/api-wrapper"; interface MenuItem { id: number; @@ -32,16 +34,15 @@ const menuItems: MenuItem[] = [ label: "View Submissions", icon: , link: "/assignments/submissions", - }, -]; + } +] export default function SideBar() { const router = useRouter(); - const userName = "Jane Doe"; - const userEmail = "janedoe@u.nus.edu"; + const { user } = useUserContext(); const [isCollapsed, setIsCollapsed] = useState(false); const [isCollapsible, setIsCollapsible] = 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", { @@ -62,6 +63,28 @@ export default function SideBar() { router.push(route); }; + useEffect(() => { + const fetchUserInfo = async () => { + try { + if (user === null) { + router.push('/login'); + } else { + const retrievedUserInfo = await userService.getUserInfo(user.uid); + if (retrievedUserInfo !== null) { + setUserInfo(retrievedUserInfo); + } + } + } catch (_error) { + } + }; + + if (user) { + fetchUserInfo().catch((_err) => {return;}); + } else { + //should never reach here since if there's no user context, middleware should redirect to login page + } + }, [user]); + // obtain current path, if is login/sign up, don't render SideBar const currentPath = usePathname(); @@ -125,8 +148,8 @@ export default function SideBar() {
); -} +} \ No newline at end of file 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 diff --git a/frontend/src/components/forms/AccountEditor.tsx b/frontend/src/components/forms/AccountEditor.tsx index ccbe0545..36ed574b 100644 --- a/frontend/src/components/forms/AccountEditor.tsx +++ b/frontend/src/components/forms/AccountEditor.tsx @@ -10,16 +10,18 @@ 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 }) { +export default function AccountEditor({ uid, userInfo }: { uid: number, 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 == "") { + if (newPassword == "" || oldPassword == "") { setAccountMessage("Please fill in the required fields"); return; } @@ -28,26 +30,24 @@ 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( + 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(""); } }; @@ -55,6 +55,13 @@ export default function AccountEditor({ userInfo }: { userInfo: UserInfo }) {
+ >; setIsInvalid?: Dispatch>; } export default function PasswordInput({ + label, password, setPassword, setIsInvalid, @@ -28,7 +30,7 @@ export default function PasswordInput({ } size="sm" value={password} diff --git a/frontend/src/components/forms/ProfileEditor.tsx b/frontend/src/components/forms/ProfileEditor.tsx index 833d331e..3ed58cef 100644 --- a/frontend/src/components/forms/ProfileEditor.tsx +++ b/frontend/src/components/forms/ProfileEditor.tsx @@ -13,9 +13,11 @@ import { Avatar, } 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({ userInfo }: { userInfo: UserInfo }) { - console.log(userInfo); + const { user, setUserContext } = useUserContext(); const [info, setInfo] = useState(userInfo); const [name, setName] = useState(info.name); const isInvalidName = useMemo(() => { @@ -62,32 +64,33 @@ export default function ProfileEditor({ userInfo }: { userInfo: UserInfo }) { return; } - const res = await fetch("https://jsonplaceholder.typicode.com/users/1", { - method: "PATCH", - body: JSON.stringify({ - name: name, - bio: bio, - photo: newPhoto, - email: info.email, - }), - }).catch((err) => { - console.log(err); - return { - status: 500, - ok: false, - }; - }); - - if (!res.ok) { - setMessage("An error occured, please try again later"); - } else { + try { + await userService.updateUserInfo( + user?.uid ?? 0, + { + name: name, + bio: bio + } + ); setMessage("Profile saved!"); setInfo({ - email: info.email, name: name, + email: info.email, bio: bio, - photo: photo, + photo: photo!, + }) + + setUserContext({ + uid: user?.uid ?? 0, + role: user?.role ?? "student", }); + } catch (error) { + if (error instanceof Error) { + const errorMessage = error.message; + setMessage(errorMessage); + } else { + setMessage("An error occured, please try again later"); + } } }; return ( diff --git a/frontend/src/components/forms/__tests__/AccountEditor.test.tsx b/frontend/src/components/forms/__tests__/AccountEditor.test.tsx index dc0ab417..b7803cc6 100644 --- a/frontend/src/components/forms/__tests__/AccountEditor.test.tsx +++ b/frontend/src/components/forms/__tests__/AccountEditor.test.tsx @@ -30,18 +30,19 @@ describe("Account Editor", () => { name: "Abc", bio: "Hello!", }; - + const errorId = -1; let hasFetchError = false; global.fetch = jest.fn((_, { body: body }: { body: string }) => { if (hasFetchError) { return Promise.reject("Fetch failed"); } - const { email: email, password: _password } = JSON.parse(body) as { - email: string; - password: string; + const { uid: uid, oldPassword: _oldPassword, newPassword: _newPassword } = JSON.parse(body) as { + uid: number; + oldPassword: string; + newPassword: string; }; - if (email == errorInfo.email) { + if (uid === errorId) { return Promise.resolve({ ok: false, status: 501, @@ -62,7 +63,7 @@ describe("Account Editor", () => { }); it("should not have any error popover on render", () => { - render(); + render(); const updateButtonWithError = screen.queryByRole("button", { hidden: true, @@ -72,7 +73,7 @@ describe("Account Editor", () => { }); it("should have an error popover given empty fields", () => { - render(); + render(); const updateButton = screen.getByRole("button", { expanded: false }); fireEvent.click(updateButton); @@ -85,7 +86,7 @@ describe("Account Editor", () => { }); it("should have an error popover given empty password", () => { - render(); + render(); const confirmInput = screen.getByLabelText("Confirm Password"); fireEvent.change(confirmInput, { target: { value: "12345678" } }); @@ -100,9 +101,9 @@ describe("Account Editor", () => { }); it("should have an error popover given empty confirmation", () => { - render(); + render(); - const passwordInput = screen.getByLabelText("Password"); + const passwordInput = screen.getByLabelText("Old Password"); fireEvent.change(passwordInput, { target: { value: "12345678" } }); const updateButton = screen.getByRole("button", { expanded: false }); fireEvent.click(updateButton); @@ -115,9 +116,9 @@ describe("Account Editor", () => { }); it("should have an error popover given confirmation different", () => { - render(); + render(); - const passwordInput = screen.getByLabelText("Password"); + const passwordInput = screen.getByLabelText("New Password"); fireEvent.change(passwordInput, { target: { value: "12345678" } }); const confirmInput = screen.getByLabelText("Confirm Password"); fireEvent.change(confirmInput, { target: { value: "different" } }); @@ -132,10 +133,12 @@ describe("Account Editor", () => { }); it("should have error popover when server goes down", async () => { - render(); + render(); - const password = screen.getByLabelText("Password"); - fireEvent.change(password, { target: { value: "12345678" } }); + const oldPassword = screen.getByLabelText("Old Password"); + fireEvent.change(oldPassword, { target: { value: "12345678" } }); + const newPassword = screen.getByLabelText("New Password"); + fireEvent.change(newPassword, { target: { value: "abcdeftghj" } }); const confirmInput = screen.getByLabelText("Confirm Password"); fireEvent.change(confirmInput, { target: { value: "12345678" } }); const updateButton = screen.getByRole("button", { expanded: false }); @@ -150,16 +153,17 @@ describe("Account Editor", () => { it("should display an error when fetch throws an error", async () => { hasFetchError = true; - render(); + render(); - const password = screen.getByLabelText("Password"); - fireEvent.change(password, { target: { value: "12345678" } }); + const oldPassword = screen.getByLabelText("Old Password"); + fireEvent.change(oldPassword, { target: { value: "12345678" } }); + const newPassword = screen.getByLabelText("New Password"); + fireEvent.change(newPassword, { target: { value: "12345678" } }); const confirmInput = screen.getByLabelText("Confirm Password"); fireEvent.change(confirmInput, { target: { value: "12345678" } }); const updateButton = screen.getByRole("button", { expanded: false }); fireEvent.click(updateButton); - expect(fetch).toHaveBeenCalled(); const updateButtonWithError = await screen.findByRole("button", { hidden: true, expanded: true, @@ -169,12 +173,14 @@ describe("Account Editor", () => { describe("Given correct fields", () => { it("should have not have error popover", async () => { - render(); + render(); - const password: HTMLInputElement = screen.getByLabelText("Password"); - fireEvent.change(password, { target: { value: "12345678" } }); + const oldPassword = screen.getByLabelText("Old Password"); + fireEvent.change(oldPassword, { target: { value: "12345678" } }); + const newPassword = screen.getByLabelText("New Password"); + fireEvent.change(newPassword, { target: { value: "12345678910" } }); const confirmInput = screen.getByLabelText("Confirm Password"); - fireEvent.change(confirmInput, { target: { value: "12345678" } }); + fireEvent.change(confirmInput, { target: { value: "12345678910" } }); const updateButton = screen.getByRole("button", { expanded: false }); fireEvent.click(updateButton); @@ -183,7 +189,7 @@ describe("Account Editor", () => { expanded: true, }); expect(updateButtonWithSuccess).toBeInTheDocument(); - const rePassword: HTMLInputElement = screen.getByLabelText("Password"); + const rePassword: HTMLInputElement = screen.getByLabelText("Old Password"); expect(rePassword.value).toBe(""); }); }); diff --git a/frontend/src/components/forms/__tests__/PasswordInput.test.tsx b/frontend/src/components/forms/__tests__/PasswordInput.test.tsx index 81ebee13..bcf68ade 100644 --- a/frontend/src/components/forms/__tests__/PasswordInput.test.tsx +++ b/frontend/src/components/forms/__tests__/PasswordInput.test.tsx @@ -16,6 +16,7 @@ describe("Password Input", () => { it("should be valid given empty values", () => { render( { it("should be invalid given not enough characters", () => { render( { it("nothing should happen on invalid input", () => { render( @@ -59,6 +62,7 @@ describe("Password Input", () => { it("should be invalid given not enough characters", () => { render( { it("should become a text input when eye is pressed", () => { render( { it("should become a password input when eye is pressed twice", () => { render( { 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 b6efde3b..f537e86e 100644 --- a/frontend/src/contexts/user-context.tsx +++ b/frontend/src/contexts/user-context.tsx @@ -1,67 +1,53 @@ "use client"; -import Cookies from "js-cookie"; -import { - createContext, - useContext, - ReactNode, - useState, - useEffect, -} from "react"; +import { createContext, useContext, ReactNode, useState, useEffect } from "react"; interface UserContextType { - user: User; - setUser: (user: User) => void; - fetchUserFromCookie: () => void; + user: User | null; + setUserContext: (user: User | null) => void; } - -const initialUser: User = { - uid: 0, - email: "", - name: "", - major: "", - course: "", - role: "", -}; +const initialUser: User | null = null; const UserContext = createContext({ - user: initialUser, - setUser: () => { - throw new Error("Not implemented"); - }, - fetchUserFromCookie: () => { + user: null, + setUserContext: () => { throw new Error("Not implemented"); - }, -}); - -const useUserContext = () => { - const context = useContext(UserContext); - if (!context) { - throw new Error("useUser must be used within a UserProvider"); } - return context; -}; +}); function UserProvider({ children }: { children: ReactNode }) { - const [user, setUser] = useState(initialUser); - - const fetchUserFromCookie = () => { - const user = Cookies.get("user"); - - if (user) { - setUser(JSON.parse(user) as User); + 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(() => { - fetchUserFromCookie(); - }, []); + localStorage.setItem("userContext", JSON.stringify(user)); + }, [user]); return ( - + {children} ); } +const useUserContext = () => { + const context = useContext(UserContext); + if (!context) { + throw new Error('useUser must be used within a UserProvider'); + } + return context; +}; -export { UserProvider, useUserContext }; +export { UserProvider, useUserContext }; \ No newline at end of file diff --git a/frontend/src/helpers/user-service/api-wrapper.ts b/frontend/src/helpers/user-service/api-wrapper.ts index 95a67e12..b263a9ee 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({ @@ -10,78 +10,155 @@ 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()) { - // the response from login is {user: user: {}}, hence we need to destructure this way - const user = (res.data as LoginResponse).user; - return user; - } else { - throw new Error("Invalid Email/Password"); - } - }); - - return response; + try { + const response = await api.post( + `/login`, + { + email: email, + password: password + }, + { withCredentials: true} + ) + if (response.status === HttpStatusCode.OK.valueOf()) { + const user = response.data as User; + return user; + } else { + throw new Error("Unknown error logging in, please try again"); + } + } catch (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 if (error?.response?.data) { + const responseData = error.response as ErrorResponse; + throw new Error(responseData.data.message); + } + } + 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", - }, - { withCredentials: true } - ) - .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) { + if (isAxiosError(error) && error?.response?.data) { + const responseData = error.response as ErrorResponse; + throw new Error(responseData.data.message); + } + throw new Error("Unknown error signing up, please try again"); + }; }; const getUserInfo = async (uid: number): Promise => { - await api - .post( - `/getUserInfo`, - { - uid: uid, - }, - { withCredentials: true } - ) - .then((res) => { - if (res.status === HttpStatusCode.OK.valueOf()) { - const userInfo = res.data as UserInfo; - return userInfo; - } else { - throw new Error( - "We are currently encountering some issues, please try again later" + 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, + bio: responseData.bio || "This person doesn't have bio", + photo: responseData.photo + } + return userInfo; + } else { + return null; + } + } 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) => { + try { + const response = await api.put( + `/updateUserPassword`, + { + uid: uid, + old_password: oldPassword, + new_password: newPassword, + }, + { withCredentials: true} ); - } - }); + if (response.status === HttpStatusCode.OK.valueOf()) { + return; + } else { + return new Error("Unknown error updating password, please try again"); + } + } catch (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 if (error?.response?.data) { + const responseData = error.response as ErrorResponse; + throw new Error(responseData.data.message); + } + } - return null; + throw new Error("Unknown error updating password, please try again"); + }; }; +const updateUserInfo = async ( + uid: number, + updateFields: Record + ): Promise => { + try { + const response = await api.put( + `/updateUserInfo?uid=${uid}`, + updateFields, + { withCredentials: true } + ); + + if (response.status === HttpStatusCode.OK.valueOf()) { + return; + } else { + throw new Error("Unknown error updating user info, please try again"); + } + } catch (error) { + if (isAxiosError(error)) { + if (error.response?.status === HttpStatusCode.UNAUTHORIZED.valueOf()) { + throw new Error("Unauthorized action, please login again"); + } 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"); + } + }; + const userService = { - login, - register, - getUserInfo, + login, + register, + getUserInfo, + updateUserPassword, + updateUserInfo }; export default userService; 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)); diff --git a/frontend/src/types/user-service.d.ts b/frontend/src/types/user-service.d.ts index e060c244..3aa21fb0 100644 --- a/frontend/src/types/user-service.d.ts +++ b/frontend/src/types/user-service.d.ts @@ -1,9 +1,5 @@ interface User { uid: number; - email: string; - name: string; - major: string; - course: string; role: string; } @@ -14,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 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" 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" ] } },