From 703812d3df39fccc10182b5aa8551a9bc252fe15 Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 14 May 2025 18:49:49 +0800 Subject: [PATCH] Add user management features. --- frontend/src/components/button.tsx | 14 + frontend/src/components/navigator.tsx | 74 ++++-- frontend/src/network/network.ts | 15 ++ frontend/src/pages/manage_me_page.tsx | 296 +++++++++++++++++++++ frontend/src/pages/manage_page.tsx | 28 +- frontend/src/pages/manage_storage_page.tsx | 12 + frontend/src/pages/manage_user_page.tsx | 10 + frontend/src/pages/user_page.tsx | 2 +- server/api/user.go | 21 ++ server/service/user.go | 15 ++ 10 files changed, 447 insertions(+), 40 deletions(-) create mode 100644 frontend/src/components/button.tsx create mode 100644 frontend/src/pages/manage_me_page.tsx diff --git a/frontend/src/components/button.tsx b/frontend/src/components/button.tsx new file mode 100644 index 0000000..abc43c6 --- /dev/null +++ b/frontend/src/components/button.tsx @@ -0,0 +1,14 @@ +import { ReactNode } from "react"; + +export default function Button({ children, onClick, className, disabled, isLoading }: { children: ReactNode, onClick?: () => void, className?: string, disabled?: boolean, isLoading?: boolean }) { + return ; +} \ No newline at end of file diff --git a/frontend/src/components/navigator.tsx b/frontend/src/components/navigator.tsx index d8e9138..773fd39 100644 --- a/frontend/src/components/navigator.tsx +++ b/frontend/src/components/navigator.tsx @@ -1,19 +1,27 @@ -import {app} from "../app.ts"; -import {network} from "../network/network.ts"; -import {useNavigate, useOutlet} from "react-router"; -import {useEffect, useState} from "react"; -import {MdOutlinePerson, MdSearch, MdSettings} from "react-icons/md"; +import { app } from "../app.ts"; +import { network } from "../network/network.ts"; +import { useNavigate, useOutlet } from "react-router"; +import { createContext, useContext, useEffect, useState } from "react"; +import { MdOutlinePerson, MdSearch, MdSettings } from "react-icons/md"; import { useTranslation } from "react-i18next"; import UploadingSideBar from "./uploading_side_bar.tsx"; -import {IoLogoGithub} from "react-icons/io"; +import { IoLogoGithub } from "react-icons/io"; export default function Navigator() { const outlet = useOutlet() const navigate = useNavigate() + const [key, setKey] = useState(0); + + const [naviContext, _] = useState({ + refresh: () => { + setKey(key + 1); + }, + }); + return <> -
+
- - + + { app.isAdmin() && } { - app.isLoggedIn() ? :
-
- {outlet} -
+ +
+ {outlet} +
+
} +interface NavigatorContext { + refresh: () => void; +} + +const navigatorContext = createContext({ + refresh: () => { + // do nothing + } +}) + +export function useNavigator() { + return useContext(navigatorContext); +} + function UserButton() { let avatar = "./avatar.png"; if (app.user) { @@ -59,7 +83,7 @@ function UserButton() { const navigate = useNavigate() - const {t} = useTranslation() + const { t } = useTranslation() return <>
@@ -67,7 +91,7 @@ function UserButton() {
Avatar + src={avatar} />
    {t('Confirm')} @@ -118,7 +142,7 @@ function SearchBar() { const [search, setSearch] = useState(""); - const {t} = useTranslation(); + const { t } = useTranslation(); useEffect(() => { const handleResize = () => { @@ -141,10 +165,10 @@ function SearchBar() { return; } const replace = window.location.pathname === "/search"; - navigate(`/search?keyword=${search}`, {replace: replace}); + navigate(`/search?keyword=${search}`, { replace: replace }); } - const searchField =
diff --git a/frontend/src/pages/manage_storage_page.tsx b/frontend/src/pages/manage_storage_page.tsx index 002d798..1349ebb 100644 --- a/frontend/src/pages/manage_storage_page.tsx +++ b/frontend/src/pages/manage_storage_page.tsx @@ -6,6 +6,7 @@ import Loading from "../components/loading.tsx"; import {MdAdd, MdDelete} from "react-icons/md"; import {ErrorAlert} from "../components/alert.tsx"; import { useTranslation } from "react-i18next"; +import { app } from "../app.ts"; export default function StorageView() { const { t } = useTranslation(); @@ -13,6 +14,9 @@ export default function StorageView() { const [loadingId, setLoadingId] = useState(null); useEffect(() => { + if (app.user == null || !app.user.is_admin) { + return; + } network.listStorages().then((response) => { if (response.success) { setStorages(response.data!); @@ -25,6 +29,14 @@ export default function StorageView() { }) }, []); + if (!app.user) { + return + } + + if (!app.user?.is_admin) { + return + } + if (storages == null) { return } diff --git a/frontend/src/pages/manage_user_page.tsx b/frontend/src/pages/manage_user_page.tsx index fe69046..995aa3e 100644 --- a/frontend/src/pages/manage_user_page.tsx +++ b/frontend/src/pages/manage_user_page.tsx @@ -7,6 +7,8 @@ import { MdMoreHoriz, MdSearch } from "react-icons/md"; import Pagination from "../components/pagination"; import showPopup, { PopupMenuItem } from "../components/popup"; import { useTranslation } from "react-i18next"; +import { app } from "../app"; +import { ErrorAlert } from "../components/alert"; export default function UserView() { const { t } = useTranslation(); @@ -16,6 +18,14 @@ export default function UserView() { const [totalPages, setTotalPages] = useState(0); + if (!app.user) { + return + } + + if (!app.user?.is_admin) { + return + } + return <>
{ diff --git a/frontend/src/pages/user_page.tsx b/frontend/src/pages/user_page.tsx index 0243f94..4e87dce 100644 --- a/frontend/src/pages/user_page.tsx +++ b/frontend/src/pages/user_page.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from "react"; import ResourcesView from "../components/resources_view"; import Loading from "../components/loading"; import Pagination from "../components/pagination"; -import { MdOutlineArrowForward, MdOutlineArrowRight } from "react-icons/md"; +import { MdOutlineArrowRight } from "react-icons/md"; export default function UserPage() { const [user, setUser] = useState(null); diff --git a/server/api/user.go b/server/api/user.go index a79e4c1..d7b6ca2 100644 --- a/server/api/user.go +++ b/server/api/user.go @@ -277,6 +277,26 @@ func handleGetUserInfo(c fiber.Ctx) error { }) } +func handleChangeUsername(c fiber.Ctx) error { + uid, ok := c.Locals("uid").(uint) + if !ok { + return model.NewUnAuthorizedError("Unauthorized") + } + newUsername := c.FormValue("username") + if newUsername == "" { + return model.NewRequestError("Username is required") + } + user, err := service.ChangeUsername(uid, newUsername) + if err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(model.Response[model.UserView]{ + Success: true, + Data: user, + Message: "Username changed successfully", + }) +} + func AddUserRoutes(r fiber.Router) { u := r.Group("user") u.Post("/register", handleUserRegister) @@ -290,4 +310,5 @@ func AddUserRoutes(r fiber.Router) { u.Get("/search", handleSearchUsers) u.Post("/delete", handleDeleteUser) u.Get("/info", handleGetUserInfo) + u.Post("/username", handleChangeUsername) } diff --git a/server/service/user.go b/server/service/user.go index 798d971..3033aa9 100644 --- a/server/service/user.go +++ b/server/service/user.go @@ -264,3 +264,18 @@ func GetUserByUsername(username string) (model.UserView, error) { } return user.ToView(), nil } + +func ChangeUsername(uid uint, newUsername string) (model.UserView, error) { + if len(newUsername) < 3 || len(newUsername) > 20 { + return model.UserView{}, model.NewRequestError("Username must be between 3 and 20 characters") + } + user, err := dao.GetUserByID(uid) + if err != nil { + return model.UserView{}, err + } + user.Username = newUsername + if err := dao.UpdateUser(user); err != nil { + return model.UserView{}, err + } + return user.ToView(), nil +}