From aae19a0e58ae61a6db889185f284aa599cf20657 Mon Sep 17 00:00:00 2001 From: jq1836 <95712150+jq1836@users.noreply.github.com> Date: Wed, 18 Sep 2024 22:21:48 +0800 Subject: [PATCH 1/9] Anything bro --- .../admin-edit-user-modal.tsx | 149 ++++++++++++++++++ .../admin-user-management.tsx | 27 +++- frontend/lib/update-user.ts | 29 ++++ 3 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 frontend/components/admin-user-management/admin-edit-user-modal.tsx create mode 100644 frontend/lib/update-user.ts diff --git a/frontend/components/admin-user-management/admin-edit-user-modal.tsx b/frontend/components/admin-user-management/admin-edit-user-modal.tsx new file mode 100644 index 0000000000..cfa4dc1062 --- /dev/null +++ b/frontend/components/admin-user-management/admin-edit-user-modal.tsx @@ -0,0 +1,149 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { Label } from "../ui/label"; +import { updateUser } from "@/lib/update-user"; +import { useAuth } from "@/app/auth/auth-context"; +import { toast } from "@/hooks/use-toast"; + +interface AdminEditUserModalProps extends React.HTMLProps { + showModal?: boolean; + setShowModal: (show: boolean) => void; + user: { id: string, username: string, email: string } | undefined; +}; + +const AdminEditUserModal: React.FC = ({ + ...props +}) => { + const auth = useAuth(); + const [editingUser, setEditingUser] = useState<{ + id?: string; + username?: string; + email?: string; + } | undefined>(); + + useEffect(() => { + setEditingUser(props?.user); + }, [props.user]); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!auth?.token) { + // Will not reach this point as button is disabled + // when token is missing + toast({ + description: "No authentication token found", + }); + return; + } + + if (!editingUser?.id) { + // Will not reach this point as button is disabled + // when editing user's id is missing + toast({ + description: "No user selected", + }); + return; + } + + const response = await updateUser(auth.token, editingUser.id, editingUser?.username, editingUser?.email); + if (!response.ok) { + toast({ + title: "Unknown Error", + description: "An unexpected error has occurred", + }); + } + switch (response.status) { + case 200: + toast({ + title: "Success", + description: "User updated successfully!", + }); + break; + case 400: + // In theory, they should never be able to send out a request + // with missing fields due to disabled submission button + toast({ + title: "Missing Fields", + description: "Please fill out all fields", + }); + break; + case 409: + toast({ + title: "Duplicted Username or Email", + description: "The username or email you entered is already in use", + }); + break; + case 500: + toast({ + title: "Server Error", + description: "The server encountered an error", + }); + break; + default: + toast({ + title: "Unknown Error", + description: "An unexpected error has occured", + }); + break; + } + } + + return ( + <> + {props.showModal && ( + + + + Edit user + +
+
+
+ +
+ setEditingUser({...editingUser, username: e.target.value})} + required + /> +
+
+
+ +
+ setEditingUser({...editingUser, email: e.target.value})} + required + /> +
+
+ + + + +
+
+ )} + + ); +}; + +export default AdminEditUserModal; diff --git a/frontend/components/admin-user-management/admin-user-management.tsx b/frontend/components/admin-user-management/admin-user-management.tsx index 7e29b9d4ee..293121f442 100644 --- a/frontend/components/admin-user-management/admin-user-management.tsx +++ b/frontend/components/admin-user-management/admin-user-management.tsx @@ -14,6 +14,8 @@ import { import UnauthorisedAccess from "@/components/common/unauthorised-access"; import LoadingScreen from "@/components/common/loading-screen"; import { useAuth } from "@/app/auth/auth-context"; +import AdminEditUserModal from "@/components/admin-user-management/admin-edit-user-modal"; +import { PencilIcon, Trash2Icon } from "lucide-react"; const fetcher = (url: string) => { const token = localStorage.getItem("jwtToken"); @@ -54,6 +56,14 @@ export default function AdminUserManagement() { >([]); const [unauthorised, setUnauthorised] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(true); + const [showModal, setShowModal] = useState(false); + const [selectedUser, setSelectedUser] = useState<{ + id: string; + username: string; + email: string; + isAdmin: boolean; + skillLevel: string; + } | undefined>(); useEffect(() => { if (data) { @@ -102,6 +112,11 @@ export default function AdminUserManagement() { return (

User Management

+ @@ -120,14 +135,20 @@ export default function AdminUserManagement() { {user.isAdmin ? "Admin" : "User"} {user.skillLevel} - diff --git a/frontend/lib/update-user.ts b/frontend/lib/update-user.ts new file mode 100644 index 0000000000..0901e90e45 --- /dev/null +++ b/frontend/lib/update-user.ts @@ -0,0 +1,29 @@ +export const updateUser = async ( + jwtToken: string, + id: string, + username?: string, + email?: string, + ) => { + if (!username && !email) { + throw new Error("Require at least one field"); + } + + const body = {}; + if (username) { + body["username"] = username; + } + if (email) { + body["email"] = email; + } + + const response = await fetch(`http://localhost:3001/users/${id}`, { + method: "PATCH", + headers: { + Authorization: `Bearer ${jwtToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + return response; + }; + \ No newline at end of file From 144ca587d17b29fd905bc41128d326f7dc7af58f Mon Sep 17 00:00:00 2001 From: jq1836 <95712150+jq1836@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:22:50 +0800 Subject: [PATCH 2/9] Add toast --- .../admin-edit-user-modal.tsx | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/frontend/components/admin-user-management/admin-edit-user-modal.tsx b/frontend/components/admin-user-management/admin-edit-user-modal.tsx index cfa4dc1062..f01ae60105 100644 --- a/frontend/components/admin-user-management/admin-edit-user-modal.tsx +++ b/frontend/components/admin-user-management/admin-edit-user-modal.tsx @@ -7,7 +7,7 @@ import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { updateUser } from "@/lib/update-user"; import { useAuth } from "@/app/auth/auth-context"; -import { toast } from "@/hooks/use-toast"; +import { useToast } from "@/hooks/use-toast"; interface AdminEditUserModalProps extends React.HTMLProps { showModal?: boolean; @@ -19,6 +19,7 @@ const AdminEditUserModal: React.FC = ({ ...props }) => { const auth = useAuth(); + const { toast } = useToast(); const [editingUser, setEditingUser] = useState<{ id?: string; username?: string; @@ -36,6 +37,7 @@ const AdminEditUserModal: React.FC = ({ // Will not reach this point as button is disabled // when token is missing toast({ + title: "Access denied", description: "No authentication token found", }); return; @@ -45,6 +47,7 @@ const AdminEditUserModal: React.FC = ({ // Will not reach this point as button is disabled // when editing user's id is missing toast({ + title: "Invalid selection", description: "No user selected", }); return; @@ -69,29 +72,49 @@ const AdminEditUserModal: React.FC = ({ // with missing fields due to disabled submission button toast({ title: "Missing Fields", - description: "Please fill out all fields", + description: "Please fill in at least 1 field", }); - break; + return; + case 401: + toast({ + title: "Access denied", + description: "Invalid session", + }); + return; + case 403: + toast({ + title: "Access denied", + description: "Only admins can update other user", + }); + return; + case 404: + toast({ + title: "User not found", + description: "User with specified ID not found", + }); + return; case 409: toast({ - title: "Duplicted Username or Email", + title: "Duplicated Username or Email", description: "The username or email you entered is already in use", }); - break; + return; case 500: toast({ title: "Server Error", description: "The server encountered an error", }); - break; + return; default: toast({ title: "Unknown Error", description: "An unexpected error has occured", }); - break; + return; } - } + + props.setShowModal(false); + }; return ( <> From ba3b9901b53e18744fe3bb512ed21da347c637d3 Mon Sep 17 00:00:00 2001 From: jq1836 <95712150+jq1836@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:26:01 +0800 Subject: [PATCH 3/9] Add UI update on user update --- .../admin-user-management/admin-edit-user-modal.tsx | 4 ++++ .../admin-user-management/admin-user-management.tsx | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/components/admin-user-management/admin-edit-user-modal.tsx b/frontend/components/admin-user-management/admin-edit-user-modal.tsx index f01ae60105..dd91a4b16c 100644 --- a/frontend/components/admin-user-management/admin-edit-user-modal.tsx +++ b/frontend/components/admin-user-management/admin-edit-user-modal.tsx @@ -13,6 +13,7 @@ interface AdminEditUserModalProps extends React.HTMLProps { showModal?: boolean; setShowModal: (show: boolean) => void; user: { id: string, username: string, email: string } | undefined; + onUserUpdate: () => void; }; const AdminEditUserModal: React.FC = ({ @@ -113,6 +114,9 @@ const AdminEditUserModal: React.FC = ({ return; } + // Remove old states, update UI and close modal + setEditingUser(undefined); + props.onUserUpdate(); props.setShowModal(false); }; diff --git a/frontend/components/admin-user-management/admin-user-management.tsx b/frontend/components/admin-user-management/admin-user-management.tsx index f0eb88769c..59e47e7411 100644 --- a/frontend/components/admin-user-management/admin-user-management.tsx +++ b/frontend/components/admin-user-management/admin-user-management.tsx @@ -40,7 +40,7 @@ const fetcher = (url: string) => { export default function AdminUserManagement() { const auth = useAuth(); - const { data, error, isLoading } = useSWR( + const { data, error, isLoading, mutate } = useSWR( "http://localhost:3001/users", fetcher ); @@ -108,6 +108,11 @@ export default function AdminUserManagement() { setUsers(users.filter((user) => user.id !== userId)); }; + const onUserUpdate = () => { + mutate(); + setSelectedUser(undefined); + } + return (

User Management

@@ -115,6 +120,7 @@ export default function AdminUserManagement() { showModal={showModal} setShowModal={setShowModal} user={selectedUser} + onUserUpdate={onUserUpdate} />
From 2ff16d9c4a0dd5e1041fc34ac4bb7773f0bb6e51 Mon Sep 17 00:00:00 2001 From: jq1836 <95712150+jq1836@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:26:32 +0800 Subject: [PATCH 4/9] Fix toast --- .../components/admin-user-management/admin-edit-user-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/admin-user-management/admin-edit-user-modal.tsx b/frontend/components/admin-user-management/admin-edit-user-modal.tsx index dd91a4b16c..e88b068bb6 100644 --- a/frontend/components/admin-user-management/admin-edit-user-modal.tsx +++ b/frontend/components/admin-user-management/admin-edit-user-modal.tsx @@ -7,7 +7,7 @@ import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { updateUser } from "@/lib/update-user"; import { useAuth } from "@/app/auth/auth-context"; -import { useToast } from "@/hooks/use-toast"; +import { useToast } from "@/components/hooks/use-toast"; interface AdminEditUserModalProps extends React.HTMLProps { showModal?: boolean; From be9a855d52fab862691754ea611f49b5bdb98119 Mon Sep 17 00:00:00 2001 From: jq1836 <95712150+jq1836@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:28:12 +0800 Subject: [PATCH 5/9] Cleanup update-user.ts --- frontend/lib/update-user.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/frontend/lib/update-user.ts b/frontend/lib/update-user.ts index 0901e90e45..262e4841ee 100644 --- a/frontend/lib/update-user.ts +++ b/frontend/lib/update-user.ts @@ -3,18 +3,14 @@ export const updateUser = async ( id: string, username?: string, email?: string, + password?: string, + skillLevel?: string, ) => { - if (!username && !email) { + if (!username && !email && !password && !skillLevel) { throw new Error("Require at least one field"); } - const body = {}; - if (username) { - body["username"] = username; - } - if (email) { - body["email"] = email; - } + const body = { username, email, password, skillLevel }; const response = await fetch(`http://localhost:3001/users/${id}`, { method: "PATCH", From d9bb53abb95ad168ef205a96a2ece3ef7e915ee7 Mon Sep 17 00:00:00 2001 From: jq1836 <95712150+jq1836@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:29:33 +0800 Subject: [PATCH 6/9] Cleanup --- .../admin-edit-user-modal.tsx | 184 ++++++++++-------- .../admin-user-management.tsx | 28 +-- frontend/lib/update-user.ts | 43 ++-- 3 files changed, 139 insertions(+), 116 deletions(-) diff --git a/frontend/components/admin-user-management/admin-edit-user-modal.tsx b/frontend/components/admin-user-management/admin-edit-user-modal.tsx index e88b068bb6..376950dbfc 100644 --- a/frontend/components/admin-user-management/admin-edit-user-modal.tsx +++ b/frontend/components/admin-user-management/admin-edit-user-modal.tsx @@ -1,7 +1,13 @@ "use client"; import React, { useEffect, useState } from "react"; -import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Label } from "../ui/label"; @@ -12,20 +18,23 @@ import { useToast } from "@/components/hooks/use-toast"; interface AdminEditUserModalProps extends React.HTMLProps { showModal?: boolean; setShowModal: (show: boolean) => void; - user: { id: string, username: string, email: string } | undefined; + user: { id: string; username: string; email: string } | undefined; onUserUpdate: () => void; -}; +} const AdminEditUserModal: React.FC = ({ ...props }) => { const auth = useAuth(); const { toast } = useToast(); - const [editingUser, setEditingUser] = useState<{ - id?: string; - username?: string; - email?: string; - } | undefined>(); + const [editingUser, setEditingUser] = useState< + | { + id?: string; + username?: string; + email?: string; + } + | undefined + >(); useEffect(() => { setEditingUser(props?.user); @@ -35,83 +44,88 @@ const AdminEditUserModal: React.FC = ({ event.preventDefault(); if (!auth?.token) { - // Will not reach this point as button is disabled - // when token is missing - toast({ - title: "Access denied", - description: "No authentication token found", - }); - return; + // Will not reach this point as button is disabled + // when token is missing + toast({ + title: "Access denied", + description: "No authentication token found", + }); + return; } if (!editingUser?.id) { - // Will not reach this point as button is disabled - // when editing user's id is missing - toast({ - title: "Invalid selection", - description: "No user selected", - }); - return; + // Will not reach this point as button is disabled + // when editing user's id is missing + toast({ + title: "Invalid selection", + description: "No user selected", + }); + return; } - const response = await updateUser(auth.token, editingUser.id, editingUser?.username, editingUser?.email); + const response = await updateUser( + auth.token, + editingUser.id, + editingUser?.username, + editingUser?.email + ); if (!response.ok) { + toast({ + title: "Unknown Error", + description: "An unexpected error has occurred", + }); + } + switch (response.status) { + case 200: + toast({ + title: "Success", + description: "User updated successfully!", + }); + break; + case 400: + // In theory, they should never be able to send out a request + // with missing fields due to disabled submission button + toast({ + title: "Missing Fields", + description: "Please fill in at least 1 field", + }); + return; + case 401: + toast({ + title: "Access denied", + description: "Invalid session", + }); + return; + case 403: + toast({ + title: "Access denied", + description: "Only admins can update other user", + }); + return; + case 404: + toast({ + title: "User not found", + description: "User with specified ID not found", + }); + return; + case 409: + toast({ + title: "Duplicated Username or Email", + description: "The username or email you entered is already in use", + }); + return; + case 500: + toast({ + title: "Server Error", + description: "The server encountered an error", + }); + return; + default: toast({ title: "Unknown Error", - description: "An unexpected error has occurred", + description: "An unexpected error has occured", }); - } - switch (response.status) { - case 200: - toast({ - title: "Success", - description: "User updated successfully!", - }); - break; - case 400: - // In theory, they should never be able to send out a request - // with missing fields due to disabled submission button - toast({ - title: "Missing Fields", - description: "Please fill in at least 1 field", - }); - return; - case 401: - toast({ - title: "Access denied", - description: "Invalid session", - }); - return; - case 403: - toast({ - title: "Access denied", - description: "Only admins can update other user", - }); - return; - case 404: - toast({ - title: "User not found", - description: "User with specified ID not found", - }); - return; - case 409: - toast({ - title: "Duplicated Username or Email", - description: "The username or email you entered is already in use", - }); - return; - case 500: - toast({ - title: "Server Error", - description: "The server encountered an error", - }); - return; - default: - toast({ - title: "Unknown Error", - description: "An unexpected error has occured", - }); - return; + return; } // Remove old states, update UI and close modal @@ -137,7 +151,9 @@ const AdminEditUserModal: React.FC = ({ id="username" placeholder={props.user?.username} value={editingUser?.username} - onChange={(e) => setEditingUser({...editingUser, username: e.target.value})} + onChange={(e) => + setEditingUser({ ...editingUser, username: e.target.value }) + } required /> @@ -150,7 +166,9 @@ const AdminEditUserModal: React.FC = ({ type="email" placeholder={props.user?.email} value={editingUser?.email} - onChange={(e) => setEditingUser({...editingUser, email: e.target.value})} + onChange={(e) => + setEditingUser({ ...editingUser, email: e.target.value }) + } required /> @@ -158,13 +176,15 @@ const AdminEditUserModal: React.FC = ({ - + diff --git a/frontend/components/admin-user-management/admin-user-management.tsx b/frontend/components/admin-user-management/admin-user-management.tsx index 59e47e7411..e7b1738225 100644 --- a/frontend/components/admin-user-management/admin-user-management.tsx +++ b/frontend/components/admin-user-management/admin-user-management.tsx @@ -56,13 +56,16 @@ export default function AdminUserManagement() { const [unauthorised, setUnauthorised] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(true); const [showModal, setShowModal] = useState(false); - const [selectedUser, setSelectedUser] = useState<{ - id: string; - username: string; - email: string; - isAdmin: boolean; - skillLevel: string; - } | undefined>(); + const [selectedUser, setSelectedUser] = useState< + | { + id: string; + username: string; + email: string; + isAdmin: boolean; + skillLevel: string; + } + | undefined + >(); useEffect(() => { if (data) { @@ -111,7 +114,7 @@ export default function AdminUserManagement() { const onUserUpdate = () => { mutate(); setSelectedUser(undefined); - } + }; return (
@@ -140,20 +143,21 @@ export default function AdminUserManagement() { {user.isAdmin ? "Admin" : "User"} {user.skillLevel} - diff --git a/frontend/lib/update-user.ts b/frontend/lib/update-user.ts index 262e4841ee..4f1c603c16 100644 --- a/frontend/lib/update-user.ts +++ b/frontend/lib/update-user.ts @@ -1,25 +1,24 @@ export const updateUser = async ( - jwtToken: string, - id: string, - username?: string, - email?: string, - password?: string, - skillLevel?: string, - ) => { - if (!username && !email && !password && !skillLevel) { - throw new Error("Require at least one field"); - } + jwtToken: string, + id: string, + username?: string, + email?: string, + password?: string, + skillLevel?: string +) => { + if (!username && !email && !password && !skillLevel) { + throw new Error("Require at least one field"); + } - const body = { username, email, password, skillLevel }; + const body = { username, email, password, skillLevel }; - const response = await fetch(`http://localhost:3001/users/${id}`, { - method: "PATCH", - headers: { - Authorization: `Bearer ${jwtToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - }); - return response; - }; - \ No newline at end of file + const response = await fetch(`http://localhost:3001/users/${id}`, { + method: "PATCH", + headers: { + Authorization: `Bearer ${jwtToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + return response; +}; From 87de0fa5e7768b3a61f8828e36ff856176aaecea Mon Sep 17 00:00:00 2001 From: jq1836 <95712150+jq1836@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:37:34 +0800 Subject: [PATCH 7/9] Update user-service README.md --- user-service/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/user-service/README.md b/user-service/README.md index be27594dbc..a655f7a9a2 100644 --- a/user-service/README.md +++ b/user-service/README.md @@ -134,13 +134,14 @@ - Required: `userId` path parameter - Body - - At least one of the following fields is required: `username` (string), `email` (string), `password` (string) + - At least one of the following fields is required: `username` (string), `email` (string), `password` (string), `skillLevel` (string) ```json { "username": "SampleUserName", "email": "sample@gmail.com", - "password": "SecurePassword" + "password": "SecurePassword", + "skillLevel": "SkillLevel", } ``` From c8c0ab228524f6da77a8049d62848947407e844f Mon Sep 17 00:00:00 2001 From: jq1836 <95712150+jq1836@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:50:29 +0800 Subject: [PATCH 8/9] Adjust modal field margins --- .../components/admin-user-management/admin-edit-user-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/admin-user-management/admin-edit-user-modal.tsx b/frontend/components/admin-user-management/admin-edit-user-modal.tsx index 376950dbfc..19f1f1774b 100644 --- a/frontend/components/admin-user-management/admin-edit-user-modal.tsx +++ b/frontend/components/admin-user-management/admin-edit-user-modal.tsx @@ -157,7 +157,7 @@ const AdminEditUserModal: React.FC = ({ required />
-
+
From 3e4dce22e6cb79a0cf4a5499f790a8183786a4eb Mon Sep 17 00:00:00 2001 From: jq1836 <95712150+jq1836@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:32:37 +0800 Subject: [PATCH 9/9] Fix issue with modal state persisting --- .../admin-edit-user-modal.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/components/admin-user-management/admin-edit-user-modal.tsx b/frontend/components/admin-user-management/admin-edit-user-modal.tsx index 19f1f1774b..47195c90b5 100644 --- a/frontend/components/admin-user-management/admin-edit-user-modal.tsx +++ b/frontend/components/admin-user-management/admin-edit-user-modal.tsx @@ -37,9 +37,14 @@ const AdminEditUserModal: React.FC = ({ >(); useEffect(() => { - setEditingUser(props?.user); + setEditingUser(props.user); }, [props.user]); + const closeModal = () => { + setEditingUser(props.user); + props.setShowModal(false); + }; + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); @@ -129,9 +134,8 @@ const AdminEditUserModal: React.FC = ({ } // Remove old states, update UI and close modal - setEditingUser(undefined); props.onUserUpdate(); - props.setShowModal(false); + closeModal(); }; return ( @@ -184,7 +188,9 @@ const AdminEditUserModal: React.FC = ({ > Save changes - +