From 4be191541e4c644f5f028c60f0440058ecd5ac3a Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Sat, 9 Aug 2025 18:29:50 +0800 Subject: [PATCH 01/52] added user management page with statistics boxes --- src/components/Identity/IAMStatisticBox.jsx | 13 +++++++++++ src/main.jsx | 6 +++++- src/pages/AdminConsole.jsx | 9 +++++++- src/pages/UserManagement.jsx | 24 +++++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/components/Identity/IAMStatisticBox.jsx create mode 100644 src/pages/UserManagement.jsx diff --git a/src/components/Identity/IAMStatisticBox.jsx b/src/components/Identity/IAMStatisticBox.jsx new file mode 100644 index 0000000..0eef154 --- /dev/null +++ b/src/components/Identity/IAMStatisticBox.jsx @@ -0,0 +1,13 @@ +import { Box, Text } from '@chakra-ui/react' +import React from 'react' + +function IAMStatisticBox({ metric, label }) { + return ( + + {metric} + {label} + + ) +} + +export default IAMStatisticBox \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index 66e63da..e934fcd 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -22,6 +22,7 @@ import PublicGallery from './pages/PublicGallery.jsx'; import PublicProfile from './pages/PublicProfile.jsx'; import Profile from './pages/Profile.jsx'; import AdminConsole from './pages/AdminConsole.jsx'; +import UserManagement from './pages/UserManagement.jsx'; const store = configureStore({ reducer: { @@ -51,7 +52,10 @@ createRoot(document.getElementById('root')).render( {/* Protected Pages */} }> } /> - } /> + + } /> + } /> + diff --git a/src/pages/AdminConsole.jsx b/src/pages/AdminConsole.jsx index 5ae7505..5407d89 100644 --- a/src/pages/AdminConsole.jsx +++ b/src/pages/AdminConsole.jsx @@ -1,7 +1,14 @@ import { Box, Button, Card, Text } from '@chakra-ui/react' import React from 'react' +import { useNavigate } from 'react-router-dom' function AdminConsole() { + const navigate = useNavigate(); + + const handleNav = (path) => { + navigate(path); + } + return ( Admin Console @@ -14,7 +21,7 @@ function AdminConsole() { - + diff --git a/src/pages/UserManagement.jsx b/src/pages/UserManagement.jsx new file mode 100644 index 0000000..daf1162 --- /dev/null +++ b/src/pages/UserManagement.jsx @@ -0,0 +1,24 @@ +import { Box, Button, Text } from '@chakra-ui/react' +import React from 'react' +import { BsPlusCircleFill } from 'react-icons/bs' +import { FaPlus, FaPlusCircle } from 'react-icons/fa' +import IAMStatisticBox from '../components/Identity/IAMStatisticBox' + +function UserManagement() { + return ( + + + User Management + + + + + + + + + + ) +} + +export default UserManagement \ No newline at end of file From 0cf5723df7f6835c30d1c1fc04fa0a5dedbd7897 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Sat, 9 Aug 2025 18:39:53 +0800 Subject: [PATCH 02/52] added new IAMUserCard component for account card on user mgmt page --- src/components/Identity/IAMUserCard.jsx | 18 ++++++++++++++++++ src/pages/UserManagement.jsx | 11 ++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 src/components/Identity/IAMUserCard.jsx diff --git a/src/components/Identity/IAMUserCard.jsx b/src/components/Identity/IAMUserCard.jsx new file mode 100644 index 0000000..ffc4201 --- /dev/null +++ b/src/components/Identity/IAMUserCard.jsx @@ -0,0 +1,18 @@ +import { Avatar, Text, VStack } from '@chakra-ui/react' +import React from 'react' + +function IAMUserCard({ userData=null }) { + return ( + + + + + + + Tohnation + Discord Mod + + ) +} + +export default IAMUserCard \ No newline at end of file diff --git a/src/pages/UserManagement.jsx b/src/pages/UserManagement.jsx index daf1162..2ffba7b 100644 --- a/src/pages/UserManagement.jsx +++ b/src/pages/UserManagement.jsx @@ -1,8 +1,7 @@ import { Box, Button, Text } from '@chakra-ui/react' -import React from 'react' -import { BsPlusCircleFill } from 'react-icons/bs' -import { FaPlus, FaPlusCircle } from 'react-icons/fa' +import { FaPlus } from 'react-icons/fa' import IAMStatisticBox from '../components/Identity/IAMStatisticBox' +import IAMUserCard from '../components/Identity/IAMUserCard' function UserManagement() { return ( @@ -17,6 +16,12 @@ function UserManagement() { + + Manage & View Accounts + + + + ) } From 4f9b016b08945f4ad11518a8593f6fc6c372ad2e Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Sat, 9 Aug 2025 23:28:50 +0800 Subject: [PATCH 03/52] improved accounts layout with SimpleGrid instead of flex box --- src/components/Identity/IAMStatisticBox.jsx | 2 +- src/components/Identity/IAMUserCard.jsx | 4 ++-- src/pages/UserManagement.jsx | 13 +++++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/Identity/IAMStatisticBox.jsx b/src/components/Identity/IAMStatisticBox.jsx index 0eef154..0e62439 100644 --- a/src/components/Identity/IAMStatisticBox.jsx +++ b/src/components/Identity/IAMStatisticBox.jsx @@ -3,7 +3,7 @@ import React from 'react' function IAMStatisticBox({ metric, label }) { return ( - + {metric} {label} diff --git a/src/components/Identity/IAMUserCard.jsx b/src/components/Identity/IAMUserCard.jsx index ffc4201..cf651b8 100644 --- a/src/components/Identity/IAMUserCard.jsx +++ b/src/components/Identity/IAMUserCard.jsx @@ -3,14 +3,14 @@ import React from 'react' function IAMUserCard({ userData=null }) { return ( - + Tohnation - Discord Mod + Discord Mod ) } diff --git a/src/pages/UserManagement.jsx b/src/pages/UserManagement.jsx index 2ffba7b..f45f3b9 100644 --- a/src/pages/UserManagement.jsx +++ b/src/pages/UserManagement.jsx @@ -1,4 +1,4 @@ -import { Box, Button, Text } from '@chakra-ui/react' +import { Box, Button, SimpleGrid, Text } from '@chakra-ui/react' import { FaPlus } from 'react-icons/fa' import IAMStatisticBox from '../components/Identity/IAMStatisticBox' import IAMUserCard from '../components/Identity/IAMUserCard' @@ -19,9 +19,14 @@ function UserManagement() { Manage & View Accounts - - - + + + ) } From e8647b63230e022106de07dbb45e032997f33186 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Sun, 10 Aug 2025 00:01:35 +0800 Subject: [PATCH 04/52] added retrieval of user data and dynamic card content --- src/components/Identity/IAMUserCard.jsx | 14 +++-- src/pages/UserManagement.jsx | 74 ++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/components/Identity/IAMUserCard.jsx b/src/components/Identity/IAMUserCard.jsx index cf651b8..5328a1f 100644 --- a/src/components/Identity/IAMUserCard.jsx +++ b/src/components/Identity/IAMUserCard.jsx @@ -1,16 +1,20 @@ import { Avatar, Text, VStack } from '@chakra-ui/react' import React from 'react' -function IAMUserCard({ userData=null }) { +function IAMUserCard({ userData }) { return ( - + - + - Tohnation - Discord Mod + {userData.fname + ' ' + userData.lname} + {userData.role ? ( + {userData.role} + ): ( + Role Unavailable + )} ) } diff --git a/src/pages/UserManagement.jsx b/src/pages/UserManagement.jsx index f45f3b9..c7e728f 100644 --- a/src/pages/UserManagement.jsx +++ b/src/pages/UserManagement.jsx @@ -1,9 +1,69 @@ -import { Box, Button, SimpleGrid, Text } from '@chakra-ui/react' +import { Box, Button, SimpleGrid, Skeleton, Spinner, Text } from '@chakra-ui/react' import { FaPlus } from 'react-icons/fa' import IAMStatisticBox from '../components/Identity/IAMStatisticBox' import IAMUserCard from '../components/Identity/IAMUserCard' +import { useNavigate } from 'react-router-dom' +import { useSelector } from 'react-redux' +import { useState } from 'react' +import server, { JSONResponse } from '../networking'; +import ToastWizard from '../components/toastWizard'; +import { useEffect } from 'react' function UserManagement() { + const navigate = useNavigate(); + const { loaded } = useSelector(state => state.auth); + + const [userData, setUserData] = useState({}); + const [retrievingUsers, setRetrievingUsers] = useState(true); + + const retrieveUserData = async () => { + setRetrievingUsers(true); + try { + const response = await server.get('/admin/listUsers') + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw new Error(errObject); + } + + // Success case + var rawData = response.data.raw.users; // dictionary where userID: userDict + for (const userID in rawData) { + rawData[userID].userID = userID; // add userID to each user object + } + + setUserData(rawData); + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in login request:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard("error", "Failed to retrieve users.", err.response.data.message); + } else { + ToastWizard.standard("error", "Something went wrong", "Failed to retrieve users. Please try again later."); + } + } else { + console.log("Unexpected error in login request:", err); + ToastWizard.standard("error", "Something went wrong", "Failed to retrieve users. Please try again later."); + } + } finally { + setRetrievingUsers(false); + } + } + + useEffect(() => { + if (loaded) { + retrieveUserData(); + } + }, [loaded]); + return ( @@ -25,7 +85,17 @@ function UserManagement() { mt={'20px'} w={'100%'} > - + {retrievingUsers ? ( + + ) : ( + Object.keys(userData).length > 0 ? ( + Object.values(userData).map((user) => ( + + )) + ) : ( + No regular users found. + ) + )} ) From 3e1e80e93105865ccfd9261816a2ece697cabc79 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Sun, 10 Aug 2025 00:26:05 +0800 Subject: [PATCH 05/52] fixed broken cdn pfp link in IAMUserCard --- src/components/Identity/IAMUserCard.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Identity/IAMUserCard.jsx b/src/components/Identity/IAMUserCard.jsx index 5328a1f..9f2ef98 100644 --- a/src/components/Identity/IAMUserCard.jsx +++ b/src/components/Identity/IAMUserCard.jsx @@ -6,10 +6,10 @@ function IAMUserCard({ userData }) { - + - - {userData.fname + ' ' + userData.lname} + + {userData.fname + ' ' + userData.lname || userData.username} {userData.role ? ( {userData.role} ): ( From 0552ec38a417204779efd281b148e9bd21106d84 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Sun, 10 Aug 2025 12:35:03 +0800 Subject: [PATCH 06/52] changed new user button to IconButton added tooltip to IAMUserCard --- src/components/Identity/IAMUserCard.jsx | 34 +++++++++++++++---------- src/pages/UserManagement.jsx | 4 +-- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/components/Identity/IAMUserCard.jsx b/src/components/Identity/IAMUserCard.jsx index 9f2ef98..4134d43 100644 --- a/src/components/Identity/IAMUserCard.jsx +++ b/src/components/Identity/IAMUserCard.jsx @@ -1,21 +1,27 @@ -import { Avatar, Text, VStack } from '@chakra-ui/react' +import { Avatar, Button, CloseButton, Dialog, Portal, Text, VStack } from '@chakra-ui/react' +import { Tooltip } from '../ui/tooltip' import React from 'react' +import { useNavigate } from 'react-router-dom' function IAMUserCard({ userData }) { + const navigate = useNavigate(); + return ( - - - - - - - {userData.fname + ' ' + userData.lname || userData.username} - {userData.role ? ( - {userData.role} - ): ( - Role Unavailable - )} - + + + + + + + + {userData.fname + ' ' + userData.lname || userData.username} + {userData.role ? ( + {userData.role} + ) : ( + Role Unavailable + )} + + ) } diff --git a/src/pages/UserManagement.jsx b/src/pages/UserManagement.jsx index c7e728f..7a97abc 100644 --- a/src/pages/UserManagement.jsx +++ b/src/pages/UserManagement.jsx @@ -1,4 +1,4 @@ -import { Box, Button, SimpleGrid, Skeleton, Spinner, Text } from '@chakra-ui/react' +import { Box, Button, IconButton, SimpleGrid, Skeleton, Spinner, Text } from '@chakra-ui/react' import { FaPlus } from 'react-icons/fa' import IAMStatisticBox from '../components/Identity/IAMStatisticBox' import IAMUserCard from '../components/Identity/IAMUserCard' @@ -68,7 +68,7 @@ function UserManagement() { User Management - + From 388c2b8009fa688a70e4681a33c06e7962acd6ea Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Sun, 10 Aug 2025 12:56:51 +0800 Subject: [PATCH 07/52] working on fixing state desync issues to make Profile more versatile for superusers --- src/components/Identity/IAMUserCard.jsx | 2 +- src/components/Identity/ProfileAvatar.jsx | 6 ++--- src/main.jsx | 1 + src/pages/Profile.jsx | 33 +++++++++++++++++------ 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/components/Identity/IAMUserCard.jsx b/src/components/Identity/IAMUserCard.jsx index 4134d43..0f5d4da 100644 --- a/src/components/Identity/IAMUserCard.jsx +++ b/src/components/Identity/IAMUserCard.jsx @@ -8,7 +8,7 @@ function IAMUserCard({ userData }) { return ( - + navigate(`/profile/${userData.userID}`)} px={'30px'} py={'20px'} w={'100%'} boxShadow={'lg'} borderRadius={'lg'} bgColor={'white'} cursor={'pointer'} _hover={{ boxShadow: '2xl' }} transition={'box-shadow 0.2s ease-in-out'}> diff --git a/src/components/Identity/ProfileAvatar.jsx b/src/components/Identity/ProfileAvatar.jsx index 0b974af..f4303ab 100644 --- a/src/components/Identity/ProfileAvatar.jsx +++ b/src/components/Identity/ProfileAvatar.jsx @@ -4,10 +4,10 @@ import ToastWizard from '../toastWizard'; import server, { JSONResponse } from '../../networking'; import { Avatar, Spinner } from '@chakra-ui/react'; -function ProfileAvatar({ accountInfo }) { +function ProfileAvatar({ accountInfo, targetProfileID }) { const { username, accountID } = useSelector(state => state.auth); - const [avatarSrc, setAvatarSrc] = useState(accountID && `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${accountID}`); + const [avatarSrc, setAvatarSrc] = useState(accountID && `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}`); const [avatarUploading, setAvatarUploading] = useState(false); const fileInputRef = useRef(null); @@ -73,7 +73,7 @@ function ProfileAvatar({ accountInfo }) { } } - setAvatarSrc(accountID && `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${accountID}?t=${Date.now()}`); + setAvatarSrc(accountID && `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}?t=${Date.now()}`); setAvatarUploading(false); } diff --git a/src/main.jsx b/src/main.jsx index 65f7d87..6c3bda3 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -63,6 +63,7 @@ createRoot(document.getElementById('root')).render( {/* Protected Pages */} }> } /> + } /> } /> } /> diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 2329c60..25fe885 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -1,12 +1,11 @@ import { Avatar, Box, Button, CloseButton, Dialog, Field, FileUpload, Flex, Grid, GridItem, HStack, Input, Portal, Skeleton, SkeletonText, Spacer, Spinner, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' import { useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { withMask } from 'use-mask-input'; import { logout } from '../slices/AuthState'; import ToastWizard from '../components/toastWizard'; import server, { JSONResponse } from '../networking'; -import { GiExitDoor } from 'react-icons/gi'; import { IoLogOutOutline } from 'react-icons/io5'; import ProfileActionBar from '../components/Identity/ProfileActionBar'; import AccountActivityCard from '../components/Identity/AccountActivityCard'; @@ -16,8 +15,12 @@ import ProfileAvatar from '../components/Identity/ProfileAvatar'; function Profile() { const navigate = useNavigate(); const dispatch = useDispatch(); + const { userID } = useParams(); + const location = useLocation(); - const { username, accountID } = useSelector(state => state.auth); + const { username, accountID, superuser } = useSelector(state => state.auth); + + const [targetProfileID, setTargetProfileID] = useState(userID || accountID); // If userID is provided, use that, otherwise use the logged-in user's accountID // Loading states const [loggingOut, setLoggingOut] = useState(false); @@ -34,6 +37,11 @@ function Profile() { const [isSmallerThan800] = useMediaQuery("(max-width: 800px)"); const handleLogout = () => { + if (targetProfileID !== accountID) { + ToastWizard.standard("error", "You cannot log out another user", "Please go to your own profile to log out."); + return; + } + setLoggingOut(true); const logoutPromise = new Promise((resolve, reject) => { dispatch(logout(true, (msg) => { @@ -89,7 +97,7 @@ function Profile() { const getAccountInfo = async () => { setInfoLoading(true); try { - const response = await server.get(`/cdn/profileInfo/${accountID}?includeLogs=true`); + const response = await server.get(`/cdn/profileInfo/${targetProfileID}?includeLogs=true`); if (response.data instanceof JSONResponse) { if (response.data.isErrorStatus()) { @@ -129,10 +137,19 @@ function Profile() { } useEffect(() => { - if (username) { + if (targetProfileID) { + getAccountInfo(); + } + }, [targetProfileID]) + + useEffect(() => { + if (userID && userID !== accountID && superuser === true) { + setTargetProfileID(userID); getAccountInfo(); + } else { + setTargetProfileID(accountID); } - }, []) + }, [userID, location]) // useEffect(() => { // console.log(accountInfo) @@ -219,9 +236,9 @@ function Profile() { - + {targetProfileID === accountID && } - + {targetProfileID === accountID && } From 7b01b7ac60b6656a0b3ff81442a476f73a649fcb Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Sun, 10 Aug 2025 14:04:30 +0800 Subject: [PATCH 08/52] updated ProfileActionBar and ProfileAvatar to consider whether accessor is superuser --- src/components/Identity/ProfileActionBar.jsx | 1 + src/components/Identity/ProfileAvatar.jsx | 42 +++++++++++++++----- src/pages/Profile.jsx | 6 ++- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/components/Identity/ProfileActionBar.jsx b/src/components/Identity/ProfileActionBar.jsx index 2a87cc8..cbe62e9 100644 --- a/src/components/Identity/ProfileActionBar.jsx +++ b/src/components/Identity/ProfileActionBar.jsx @@ -33,6 +33,7 @@ function ProfileActionBar({ accountInfo, originalAccountInfo, setAccountInfo, ge if (accountInfo.lname !== originalAccountInfo.lname) updateDict.lname = accountInfo.lname; if (accountInfo.contact !== originalAccountInfo.contact) updateDict.contact = accountInfo.contact; if (accountInfo.email !== originalAccountInfo.email) updateDict.email = accountInfo.email; + if (targetProfileID) updateDict.userID = targetProfileID; try { const response = await server.post('/profile/update', updateDict); diff --git a/src/components/Identity/ProfileAvatar.jsx b/src/components/Identity/ProfileAvatar.jsx index f4303ab..48e3b24 100644 --- a/src/components/Identity/ProfileAvatar.jsx +++ b/src/components/Identity/ProfileAvatar.jsx @@ -2,7 +2,8 @@ import React, { useRef, useState } from 'react' import { useSelector } from 'react-redux'; import ToastWizard from '../toastWizard'; import server, { JSONResponse } from '../../networking'; -import { Avatar, Spinner } from '@chakra-ui/react'; +import { Avatar, Float, IconButton, Spinner } from '@chakra-ui/react'; +import { LuTrash2 } from 'react-icons/lu'; function ProfileAvatar({ accountInfo, targetProfileID }) { const { username, accountID } = useSelector(state => state.auth); @@ -12,6 +13,12 @@ function ProfileAvatar({ accountInfo, targetProfileID }) { const fileInputRef = useRef(null); const handleFileChange = async (e) => { + if (targetProfileID !== accountID) { + ToastWizard.standard("error", "You cannot change the avatar of another user, despite being a super user."); + fileInputRef.current.value = null; + return; + } + if (e.target.files.length === 0) { ToastWizard.standard("warning", "No avatar image selected for upload") return; @@ -82,17 +89,32 @@ function ProfileAvatar({ accountInfo, targetProfileID }) { }; return <> - - + {targetProfileID === accountID && + + } + + {accountInfo.pfp === true && + + + + + + } {avatarUploading && } diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 25fe885..200a1ae 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -138,12 +138,14 @@ function Profile() { useEffect(() => { if (targetProfileID) { + console.log('Detected change', targetProfileID) getAccountInfo(); } }, [targetProfileID]) useEffect(() => { if (userID && userID !== accountID && superuser === true) { + console.log(userID) setTargetProfileID(userID); getAccountInfo(); } else { @@ -172,7 +174,7 @@ function Profile() { > - + {fullName} @@ -243,7 +245,7 @@ function Profile() { - + } From b96e42e6b7b0b3a70eb0429b35a0496431b050ac Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Sun, 10 Aug 2025 14:08:54 +0800 Subject: [PATCH 09/52] added info alert to warn about superuser access --- src/pages/Profile.jsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 200a1ae..7dfe351 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -1,4 +1,4 @@ -import { Avatar, Box, Button, CloseButton, Dialog, Field, FileUpload, Flex, Grid, GridItem, HStack, Input, Portal, Skeleton, SkeletonText, Spacer, Spinner, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' +import { Alert, Avatar, Box, Button, CloseButton, Dialog, Field, FileUpload, Flex, Grid, GridItem, HStack, Input, Portal, Skeleton, SkeletonText, Spacer, Spinner, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' import { useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; @@ -174,6 +174,18 @@ function Profile() { > + {targetProfileID !== accountID && + + + + Superuser Access + + You're viewing this profile with superuser privileges. You're allowed to carry out some actions on this profile, but be cautious as changes will affect the user directly. + + + + } + From 118caea9d96613961e33cd4d612b858bf23f7847 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Mon, 11 Aug 2025 21:34:46 +0800 Subject: [PATCH 10/52] fixed state issues with targetProfileID --- .../Identity/AccountActivityCard.jsx | 4 +- src/pages/Profile.jsx | 37 +++++++++---------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/components/Identity/AccountActivityCard.jsx b/src/components/Identity/AccountActivityCard.jsx index 2f5bea3..713b8ca 100644 --- a/src/components/Identity/AccountActivityCard.jsx +++ b/src/components/Identity/AccountActivityCard.jsx @@ -10,7 +10,7 @@ function AccountActivityCard({ infoLoading, auditLogsData }) { Account Activity - + {auditLogsData.length == 0 ? ( <> @@ -19,7 +19,7 @@ function AccountActivityCard({ infoLoading, auditLogsData }) { 💤 Oops! No account activity yet. ) : ( - } gap={4}> + } gap={4} pb={'20px'}> {auditLogsData.map((log, index) => ( diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 7dfe351..59694ac 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -1,4 +1,4 @@ -import { Alert, Avatar, Box, Button, CloseButton, Dialog, Field, FileUpload, Flex, Grid, GridItem, HStack, Input, Portal, Skeleton, SkeletonText, Spacer, Spinner, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' +import { Alert, Avatar, Box, Button, CloseButton, Dialog, Field, FileUpload, Flex, Grid, GridItem, HStack, Input, Portal, Skeleton, SkeletonCircle, SkeletonText, Spacer, Spinner, Stack, Text, useMediaQuery, VStack } from '@chakra-ui/react' import { useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; @@ -11,6 +11,7 @@ import ProfileActionBar from '../components/Identity/ProfileActionBar'; import AccountActivityCard from '../components/Identity/AccountActivityCard'; import ChangePasswordDialogButton from '../components/Identity/ChangePasswordDialogButton'; import ProfileAvatar from '../components/Identity/ProfileAvatar'; +import CentredSpinner from '../components/CentredSpinner'; function Profile() { const navigate = useNavigate(); @@ -18,9 +19,9 @@ function Profile() { const { userID } = useParams(); const location = useLocation(); - const { username, accountID, superuser } = useSelector(state => state.auth); + const { username, accountID, superuser, loaded } = useSelector(state => state.auth); - const [targetProfileID, setTargetProfileID] = useState(userID || accountID); // If userID is provided, use that, otherwise use the logged-in user's accountID + const [targetProfileID, setTargetProfileID] = useState(null); // If userID is provided, use that, otherwise use the logged-in user's accountID // Loading states const [loggingOut, setLoggingOut] = useState(false); @@ -116,6 +117,7 @@ function Profile() { setOriginalAccountInfo(preppedAccInfo); setAccountInfo(preppedAccInfo); setAuditLogsData(processAuditLogs(response.data.raw.info.logs || [])); + setInfoLoading(false); } else { throw new Error("Unexpected response format"); } @@ -132,31 +134,24 @@ function Profile() { ToastWizard.standard("error", "Couldn't retrieve profile.", "Failed to retrieve your information. Please try again.", 3000, true, getAccountInfo, 'Retry'); } } - - setInfoLoading(false); } useEffect(() => { if (targetProfileID) { - console.log('Detected change', targetProfileID) getAccountInfo(); } }, [targetProfileID]) useEffect(() => { - if (userID && userID !== accountID && superuser === true) { - console.log(userID) - setTargetProfileID(userID); - getAccountInfo(); - } else { - setTargetProfileID(accountID); + if (loaded) { + if (userID && userID !== accountID && !superuser) { + console.log("Unauthorised access to superuser protected resource; redirecting...") + navigate('/profile'); + } else { + setTargetProfileID(userID || accountID); + } } - }, [userID, location]) - - // useEffect(() => { - // console.log(accountInfo) - // console.log(auditLogsData) - // }, [accountInfo, auditLogsData]) + }, [loaded, userID]) const handleEnterKey = (e) => { if (e.key === 'Enter') { @@ -170,7 +165,7 @@ function Profile() { templateColumns={isSmallerThan800 ? "repeat(1, 1fr)" : "repeat(2, 1fr)"} gap={10} p={isSmallerThan800 ? '10px 0px 30px 0px' : '20px'} - mt={isSmallerThan800 ? '30px' : '30px'} + mt={'10px'} > @@ -186,7 +181,9 @@ function Profile() { } - + + + {fullName} From b23ad12d5d91dd1665235fb45418dbe31611c306 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Mon, 11 Aug 2025 21:42:16 +0800 Subject: [PATCH 11/52] fixed rendering lag with ProfileAvatar --- src/components/Identity/ProfileAvatar.jsx | 10 +++++++--- src/components/Navbar.jsx | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/Identity/ProfileAvatar.jsx b/src/components/Identity/ProfileAvatar.jsx index 48e3b24..20af108 100644 --- a/src/components/Identity/ProfileAvatar.jsx +++ b/src/components/Identity/ProfileAvatar.jsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { useSelector } from 'react-redux'; import ToastWizard from '../toastWizard'; import server, { JSONResponse } from '../../networking'; @@ -8,7 +8,7 @@ import { LuTrash2 } from 'react-icons/lu'; function ProfileAvatar({ accountInfo, targetProfileID }) { const { username, accountID } = useSelector(state => state.auth); - const [avatarSrc, setAvatarSrc] = useState(accountID && `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}`); + const [avatarSrc, setAvatarSrc] = useState(targetProfileID ? `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}?t=${Date.now()}`: null); const [avatarUploading, setAvatarUploading] = useState(false); const fileInputRef = useRef(null); @@ -80,7 +80,7 @@ function ProfileAvatar({ accountInfo, targetProfileID }) { } } - setAvatarSrc(accountID && `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}?t=${Date.now()}`); + setAvatarSrc(targetProfileID ? `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}?t=${Date.now()}`: null); setAvatarUploading(false); } @@ -88,6 +88,10 @@ function ProfileAvatar({ accountInfo, targetProfileID }) { fileInputRef.current.click(); }; + useEffect(() => { + setAvatarSrc(targetProfileID ? `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}?t=${Date.now()}`: null); + }, [targetProfileID]); + return <> {targetProfileID === accountID && ArchAIve - + From d22e2f8819d9174bfd0b67b8ce5e4e1f3b4f4a8f Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Mon, 11 Aug 2025 22:13:19 +0800 Subject: [PATCH 12/52] completed delete picture functionality --- src/components/Identity/ProfileAvatar.jsx | 85 +++++++++++++++++++---- src/pages/Profile.jsx | 2 +- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/src/components/Identity/ProfileAvatar.jsx b/src/components/Identity/ProfileAvatar.jsx index 20af108..b9ce4cf 100644 --- a/src/components/Identity/ProfileAvatar.jsx +++ b/src/components/Identity/ProfileAvatar.jsx @@ -2,14 +2,16 @@ import React, { useEffect, useRef, useState } from 'react' import { useSelector } from 'react-redux'; import ToastWizard from '../toastWizard'; import server, { JSONResponse } from '../../networking'; -import { Avatar, Float, IconButton, Spinner } from '@chakra-ui/react'; +import { Avatar, Float, IconButton, Popover, Portal, Spinner, VStack } from '@chakra-ui/react'; import { LuTrash2 } from 'react-icons/lu'; +import { BiCloudUpload } from 'react-icons/bi'; -function ProfileAvatar({ accountInfo, targetProfileID }) { +function ProfileAvatar({ accountInfo, targetProfileID, reloadAccountInfo }) { const { username, accountID } = useSelector(state => state.auth); - const [avatarSrc, setAvatarSrc] = useState(targetProfileID ? `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}?t=${Date.now()}`: null); + const [avatarSrc, setAvatarSrc] = useState(targetProfileID ? `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}?t=${Date.now()}` : null); const [avatarUploading, setAvatarUploading] = useState(false); + const [deletingPicture, setDeletingPicture] = useState(false); const fileInputRef = useRef(null); const handleFileChange = async (e) => { @@ -63,6 +65,7 @@ function ProfileAvatar({ accountInfo, targetProfileID }) { // Success case ToastWizard.standard("success", "Avatar updated successfully!"); + reloadAccountInfo(); } else { throw new Error("Unexpected response format"); } @@ -72,24 +75,77 @@ function ProfileAvatar({ accountInfo, targetProfileID }) { if (err.response.data.userErrorType()) { ToastWizard.standard("error", "Avatar update failed.", err.response.data.message); } else { - ToastWizard.standard("error", "Something went wrong in updating your avatar.", "Please try again."); + ToastWizard.standard("error", "Something went wrong", "Couldn't update avatar. Please try again."); } } else { - console.log("Unexpected error in login request:", err); - ToastWizard.standard("error", "Something went wrong in updating your avatar.", "Please try again."); + console.log("Unexpected error in avatar update request:", err); + ToastWizard.standard("error", "Something went wrong", "Couldn't update avatar. Please try again."); } } - setAvatarSrc(targetProfileID ? `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}?t=${Date.now()}`: null); + setAvatarSrc(targetProfileID ? `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}?t=${Date.now()}` : null); setAvatarUploading(false); } + const handleDeletePicture = async (e) => { + e.stopPropagation(); // Prevent triggering the avatar click event + + if (accountInfo.pfp !== true) { + ToastWizard.standard("error", "No profile picture to delete."); + return; + } + + setDeletingPicture(true); + + try { + const response = await server.post('/profile/deletePicture', { + userID: targetProfileID + }); + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw new Error(errObject); + } + + // Success case + ToastWizard.standard("success", "Avatar deleted successfully."); + setAvatarSrc(targetProfileID ? `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}?t=${Date.now()}` : null); + reloadAccountInfo(); + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in avatar deletion request:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard("error", "Avatar deletion failed.", err.response.data.message); + } else { + ToastWizard.standard("error", "Something went wrong", "Couldn't delete avatar. Please try again."); + } + } else { + console.log("Unexpected error in avatar deletion request:", err); + ToastWizard.standard("error", "Something went wrong", "Couldn't delete avatar. Please try again."); + } + } finally { + setDeletingPicture(false); + } + } + const handleAvatarClick = (e) => { - fileInputRef.current.click(); + if (fileInputRef.current) { + fileInputRef.current.click(); + } else { + ToastWizard.standard("warning", "Superusers cannot change user avatars.") + } }; useEffect(() => { - setAvatarSrc(targetProfileID ? `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}?t=${Date.now()}`: null); + setAvatarSrc(targetProfileID ? `${import.meta.env.VITE_BACKEND_URL}/cdn/pfp/${targetProfileID}?t=${Date.now()}` : null); }, [targetProfileID]); return <> @@ -103,24 +159,25 @@ function ProfileAvatar({ accountInfo, targetProfileID }) { style={{ display: 'none' }} /> } - + - {accountInfo.pfp === true && + {(accountInfo.pfp === true || avatarUploading) && - + {avatarUploading || deletingPicture ? : } } - {avatarUploading && } } diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 59694ac..3aa680a 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -182,7 +182,7 @@ function Profile() { } - + From 7c7035c711e51ae11a4617136e388a6af262d942 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Mon, 11 Aug 2025 22:38:58 +0800 Subject: [PATCH 13/52] added ability to change role --- src/components/Identity/ProfileActionBar.jsx | 6 +++++- src/pages/Profile.jsx | 21 ++++++++++---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/components/Identity/ProfileActionBar.jsx b/src/components/Identity/ProfileActionBar.jsx index cbe62e9..9750238 100644 --- a/src/components/Identity/ProfileActionBar.jsx +++ b/src/components/Identity/ProfileActionBar.jsx @@ -4,12 +4,15 @@ import { useEffect, useState } from 'react' import { LuSquarePlus, LuTrash2 } from 'react-icons/lu'; import ToastWizard from '../toastWizard'; import server, { JSONResponse } from '../../networking'; +import { useSelector } from 'react-redux'; -function ProfileActionBar({ accountInfo, originalAccountInfo, setAccountInfo, getAccountInfo, enterKeyHit }) { +function ProfileActionBar({ accountInfo, originalAccountInfo, setAccountInfo, getAccountInfo, enterKeyHit, targetProfileID }) { const [changesMade, setChangesMade] = useState(false); const [isConfirmSaveOpen, setIsConfirmSaveOpen] = useState(false); const [saveLoading, setSaveLoading] = useState(false); + const { superuser } = useSelector(state => state.auth); + useEffect(() => { setChangesMade(!isEqual(accountInfo, originalAccountInfo)); }, [accountInfo, originalAccountInfo]) @@ -33,6 +36,7 @@ function ProfileActionBar({ accountInfo, originalAccountInfo, setAccountInfo, ge if (accountInfo.lname !== originalAccountInfo.lname) updateDict.lname = accountInfo.lname; if (accountInfo.contact !== originalAccountInfo.contact) updateDict.contact = accountInfo.contact; if (accountInfo.email !== originalAccountInfo.email) updateDict.email = accountInfo.email; + if (accountInfo.role !== originalAccountInfo.role && superuser === true) updateDict.role = accountInfo.role; if (targetProfileID) updateDict.userID = targetProfileID; try { diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 3aa680a..5a415e3 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -161,13 +161,13 @@ function Profile() { return <> - + {targetProfileID !== accountID && @@ -193,39 +193,38 @@ function Profile() { Name & Account Details - + First Name setAccountInfo({ ...accountInfo, fname: e.target.value })} /> - - Last Name setAccountInfo({ ...accountInfo, lname: e.target.value })} /> - - Username setAccountInfo({ ...accountInfo, username: e.target.value })} /> - - Email setAccountInfo({ ...accountInfo, email: e.target.value })} /> - - Contact setAccountInfo({ ...accountInfo, contact: e.target.value })} /> + + {superuser === true && <> + + Role + setAccountInfo({ ...accountInfo, role: e.target.value })} /> + + } From ae57786b8efd3d189227e330fe39d28b0e341c8a Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Mon, 11 Aug 2025 22:41:05 +0800 Subject: [PATCH 14/52] fixed conditional increase of acc details card rows num --- src/pages/Profile.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 5a415e3..0eaf22f 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -167,8 +167,8 @@ function Profile() { p={isSmallerThan800 ? '10px 0px 30px 0px' : '20px'} mt={'10px'} > - - + + {targetProfileID !== accountID && From c740874461188ffe16136a8528b525aaff17a6e8 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Mon, 11 Aug 2025 22:45:25 +0800 Subject: [PATCH 15/52] added admin badge if user is admin --- src/components/Sidebar.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 8ef87a5..e82df02 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -1,4 +1,4 @@ -import { Box, Button, CloseButton, Drawer, Image, Portal, Span, Text, VStack } from '@chakra-ui/react' +import { Badge, Box, Button, CloseButton, Drawer, Image, Portal, Span, Text, VStack } from '@chakra-ui/react' import React, { useState } from 'react' import { useSelector } from 'react-redux' import logoVisual from '../assets/logoVisual.svg'; @@ -72,6 +72,9 @@ function Sidebar({ isOpen, onOpenChange }) { + {superuser === true && ( + ADMIN + )} {username && ( Signed in: {username} )} From a3dc77fc86753471dee7c5d21fd023a439509548 Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 12 Aug 2025 01:39:42 +0800 Subject: [PATCH 16/52] Fixed path routes for group view and artefact editor --- src/main.jsx | 6 ++---- src/pages/ArtefactEditor.jsx | 4 ++-- src/pages/GroupView.jsx | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main.jsx b/src/main.jsx index c37405d..a73c062 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -52,10 +52,8 @@ createRoot(document.getElementById('root')).render( } /> - - } /> - } /> - + } /> + } /> diff --git a/src/pages/ArtefactEditor.jsx b/src/pages/ArtefactEditor.jsx index 5a297fe..e0494cb 100644 --- a/src/pages/ArtefactEditor.jsx +++ b/src/pages/ArtefactEditor.jsx @@ -2,9 +2,9 @@ import { Box, Flex } from "@chakra-ui/react"; import { useParams } from 'react-router-dom'; function ArtefactEditor() { - const { artID } = useParams(); + const { colID, artID } = useParams(); return ( - ArtefactEditor for {artID} + ArtefactEditor for {colID} {artID} ); } diff --git a/src/pages/GroupView.jsx b/src/pages/GroupView.jsx index 575240f..d3fbe4d 100644 --- a/src/pages/GroupView.jsx +++ b/src/pages/GroupView.jsx @@ -1,9 +1,9 @@ import { useParams } from 'react-router-dom'; function GroupView() { - const { artID, colID } = useParams(); + const { colID } = useParams(); - return
GroupView for collection {colID}, {artID}
; + return
GroupView for collection {colID}
; } export default GroupView; From 3aa6174e48a019df396373c398c92aa11deb7f3f Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 12 Aug 2025 02:06:51 +0800 Subject: [PATCH 17/52] Added fetch metadata request for artefact editor --- src/pages/ArtefactEditor.jsx | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/pages/ArtefactEditor.jsx b/src/pages/ArtefactEditor.jsx index e0494cb..7d6c2b5 100644 --- a/src/pages/ArtefactEditor.jsx +++ b/src/pages/ArtefactEditor.jsx @@ -1,8 +1,73 @@ import { Box, Flex } from "@chakra-ui/react"; import { useParams } from 'react-router-dom'; +import { useEffect, useRef, useState } from "react"; +import ToastWizard from "../components/toastWizard"; +import server, { JSONResponse } from "../networking"; function ArtefactEditor() { const { colID, artID } = useParams(); + + // Metadata fetching states + const [metadata, setMetadata] = useState(null); + const [loading, setLoading] = useState(false); + + // Metadata fetching function + const fetchMetadata = async () => { + + setLoading(true); + + try { + const response = await server.get(`/cdn/artefactMetadata/${artID}`); + console.log(response) + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw errObject; + } + + // Success case + setMetadata(response.data.raw.data); + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + if (err.response.data.userErrorType()) { + ToastWizard.standard( + "error", + "We could not load the details", + err.response.data.message || "There was a problem retrieving this item information. Please try again later." + ); + } else { + ToastWizard.standard( + "error", + "Something went wrong", + "We are having trouble loading the information. Please try again in a moment." + ); + } + } else { + ToastWizard.standard( + "error", + "Connection issue", + "We could not connect to the server. Please check your internet connection or try again later." + ); + } + setMetadata(null); + } finally { + setLoading(false); + } + }; + + // Fetch metadata when currentItem changes or modal opens + useEffect(() => { + fetchMetadata(); + }, []); + return ( ArtefactEditor for {colID} {artID} ); From f8a526e1785ec25373daf65ae6950fdc90e87bba Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 12 Aug 2025 14:37:58 +0800 Subject: [PATCH 18/52] added IAMLogoutUserButton for superuser profile management --- .../Identity/IAMLogoutUserButton.jsx | 94 +++++++++++++++++++ src/pages/Profile.jsx | 9 +- 2 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 src/components/Identity/IAMLogoutUserButton.jsx diff --git a/src/components/Identity/IAMLogoutUserButton.jsx b/src/components/Identity/IAMLogoutUserButton.jsx new file mode 100644 index 0000000..1ada280 --- /dev/null +++ b/src/components/Identity/IAMLogoutUserButton.jsx @@ -0,0 +1,94 @@ +import { Button, CloseButton, Dialog, Portal } from '@chakra-ui/react'; +import { useSelector } from 'react-redux' +import ToastWizard from '../toastWizard'; +import { useState } from 'react'; +import server, { JSONResponse } from '../../networking'; + +function IAMLogoutUserButton({ targetProfileID }) { + const { accountID, superuser } = useSelector(state => state.auth); + + const [dialogOpen, setDialogOpen] = useState(false); + const [invalidatingSession, setInvalidatingSession] = useState(false); + + const handleUserSessionInvalidate = async () => { + if (superuser !== true) { + ToastWizard.standard("error", "Superuser privileges required to log out other users.") + return; + } + if (targetProfileID === accountID) { + ToastWizard.standard("error", "Unexpected session invalidation attempt for admin profile", "You cannot invalidate your own session. To log yourself out, please click the avatar at the top-right.") + return; + } + + setInvalidatingSession(true); + + try { + const response = await server.post('/admin/invalidateUserSession', { + userID: targetProfileID + }) + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw new Error(errObject); + } + + // Success case + ToastWizard.standard("success", "User session invalidated", "The user will need to re-login if they had already logged in.") + setDialogOpen(false); + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in user session invalidation request:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard("error", "User session invalidation failed", err.response.data.message); + } else { + ToastWizard.standard("error", "Something went wrong", "Couldn't invalidate the user's login session. Please try again.") + } + } else { + console.log("Unexpected error in user session invalidation request:", err); + ToastWizard.standard("error", "Something went wrong", "Couldn't invalidate the user's login session. Please try again.") + } + } finally { + setInvalidatingSession(false); + } + } + + return ( + setDialogOpen(e.open)}> + + + + + + + + + Logout user? + + + This will clear the user's session, if they have a valid one currently. They will have to re-login. + + + + + + + + + + + + + + + ) +} + +export default IAMLogoutUserButton \ No newline at end of file diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 0eaf22f..c88db1d 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -12,14 +12,14 @@ import AccountActivityCard from '../components/Identity/AccountActivityCard'; import ChangePasswordDialogButton from '../components/Identity/ChangePasswordDialogButton'; import ProfileAvatar from '../components/Identity/ProfileAvatar'; import CentredSpinner from '../components/CentredSpinner'; +import IAMLogoutUserButton from '../components/Identity/IAMLogoutUserButton'; function Profile() { const navigate = useNavigate(); const dispatch = useDispatch(); const { userID } = useParams(); - const location = useLocation(); - - const { username, accountID, superuser, loaded } = useSelector(state => state.auth); + + const { accountID, superuser, loaded } = useSelector(state => state.auth); const [targetProfileID, setTargetProfileID] = useState(null); // If userID is provided, use that, otherwise use the logged-in user's accountID @@ -247,6 +247,9 @@ function Profile() { {targetProfileID === accountID && } + {targetProfileID !== accountID && <> + + } {targetProfileID === accountID && } From ee52930e99d5dc7dc2409f25ece261eac53054f3 Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 12 Aug 2025 15:10:23 +0800 Subject: [PATCH 19/52] Added rough layout for artefact editor --- src/main.jsx | 12 +++---- src/pages/ArtefactEditor.jsx | 67 +++++++++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/src/main.jsx b/src/main.jsx index a73c062..1b3c99e 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -49,18 +49,18 @@ createRoot(document.getElementById('root')).render( } /> - - - } /> - } /> - } /> - {/* Protected Pages */} }> } /> } /> + + + } /> + } /> + } /> + diff --git a/src/pages/ArtefactEditor.jsx b/src/pages/ArtefactEditor.jsx index 7d6c2b5..69579f5 100644 --- a/src/pages/ArtefactEditor.jsx +++ b/src/pages/ArtefactEditor.jsx @@ -1,21 +1,18 @@ -import { Box, Flex } from "@chakra-ui/react"; +import { Box, Flex, Text, Center, Spinner } from "@chakra-ui/react"; import { useParams } from 'react-router-dom'; import { useEffect, useRef, useState } from "react"; +import { useSelector } from "react-redux"; import ToastWizard from "../components/toastWizard"; import server, { JSONResponse } from "../networking"; +import CentredSpinner from "../components/CentredSpinner"; function ArtefactEditor() { - const { colID, artID } = useParams(); - - // Metadata fetching states + const { artID } = useParams(); const [metadata, setMetadata] = useState(null); - const [loading, setLoading] = useState(false); + const { loaded } = useSelector(state => state.auth); // Metadata fetching function const fetchMetadata = async () => { - - setLoading(true); - try { const response = await server.get(`/cdn/artefactMetadata/${artID}`); console.log(response) @@ -41,36 +38,72 @@ function ArtefactEditor() { ToastWizard.standard( "error", "We could not load the details", - err.response.data.message || "There was a problem retrieving this item information. Please try again later." + err.response.data.message || "There was a problem retrieving this item information. Please try again later.", + 3000, + true, + fetchMetadata, + 'Retry' ); } else { ToastWizard.standard( "error", "Something went wrong", - "We are having trouble loading the information. Please try again in a moment." + "We are having trouble loading the information. Please try again in a moment.", + 3000, + true, + fetchMetadata, + 'Retry' ); } } else { ToastWizard.standard( "error", "Connection issue", - "We could not connect to the server. Please check your internet connection or try again later." + "We could not connect to the server. Please check your internet connection or try again later.", + 3000, + true, + fetchMetadata, + 'Retry' ); } setMetadata(null); - } finally { - setLoading(false); } }; // Fetch metadata when currentItem changes or modal opens useEffect(() => { - fetchMetadata(); - }, []); + if (loaded) { + fetchMetadata() + } + }, [loaded]); + + // Show loading spinner while data is being fetched + if (!metadata) { + return + } return ( - ArtefactEditor for {colID} {artID} + <> + + Data Studio + {metadata.name} + + + + + + + + + + + + ); } -export default ArtefactEditor; +export default ArtefactEditor; \ No newline at end of file From 55eed68d5fb839be2f9b38d2717a6de23a7fc8b4 Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 12 Aug 2025 15:13:22 +0800 Subject: [PATCH 20/52] Fixed metadataDisplay to set metadata based on response.data.raw.data.metadata --- src/components/Catalogue/metadataDisplay.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Catalogue/metadataDisplay.jsx b/src/components/Catalogue/metadataDisplay.jsx index 9e1972a..36e2b31 100644 --- a/src/components/Catalogue/metadataDisplay.jsx +++ b/src/components/Catalogue/metadataDisplay.jsx @@ -113,7 +113,7 @@ function MetadataDisplay({ currentItem, isOpen }) { } // Success case - setMetadata(response.data.raw.data); + setMetadata(response.data.raw.data.metadata); } else { throw new Error("Unexpected response format"); } From 47e8eaea583db69d5d29dc7c40cf024378b657b1 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 12 Aug 2025 15:17:49 +0800 Subject: [PATCH 21/52] added IAMResetPasswordButton for superuser profile management --- .../Identity/IAMLogoutUserButton.jsx | 2 +- .../Identity/IAMResetPasswordButton.jsx | 94 +++++++++++++++++++ src/pages/Profile.jsx | 4 +- 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/components/Identity/IAMResetPasswordButton.jsx diff --git a/src/components/Identity/IAMLogoutUserButton.jsx b/src/components/Identity/IAMLogoutUserButton.jsx index 1ada280..c273c0c 100644 --- a/src/components/Identity/IAMLogoutUserButton.jsx +++ b/src/components/Identity/IAMLogoutUserButton.jsx @@ -61,7 +61,7 @@ function IAMLogoutUserButton({ targetProfileID }) { } return ( - setDialogOpen(e.open)}> + setDialogOpen(e.open)} placement={'center'}> diff --git a/src/components/Identity/IAMResetPasswordButton.jsx b/src/components/Identity/IAMResetPasswordButton.jsx new file mode 100644 index 0000000..81c51f2 --- /dev/null +++ b/src/components/Identity/IAMResetPasswordButton.jsx @@ -0,0 +1,94 @@ +import { Button, CloseButton, Dialog, Portal } from '@chakra-ui/react'; +import { useSelector } from 'react-redux' +import ToastWizard from '../toastWizard'; +import { useState } from 'react'; +import server, { JSONResponse } from '../../networking'; + +function IAMResetPasswordButton({ targetProfileID }) { + const { accountID, superuser } = useSelector(state => state.auth); + + const [dialogOpen, setDialogOpen] = useState(false); + const [resettingPassword, setResettingPassword] = useState(false); + + const handleUserPasswordReset = async () => { + if (superuser !== true) { + ToastWizard.standard("error", "Superuser privileges required to reset a user's password.") + return; + } + if (targetProfileID === accountID) { + ToastWizard.standard("error", "Unexpected password reset attempt for admin profile", "You cannot reset your own password. To change your password, please click the avatar at the top-right.") + return; + } + + setResettingPassword(true); + + try { + const response = await server.post('/admin/resetPassword', { + userID: targetProfileID + }) + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw new Error(errObject); + } + + // Success case + ToastWizard.standard("success", "Password reset successfully", "New login instructions have been sent to the user.") + setDialogOpen(false); + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in user password reset request:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard("error", "Password reset failed", err.response.data.message); + } else { + ToastWizard.standard("error", "Something went wrong", "Couldn't reset the user's password. Please try again.") + } + } else { + console.log("Unexpected error in user password reset request:", err); + ToastWizard.standard("error", "Something went wrong", "Couldn't reset the user's password. Please try again.") + } + } finally { + setResettingPassword(false); + } + } + + return ( + setDialogOpen(e.open)} placement={'center'}> + + + + + + + + + Reset user's password? + + + This will change the user's password to an auto-generated one and log them out. They will receive an email with new login instructions. + + + + + + + + + + + + + + + ) +} + +export default IAMResetPasswordButton \ No newline at end of file diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index c88db1d..0485f7b 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -13,6 +13,7 @@ import ChangePasswordDialogButton from '../components/Identity/ChangePasswordDia import ProfileAvatar from '../components/Identity/ProfileAvatar'; import CentredSpinner from '../components/CentredSpinner'; import IAMLogoutUserButton from '../components/Identity/IAMLogoutUserButton'; +import IAMResetPasswordButton from '../components/Identity/IAMResetPasswordButton'; function Profile() { const navigate = useNavigate(); @@ -245,10 +246,11 @@ function Profile() { - + {targetProfileID === accountID && } {targetProfileID !== accountID && <> + } {targetProfileID === accountID && } From 8dba01ef1c62dc176ff257af885fdc19a0695268 Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 12 Aug 2025 16:43:59 +0800 Subject: [PATCH 22/52] Added back arrow and chevrons for artefact editor --- src/pages/ArtefactEditor.jsx | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/pages/ArtefactEditor.jsx b/src/pages/ArtefactEditor.jsx index 69579f5..f52f1e7 100644 --- a/src/pages/ArtefactEditor.jsx +++ b/src/pages/ArtefactEditor.jsx @@ -5,6 +5,8 @@ import { useSelector } from "react-redux"; import ToastWizard from "../components/toastWizard"; import server, { JSONResponse } from "../networking"; import CentredSpinner from "../components/CentredSpinner"; +import { FaArrowAltCircleLeft, FaChevronLeft, FaChevronRight } from "react-icons/fa"; +import ArrowOverlay from "../components/Catalogue/arrowOverlay"; function ArtefactEditor() { const { artID } = useParams(); @@ -83,26 +85,33 @@ function ArtefactEditor() { } return ( - <> - - Data Studio - {metadata.name} - - - - + + + + + Data Studio + {metadata.name} + + + + + + + + + + + + + - + ); } From 89141f98204827ceab2715eedb048750ec5ee9b8 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 12 Aug 2025 16:49:01 +0800 Subject: [PATCH 23/52] added IAMDeleteAccountButton for superuser profile management --- .../Identity/IAMDeleteAccountButton.jsx | 109 ++++++++++++++++++ src/pages/Profile.jsx | 28 +++-- 2 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 src/components/Identity/IAMDeleteAccountButton.jsx diff --git a/src/components/Identity/IAMDeleteAccountButton.jsx b/src/components/Identity/IAMDeleteAccountButton.jsx new file mode 100644 index 0000000..fb356dd --- /dev/null +++ b/src/components/Identity/IAMDeleteAccountButton.jsx @@ -0,0 +1,109 @@ +import { Button, CloseButton, Dialog, Field, Input, Portal, Text } from '@chakra-ui/react'; +import { useSelector } from 'react-redux' +import ToastWizard from '../toastWizard'; +import { useState } from 'react'; +import server, { JSONResponse } from '../../networking'; +import { LuTrash2 } from 'react-icons/lu'; +import { useNavigate } from 'react-router-dom'; + +function IAMDeleteAccountButton({ targetUsername, targetProfileID }) { + const navigate = useNavigate(); + const { accountID, superuser } = useSelector(state => state.auth); + + const [dialogOpen, setDialogOpen] = useState(false); + const [deletingAccount, setDeletingAccount] = useState(false); + const [confirmUsername, setConfirmUsername] = useState(''); + + const handleUserAccountDelete = async () => { + if (superuser !== true) { + ToastWizard.standard("error", "Superuser privileges required to delete a user's account.") + return; + } + if (targetProfileID === accountID) { + ToastWizard.standard("error", "Unexpected delete attempt for admin profile", "You cannot delete your own account.") + return; + } + + setDeletingAccount(true); + + try { + const response = await server.post('/admin/deleteUser', { + userID: targetProfileID + }) + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw new Error(errObject); + } + + // Success case + ToastWizard.standard("success", "Account deleted", "The user and their data has been successfully deleted.") + setDialogOpen(false) + navigate('/admin/iam'); + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in account deletion request:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard("error", "Account deletion failed", err.response.data.message); + } else { + ToastWizard.standard("error", "Something went wrong", "Couldn't delete the user's account. Please try again.") + } + } else { + console.log("Unexpected error in user account deletion request:", err); + ToastWizard.standard("error", "Something went wrong", "Couldn't delete the user's account. Please try again.") + } + } finally { + setDeletingAccount(false); + } + } + + return ( + setDialogOpen(e.open)} placement={'center'}> + + + + + + + + + Delete user account? + + + This is a destructive action that cannot be undone. + The user's account and all associated data will be permanently deleted. + Please re-enter the user's username to proceed with deletion: + + Username + setConfirmUsername(e.target.value)} /> + Type in: {targetUsername} + + + + + + + + + + + + + + + + ) +} + +export default IAMDeleteAccountButton \ No newline at end of file diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 0485f7b..319b83b 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -14,12 +14,13 @@ import ProfileAvatar from '../components/Identity/ProfileAvatar'; import CentredSpinner from '../components/CentredSpinner'; import IAMLogoutUserButton from '../components/Identity/IAMLogoutUserButton'; import IAMResetPasswordButton from '../components/Identity/IAMResetPasswordButton'; +import IAMDeleteAccountButton from '../components/Identity/IAMDeleteAccountButton'; function Profile() { const navigate = useNavigate(); const dispatch = useDispatch(); const { userID } = useParams(); - + const { accountID, superuser, loaded } = useSelector(state => state.auth); const [targetProfileID, setTargetProfileID] = useState(null); // If userID is provided, use that, otherwise use the logged-in user's accountID @@ -168,7 +169,7 @@ function Profile() { p={isSmallerThan800 ? '10px 0px 30px 0px' : '20px'} mt={'10px'} > - + {targetProfileID !== accountID && @@ -246,15 +247,20 @@ function Profile() { - - {targetProfileID === accountID && } - {targetProfileID !== accountID && <> - - - } - - {targetProfileID === accountID && } - + {!infoLoading && + + {targetProfileID === accountID ? <> + + + + : <> + + + + + } + + }
From 77ce0191e978f20362c61a259ca65a38a0fd0b55 Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 12 Aug 2025 17:03:52 +0800 Subject: [PATCH 24/52] fixed responsiveness issue --- src/components/Identity/IAMDeleteAccountButton.jsx | 2 +- src/pages/Profile.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Identity/IAMDeleteAccountButton.jsx b/src/components/Identity/IAMDeleteAccountButton.jsx index fb356dd..5c01bbe 100644 --- a/src/components/Identity/IAMDeleteAccountButton.jsx +++ b/src/components/Identity/IAMDeleteAccountButton.jsx @@ -94,7 +94,7 @@ function IAMDeleteAccountButton({ targetUsername, targetProfileID }) { - + diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 319b83b..448454b 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -248,7 +248,7 @@ function Profile() { {!infoLoading && - + {targetProfileID === accountID ? <> From a09a706bf58a4f44caa13ff5c836f3b4848f7afc Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 12 Aug 2025 17:05:11 +0800 Subject: [PATCH 25/52] Added image rendering for AE --- src/pages/ArtefactEditor.jsx | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/pages/ArtefactEditor.jsx b/src/pages/ArtefactEditor.jsx index f52f1e7..0bff8d8 100644 --- a/src/pages/ArtefactEditor.jsx +++ b/src/pages/ArtefactEditor.jsx @@ -1,4 +1,4 @@ -import { Box, Flex, Text, Center, Spinner } from "@chakra-ui/react"; +import { Box, Flex, Text, Image } from "@chakra-ui/react"; import { useParams } from 'react-router-dom'; import { useEffect, useRef, useState } from "react"; import { useSelector } from "react-redux"; @@ -6,7 +6,6 @@ import ToastWizard from "../components/toastWizard"; import server, { JSONResponse } from "../networking"; import CentredSpinner from "../components/CentredSpinner"; import { FaArrowAltCircleLeft, FaChevronLeft, FaChevronRight } from "react-icons/fa"; -import ArrowOverlay from "../components/Catalogue/arrowOverlay"; function ArtefactEditor() { const { artID } = useParams(); @@ -84,8 +83,11 @@ function ArtefactEditor() { return } + console.log(metadata) + console.log(`${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${artID}`) + return ( - + @@ -94,20 +96,26 @@ function ArtefactEditor() { - - + + - - + + - - + - + From 7c5f36c8b1cace067c4467d5ed92fa90f51f4977 Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 12 Aug 2025 17:09:43 +0800 Subject: [PATCH 26/52] Fixed sizing and layout in AE --- src/pages/ArtefactEditor.jsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/ArtefactEditor.jsx b/src/pages/ArtefactEditor.jsx index 0bff8d8..e0a0060 100644 --- a/src/pages/ArtefactEditor.jsx +++ b/src/pages/ArtefactEditor.jsx @@ -92,30 +92,30 @@ function ArtefactEditor() { Data Studio - {metadata.name} + {metadata.name} - - + + - + - + - + From f98a9e23d648af7ee2e83c888e1e043cbb617bfb Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 12 Aug 2025 17:12:32 +0800 Subject: [PATCH 27/52] fixed small responsiveness issues --- src/pages/Profile.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 448454b..28248b5 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -248,11 +248,11 @@ function Profile() { {!infoLoading && - + {targetProfileID === accountID ? <> - + : <> From 512c63ef9993e8b9dc3be359ca929660400d891a Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 12 Aug 2025 17:29:23 +0800 Subject: [PATCH 28/52] added editorCard --- src/components/DataStudio/editorCard.jsx | 11 +++++++++++ src/pages/ArtefactEditor.jsx | 16 +++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 src/components/DataStudio/editorCard.jsx diff --git a/src/components/DataStudio/editorCard.jsx b/src/components/DataStudio/editorCard.jsx new file mode 100644 index 0000000..b883e74 --- /dev/null +++ b/src/components/DataStudio/editorCard.jsx @@ -0,0 +1,11 @@ +import { Card } from "@chakra-ui/react" + +function EditorCard() { + return ( + + + + ) +} + +export default EditorCard \ No newline at end of file diff --git a/src/pages/ArtefactEditor.jsx b/src/pages/ArtefactEditor.jsx index e0a0060..9017727 100644 --- a/src/pages/ArtefactEditor.jsx +++ b/src/pages/ArtefactEditor.jsx @@ -6,6 +6,7 @@ import ToastWizard from "../components/toastWizard"; import server, { JSONResponse } from "../networking"; import CentredSpinner from "../components/CentredSpinner"; import { FaArrowAltCircleLeft, FaChevronLeft, FaChevronRight } from "react-icons/fa"; +import EditorCard from "../components/DataStudio/editorCard"; function ArtefactEditor() { const { artID } = useParams(); @@ -83,9 +84,6 @@ function ArtefactEditor() { return } - console.log(metadata) - console.log(`${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${artID}`) - return ( @@ -97,25 +95,25 @@ function ArtefactEditor() { - + - + - + + - + From 7d8474aed24b2bcf15520b074df0f5da55bc44ea Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 12 Aug 2025 17:36:13 +0800 Subject: [PATCH 29/52] tightened security with new SuperuserLayout --- src/SuperuserLayout.jsx | 20 ++++++++++++++++++++ src/main.jsx | 3 ++- src/pages/UserManagement.jsx | 6 +++--- 3 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 src/SuperuserLayout.jsx diff --git a/src/SuperuserLayout.jsx b/src/SuperuserLayout.jsx new file mode 100644 index 0000000..0225ec1 --- /dev/null +++ b/src/SuperuserLayout.jsx @@ -0,0 +1,20 @@ +import React, { useEffect } from 'react' +import { useSelector } from 'react-redux' +import { Outlet, useNavigate } from 'react-router-dom'; + +function SuperuserLayout() { + const navigate = useNavigate(); + const { loaded, superuser } = useSelector(state => state.auth); + + useEffect(() => { + if (loaded && superuser !== true) { + console.log('Unauthorised access to superuser privileged resource; redirecting...') + navigate('/'); + return; + } + }, [loaded, superuser]); + + return +} + +export default SuperuserLayout \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index 6c3bda3..d0a4220 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -26,6 +26,7 @@ import GroupView from './pages/GroupView.jsx'; import Profile from './pages/Profile.jsx'; import AdminConsole from './pages/AdminConsole.jsx'; import UserManagement from './pages/UserManagement.jsx'; +import SuperuserLayout from './SuperuserLayout.jsx'; const store = configureStore({ reducer: { @@ -64,7 +65,7 @@ createRoot(document.getElementById('root')).render( }> } /> } /> - + }> } /> } /> diff --git a/src/pages/UserManagement.jsx b/src/pages/UserManagement.jsx index 7a97abc..d6820af 100644 --- a/src/pages/UserManagement.jsx +++ b/src/pages/UserManagement.jsx @@ -11,7 +11,7 @@ import { useEffect } from 'react' function UserManagement() { const navigate = useNavigate(); - const { loaded } = useSelector(state => state.auth); + const { loaded, superuser } = useSelector(state => state.auth); const [userData, setUserData] = useState({}); const [retrievingUsers, setRetrievingUsers] = useState(true); @@ -59,10 +59,10 @@ function UserManagement() { } useEffect(() => { - if (loaded) { + if (loaded && superuser === true) { retrieveUserData(); } - }, [loaded]); + }, [loaded, superuser]); return ( From f046b57bd72381be68d2ebe686f6d4521c7c6f8f Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 12 Aug 2025 18:00:16 +0800 Subject: [PATCH 30/52] Added title and button for editCard --- src/components/DataStudio/editorCard.jsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/DataStudio/editorCard.jsx b/src/components/DataStudio/editorCard.jsx index b883e74..a17d0a8 100644 --- a/src/components/DataStudio/editorCard.jsx +++ b/src/components/DataStudio/editorCard.jsx @@ -1,9 +1,21 @@ -import { Card } from "@chakra-ui/react" +import { Card, Button, Flex } from "@chakra-ui/react" +import { IoInformationCircleOutline } from "react-icons/io5" function EditorCard() { return ( - + + + + Artefact Details + + + + + + ) } From 8b2d04014992adf9a5d4907469bd624fe9f9818d Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 12 Aug 2025 18:01:58 +0800 Subject: [PATCH 31/52] Shifted addInfo in metadatadisplay to be before the figure headshot --- src/components/Catalogue/metadataDisplay.jsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/Catalogue/metadataDisplay.jsx b/src/components/Catalogue/metadataDisplay.jsx index 36e2b31..5ab3368 100644 --- a/src/components/Catalogue/metadataDisplay.jsx +++ b/src/components/Catalogue/metadataDisplay.jsx @@ -245,6 +245,14 @@ function MetadataDisplay({ currentItem, isOpen }) { Caption: {metadata.caption || "N/A"} + {/* Additional figure metadata, if present */} + {metadata.addInfo && ( + <> + Additional Info: + {metadata.addInfo} + + )} + {/* Figure headshots (if available) */} {Array.isArray(metadata.figureIDs) && metadata.figureIDs.length > 0 ? ( @@ -277,14 +285,6 @@ function MetadataDisplay({ currentItem, isOpen }) { ) : ( N/A )} - - {/* Additional figure metadata, if present */} - {metadata.addInfo && ( - <> - Additional Info: - {metadata.addInfo} - - )} )} From 84912b863d9cbc3776297073569570a8d3be537d Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 12 Aug 2025 18:02:23 +0800 Subject: [PATCH 32/52] minor fixes --- src/components/Identity/ChangePasswordDialogButton.jsx | 4 ++-- src/pages/Profile.jsx | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/Identity/ChangePasswordDialogButton.jsx b/src/components/Identity/ChangePasswordDialogButton.jsx index 94a8379..8194390 100644 --- a/src/components/Identity/ChangePasswordDialogButton.jsx +++ b/src/components/Identity/ChangePasswordDialogButton.jsx @@ -127,9 +127,9 @@ function ChangePasswordDialogButton() { - {/* + - */} + diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 28248b5..10a6d77 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -11,7 +11,6 @@ import ProfileActionBar from '../components/Identity/ProfileActionBar'; import AccountActivityCard from '../components/Identity/AccountActivityCard'; import ChangePasswordDialogButton from '../components/Identity/ChangePasswordDialogButton'; import ProfileAvatar from '../components/Identity/ProfileAvatar'; -import CentredSpinner from '../components/CentredSpinner'; import IAMLogoutUserButton from '../components/Identity/IAMLogoutUserButton'; import IAMResetPasswordButton from '../components/Identity/IAMResetPasswordButton'; import IAMDeleteAccountButton from '../components/Identity/IAMDeleteAccountButton'; @@ -23,7 +22,7 @@ function Profile() { const { accountID, superuser, loaded } = useSelector(state => state.auth); - const [targetProfileID, setTargetProfileID] = useState(null); // If userID is provided, use that, otherwise use the logged-in user's accountID + const [targetProfileID, setTargetProfileID] = useState(null); // Loading states const [loggingOut, setLoggingOut] = useState(false); From 8d09a81167d4cc2d3fb78d035c1d665667ed757b Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 12 Aug 2025 18:03:59 +0800 Subject: [PATCH 33/52] changed close dialog for change pwd to Cancel --- src/components/Identity/ChangePasswordDialogButton.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Identity/ChangePasswordDialogButton.jsx b/src/components/Identity/ChangePasswordDialogButton.jsx index 8194390..a7770c7 100644 --- a/src/components/Identity/ChangePasswordDialogButton.jsx +++ b/src/components/Identity/ChangePasswordDialogButton.jsx @@ -128,7 +128,7 @@ function ChangePasswordDialogButton() { - + From 795ca27397942572a1cea523fc3f5191702696bf Mon Sep 17 00:00:00 2001 From: Prakhar Trivedi Date: Tue, 12 Aug 2025 18:48:15 +0800 Subject: [PATCH 34/52] completed create user dialog --- src/components/Identity/CreateUserDialog.jsx | 149 +++++++++++++++++++ src/pages/AdminConsole.jsx | 14 +- src/pages/Profile.jsx | 2 +- src/pages/UserManagement.jsx | 8 +- 4 files changed, 154 insertions(+), 19 deletions(-) create mode 100644 src/components/Identity/CreateUserDialog.jsx diff --git a/src/components/Identity/CreateUserDialog.jsx b/src/components/Identity/CreateUserDialog.jsx new file mode 100644 index 0000000..699e46d --- /dev/null +++ b/src/components/Identity/CreateUserDialog.jsx @@ -0,0 +1,149 @@ +import { Button, CloseButton, Dialog, Field, IconButton, Input, Portal, Text, VStack } from '@chakra-ui/react'; +import { useState, useEffect } from 'react'; +import { FaPlus } from 'react-icons/fa'; +import { useNavigate } from 'react-router-dom'; +import { withMask } from 'use-mask-input'; +import ToastWizard from '../toastWizard'; +import server, { JSONResponse } from '../../networking'; + +function CreateUserDialog() { + const navigate = useNavigate(); + + const [dialogOpen, setDialogOpen] = useState(false); + const [fname, setFname] = useState(''); + const [lname, setLname] = useState(''); + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [contact, setContact] = useState(''); + const [role, setRole] = useState(''); + const [creatingUser, setCreatingUser] = useState(false); + + const isDisabled = !fname || !lname || !username || !email || !contact || !role; + + const handleCreateUser = async () => { + if (isDisabled) { + ToastWizard.standard("error", "All fields are required."); + return; + } + + setCreatingUser(true); + + try { + const response = await server.post('/admin/createUser', { + fname, + lname, + username, + email, + contact, + role + }) + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw new Error(errObject); + } + + // Success case + ToastWizard.standard("success", "Account Created", `Welcome ${fname} to the family! Login instructions have been sent via email!`); + setDialogOpen(false); + if (response.data.raw.newUserID) { + navigate(`/profile/${response.data.raw.newUserID}`); + } + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in user creation request:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard("error", "User creation failed.", err.response.data.message); + } else { + ToastWizard.standard("error", "Something went wrong", "Couldn't create user. Please try again.") + } + } else { + console.log("Unexpected error in user creation request:", err); + ToastWizard.standard("error", "Something went wrong", "Couldn't create user. Please try again.") + } + } finally { + setCreatingUser(false); + } + } + + const handleEnterKey = (e) => { + if (e.key === 'Enter') { + handleCreateUser(); + } + } + + return ( + setDialogOpen(e.open)} unmountOnExit> + + + + + + + + + Create New Account + + + + Create new accounts to enable more people to gain access to ArchAIve's innovative artefact digitisation services! + Users will be able to access most of the platform's features, except those that are sensitive like managing other users. + Every user's activity is monitored closely, empowering you to redress in any dire situations at all times. + + + + First Name + setFname(e.target.value)} /> + + + + Last Name + setLname(e.target.value)} /> + + + + Username + setUsername(e.target.value)} /> + + + + Email + setEmail(e.target.value)} /> + + + + Contact + setContact(e.target.value)} /> + + + + Role + setRole(e.target.value)} /> + + + + + + + + + + + + + + + + + ) +} + +export default CreateUserDialog \ No newline at end of file diff --git a/src/pages/AdminConsole.jsx b/src/pages/AdminConsole.jsx index 5407d89..9769402 100644 --- a/src/pages/AdminConsole.jsx +++ b/src/pages/AdminConsole.jsx @@ -12,7 +12,7 @@ function AdminConsole() { return ( Admin Console - + User Management @@ -25,18 +25,6 @@ function AdminConsole() { - - - Data Management - - Analyse and manage batches, collections, artefacts and other data that goes into the platform. - - - - - - - Platform Controls diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 10a6d77..3091463 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -241,7 +241,7 @@ function Profile() { Security & Authentication - Last Login: {originalAccountInfo.lastLogin} + Last Login: {originalAccountInfo.lastLogin || 'Never'} diff --git a/src/pages/UserManagement.jsx b/src/pages/UserManagement.jsx index d6820af..75bc85c 100644 --- a/src/pages/UserManagement.jsx +++ b/src/pages/UserManagement.jsx @@ -1,16 +1,14 @@ -import { Box, Button, IconButton, SimpleGrid, Skeleton, Spinner, Text } from '@chakra-ui/react' -import { FaPlus } from 'react-icons/fa' +import { Box, SimpleGrid, Skeleton, Text } from '@chakra-ui/react' import IAMStatisticBox from '../components/Identity/IAMStatisticBox' import IAMUserCard from '../components/Identity/IAMUserCard' -import { useNavigate } from 'react-router-dom' import { useSelector } from 'react-redux' import { useState } from 'react' import server, { JSONResponse } from '../networking'; import ToastWizard from '../components/toastWizard'; import { useEffect } from 'react' +import CreateUserDialog from '../components/Identity/CreateUserDialog' function UserManagement() { - const navigate = useNavigate(); const { loaded, superuser } = useSelector(state => state.auth); const [userData, setUserData] = useState({}); @@ -68,7 +66,7 @@ function UserManagement() { User Management - + From aa43d21cb0229438bb5fc5e1e7d426b787ab0d7e Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 12 Aug 2025 18:53:06 +0800 Subject: [PATCH 35/52] Added metadata toggle --- .../Catalogue/transcriptionToggle.jsx | 2 +- src/components/DataStudio/editorCard.jsx | 15 ++++- src/components/DataStudio/metadataToggle.jsx | 59 +++++++++++++++++++ src/pages/ArtefactEditor.jsx | 2 +- 4 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 src/components/DataStudio/metadataToggle.jsx diff --git a/src/components/Catalogue/transcriptionToggle.jsx b/src/components/Catalogue/transcriptionToggle.jsx index adec14b..b1ec21d 100644 --- a/src/components/Catalogue/transcriptionToggle.jsx +++ b/src/components/Catalogue/transcriptionToggle.jsx @@ -57,7 +57,7 @@ const TranscriptionToggle = ({ value, onChange }) => { p="1" rounded="lg" boxShadow="sm" - gap="00" + gap="0" > diff --git a/src/components/DataStudio/editorCard.jsx b/src/components/DataStudio/editorCard.jsx index a17d0a8..ccaf539 100644 --- a/src/components/DataStudio/editorCard.jsx +++ b/src/components/DataStudio/editorCard.jsx @@ -1,7 +1,11 @@ -import { Card, Button, Flex } from "@chakra-ui/react" +import { Card, Button, Flex, Text } from "@chakra-ui/react" +import { useState } from "react"; import { IoInformationCircleOutline } from "react-icons/io5" +import MetadataToggle from "./metadataToggle"; + +function EditorCard(metadata) { + const [selectedTranscription, setSelectedTranscription] = useState("tradCN"); -function EditorCard() { return ( @@ -14,7 +18,12 @@ function EditorCard() { - + + + ) diff --git a/src/components/DataStudio/metadataToggle.jsx b/src/components/DataStudio/metadataToggle.jsx new file mode 100644 index 0000000..dc7927b --- /dev/null +++ b/src/components/DataStudio/metadataToggle.jsx @@ -0,0 +1,59 @@ +import { SegmentGroup, Text } from "@chakra-ui/react"; + +const MetadataToggle = ({ value, onChange }) => { + const items = [ + { value: "tradCN", label: "Trad CN" }, + { value: "simplifiedCN", label: "Simp CN" }, + { value: "english", label: "ENG" }, + { value: "summary", label: "SUM" }, + ]; + + return ( + onChange(value)} + size="sm" + bg="gray.100" + p="1" + rounded="lg" + boxShadow="sm" + w="100%" + display="flex" + position="relative" + > + + ({ + value: item.value, + label: ( + + {item.label} + + ), + style: { + flex: 1, + minWidth: 0, + position: "relative", + zIndex: 1, + display: "flex", + justifyContent: "center", + alignItems: "center", + }, + }))} + /> + + ); +}; + +export default MetadataToggle; \ No newline at end of file diff --git a/src/pages/ArtefactEditor.jsx b/src/pages/ArtefactEditor.jsx index 9017727..6af0a37 100644 --- a/src/pages/ArtefactEditor.jsx +++ b/src/pages/ArtefactEditor.jsx @@ -110,7 +110,7 @@ function ArtefactEditor() { - + From fd57f372fb22c73f237c436edbe8ee658c8ca70f Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 12 Aug 2025 22:53:03 +0800 Subject: [PATCH 36/52] Added rendering of name and mm transcription using text area --- src/components/DataStudio/editorCard.jsx | 26 ++++++++++++++++++++---- src/pages/ArtefactEditor.jsx | 1 - 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/components/DataStudio/editorCard.jsx b/src/components/DataStudio/editorCard.jsx index ccaf539..4ceabb9 100644 --- a/src/components/DataStudio/editorCard.jsx +++ b/src/components/DataStudio/editorCard.jsx @@ -1,9 +1,9 @@ -import { Card, Button, Flex, Text } from "@chakra-ui/react" +import { Card, Button, Flex, Text, Field, Input, Textarea } from "@chakra-ui/react" import { useState } from "react"; import { IoInformationCircleOutline } from "react-icons/io5" import MetadataToggle from "./metadataToggle"; -function EditorCard(metadata) { +function EditorCard({ metadata }) { const [selectedTranscription, setSelectedTranscription] = useState("tradCN"); return ( @@ -17,13 +17,31 @@ function EditorCard(metadata) { Manage Group - - + + + Name: + + + + + + + Transcription: + + +