From 35785a1a288bb57d4a96abb36ccb1437bedf84e6 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 20 Nov 2023 23:14:16 +0800 Subject: [PATCH] chore(frontend): update user module --- .../web/src/components/CreateUserDialog.tsx | 14 ++- frontend/web/src/components/Header.tsx | 4 +- .../components/ShortcutActionsDropdown.tsx | 3 +- .../src/components/setting/AccountSection.tsx | 3 +- .../src/components/setting/MemberSection.tsx | 1 + frontend/web/src/helpers/api.ts | 25 ----- frontend/web/src/pages/ShortcutDetail.tsx | 3 +- frontend/web/src/pages/SignIn.tsx | 18 +--- frontend/web/src/pages/SignUp.tsx | 16 +--- .../web/src/pages/SubscriptionSetting.tsx | 3 +- frontend/web/src/pages/WorkspaceSetting.tsx | 3 +- frontend/web/src/stores/v1/user.ts | 95 +++++++++++-------- frontend/web/src/stores/v1/view.ts | 1 + frontend/web/src/types/modules/shortcut.d.ts | 2 +- frontend/web/src/types/modules/user.d.ts | 34 +------ 15 files changed, 87 insertions(+), 138 deletions(-) diff --git a/frontend/web/src/components/CreateUserDialog.tsx b/frontend/web/src/components/CreateUserDialog.tsx index fec0b42..140d201 100644 --- a/frontend/web/src/components/CreateUserDialog.tsx +++ b/frontend/web/src/components/CreateUserDialog.tsx @@ -3,6 +3,7 @@ import { isUndefined } from "lodash-es"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; +import { Role, User } from "@/types/proto/api/v2/user_service"; import useLoading from "../hooks/useLoading"; import useUserStore from "../stores/v1/user"; import Icon from "./Icon"; @@ -14,11 +15,9 @@ interface Props { } interface State { - userCreate: UserCreate; + userCreate: Pick; } -const roles: Role[] = ["USER", "ADMIN"]; - const CreateUserDialog: React.FC = (props: Props) => { const { onClose, onConfirm, user } = props; const { t } = useTranslation(); @@ -28,7 +27,7 @@ const CreateUserDialog: React.FC = (props: Props) => { email: "", nickname: "", password: "", - role: "USER", + role: Role.USER, }, }); const requestState = useLoading(false); @@ -95,7 +94,7 @@ const CreateUserDialog: React.FC = (props: Props) => { try { if (user) { - const userPatch: UserPatch = { + const userPatch: Partial = { id: user.id, }; if (user.email !== state.userCreate.email) { @@ -179,9 +178,8 @@ const CreateUserDialog: React.FC = (props: Props) => {
- {roles.map((role) => ( - - ))} + +
diff --git a/frontend/web/src/components/Header.tsx b/frontend/web/src/components/Header.tsx index 4bdc7ad..bf40b2a 100644 --- a/frontend/web/src/components/Header.tsx +++ b/frontend/web/src/components/Header.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { Link, useLocation } from "react-router-dom"; import useWorkspaceStore from "@/stores/v1/workspace"; import { PlanType } from "@/types/proto/api/v2/subscription_service"; +import { Role } from "@/types/proto/api/v2/user_service"; import * as api from "../helpers/api"; import useUserStore from "../stores/v1/user"; import AboutDialog from "./AboutDialog"; @@ -17,11 +18,12 @@ const Header: React.FC = () => { const currentUser = useUserStore().getCurrentUser(); const [showAboutDialog, setShowAboutDialog] = useState(false); const profile = workspaceStore.profile; - const isAdmin = currentUser.role === "ADMIN"; + const isAdmin = currentUser.role === Role.ADMIN; const shouldShowRouterSwitch = location.pathname === "/" || location.pathname === "/collections"; const handleSignOutButtonClick = async () => { await api.signout(); + localStorage.removeItem("userId"); window.location.href = "/auth"; }; diff --git a/frontend/web/src/components/ShortcutActionsDropdown.tsx b/frontend/web/src/components/ShortcutActionsDropdown.tsx index 9d3801a..0d4b944 100644 --- a/frontend/web/src/components/ShortcutActionsDropdown.tsx +++ b/frontend/web/src/components/ShortcutActionsDropdown.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import useNavigateTo from "@/hooks/useNavigateTo"; +import { Role } from "@/types/proto/api/v2/user_service"; import { shortcutService } from "../services"; import useUserStore from "../stores/v1/user"; import { showCommonDialog } from "./Alert"; @@ -20,7 +21,7 @@ const ShortcutActionsDropdown = (props: Props) => { const currentUser = useUserStore().getCurrentUser(); const [showEditDrawer, setShowEditDrawer] = useState(false); const [showQRCodeDialog, setShowQRCodeDialog] = useState(false); - const havePermission = currentUser.role === "ADMIN" || shortcut.creatorId === currentUser.id; + const havePermission = currentUser.role === Role.ADMIN || shortcut.creatorId === currentUser.id; const handleDeleteShortcutButtonClick = (shortcut: Shortcut) => { showCommonDialog({ diff --git a/frontend/web/src/components/setting/AccountSection.tsx b/frontend/web/src/components/setting/AccountSection.tsx index f30c5b6..09684f7 100644 --- a/frontend/web/src/components/setting/AccountSection.tsx +++ b/frontend/web/src/components/setting/AccountSection.tsx @@ -1,6 +1,7 @@ import { Button } from "@mui/joy"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { Role } from "@/types/proto/api/v2/user_service"; import useUserStore from "../../stores/v1/user"; import ChangePasswordDialog from "../ChangePasswordDialog"; import EditUserinfoDialog from "../EditUserinfoDialog"; @@ -10,7 +11,7 @@ const AccountSection: React.FC = () => { const currentUser = useUserStore().getCurrentUser(); const [showEditUserinfoDialog, setShowEditUserinfoDialog] = useState(false); const [showChangePasswordDialog, setShowChangePasswordDialog] = useState(false); - const isAdmin = currentUser.role === "ADMIN"; + const isAdmin = currentUser.role === Role.ADMIN; return ( <> diff --git a/frontend/web/src/components/setting/MemberSection.tsx b/frontend/web/src/components/setting/MemberSection.tsx index a19f3a5..b815ac6 100644 --- a/frontend/web/src/components/setting/MemberSection.tsx +++ b/frontend/web/src/components/setting/MemberSection.tsx @@ -2,6 +2,7 @@ import { Button, IconButton } from "@mui/joy"; import { useEffect, useState } from "react"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; +import { User } from "@/types/proto/api/v2/user_service"; import useUserStore from "../../stores/v1/user"; import { showCommonDialog } from "../Alert"; import CreateUserDialog from "../CreateUserDialog"; diff --git a/frontend/web/src/helpers/api.ts b/frontend/web/src/helpers/api.ts index e67dae4..f00a2ee 100644 --- a/frontend/web/src/helpers/api.ts +++ b/frontend/web/src/helpers/api.ts @@ -1,5 +1,4 @@ import axios from "axios"; -import { userServiceClient } from "@/grpcweb"; export function signin(email: string, password: string) { return axios.post("/api/v1/auth/signin", { @@ -20,30 +19,6 @@ export function signout() { return axios.post("/api/v1/auth/logout"); } -export function getMyselfUser() { - return axios.get("/api/v1/user/me"); -} - -export function getUserList() { - return axios.get("/api/v1/user"); -} - -export function getUserById(id: number) { - return axios.get(`/api/v1/user/${id}`); -} - -export function createUser(userCreate: UserCreate) { - return axios.post("/api/v1/user", userCreate); -} - -export function patchUser(userPatch: UserPatch) { - return axios.patch(`/api/v1/user/${userPatch.id}`, userPatch); -} - -export function deleteUser(userId: UserId) { - return userServiceClient.deleteUser({ id: userId }); -} - export function getShortcutList(shortcutFind?: ShortcutFind) { const queryList = []; if (shortcutFind?.tag) { diff --git a/frontend/web/src/pages/ShortcutDetail.tsx b/frontend/web/src/pages/ShortcutDetail.tsx index d36e2e9..fc5cdea 100644 --- a/frontend/web/src/pages/ShortcutDetail.tsx +++ b/frontend/web/src/pages/ShortcutDetail.tsx @@ -6,6 +6,7 @@ import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useLoaderData } from "react-router-dom"; import useNavigateTo from "@/hooks/useNavigateTo"; +import { Role } from "@/types/proto/api/v2/user_service"; import { showCommonDialog } from "../components/Alert"; import AnalyticsView from "../components/AnalyticsView"; import CreateShortcutDrawer from "../components/CreateShortcutDrawer"; @@ -31,7 +32,7 @@ const ShortcutDetail = () => { showEditDrawer: false, }); const [showQRCodeDialog, setShowQRCodeDialog] = useState(false); - const havePermission = currentUser.role === "ADMIN" || shortcut.creatorId === currentUser.id; + const havePermission = currentUser.role === Role.ADMIN || shortcut.creatorId === currentUser.id; const shortcutLink = absolutifyLink(`/s/${shortcut.name}`); const favicon = getFaviconWithGoogleS2(shortcut.link); diff --git a/frontend/web/src/pages/SignIn.tsx b/frontend/web/src/pages/SignIn.tsx index a87c529..9e07708 100644 --- a/frontend/web/src/pages/SignIn.tsx +++ b/frontend/web/src/pages/SignIn.tsx @@ -3,16 +3,12 @@ import React, { FormEvent, useEffect, useState } from "react"; import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import useNavigateTo from "@/hooks/useNavigateTo"; import useWorkspaceStore from "@/stores/v1/workspace"; import * as api from "../helpers/api"; import useLoading from "../hooks/useLoading"; -import useUserStore from "../stores/v1/user"; const SignIn: React.FC = () => { const { t } = useTranslation(); - const navigateTo = useNavigateTo(); - const userStore = useUserStore(); const workspaceStore = useWorkspaceStore(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -20,11 +16,7 @@ const SignIn: React.FC = () => { const allowConfirm = email.length > 0 && password.length > 0; useEffect(() => { - if (userStore.getCurrentUser()) { - return navigateTo("/", { - replace: true, - }); - } + localStorage.removeItem("userId"); if (workspaceStore.profile.mode === "demo") { setEmail("steven@yourselfhosted.com"); @@ -50,12 +42,10 @@ const SignIn: React.FC = () => { try { actionBtnLoadingState.setLoading(); - await api.signin(email, password); - const user = await userStore.fetchCurrentUser(); + const { data: user } = await api.signin(email, password); if (user) { - navigateTo("/", { - replace: true, - }); + localStorage.setItem("userId", `${user.id}`); + window.location.href = "/"; } else { toast.error("Signin failed"); } diff --git a/frontend/web/src/pages/SignUp.tsx b/frontend/web/src/pages/SignUp.tsx index dceffbf..6f33bee 100644 --- a/frontend/web/src/pages/SignUp.tsx +++ b/frontend/web/src/pages/SignUp.tsx @@ -7,12 +7,10 @@ import useNavigateTo from "@/hooks/useNavigateTo"; import useWorkspaceStore from "@/stores/v1/workspace"; import * as api from "../helpers/api"; import useLoading from "../hooks/useLoading"; -import useUserStore from "../stores/v1/user"; const SignUp: React.FC = () => { const { t } = useTranslation(); const navigateTo = useNavigateTo(); - const userStore = useUserStore(); const workspaceStore = useWorkspaceStore(); const [email, setEmail] = useState(""); const [nickname, setNickname] = useState(""); @@ -21,11 +19,7 @@ const SignUp: React.FC = () => { const allowConfirm = email.length > 0 && nickname.length > 0 && password.length > 0; useEffect(() => { - if (userStore.getCurrentUser()) { - return navigateTo("/", { - replace: true, - }); - } + localStorage.removeItem("userId"); if (!workspaceStore.profile.enableSignup) { return navigateTo("/auth", { @@ -57,12 +51,10 @@ const SignUp: React.FC = () => { try { actionBtnLoadingState.setLoading(); - await api.signup(email, nickname, password); - const user = await userStore.fetchCurrentUser(); + const { data: user } = await api.signup(email, nickname, password); if (user) { - navigateTo("/", { - replace: true, - }); + localStorage.setItem("userId", `${user.id}`); + window.location.href = "/"; } else { toast.error("Signup failed"); } diff --git a/frontend/web/src/pages/SubscriptionSetting.tsx b/frontend/web/src/pages/SubscriptionSetting.tsx index 6e7978e..3964b6b 100644 --- a/frontend/web/src/pages/SubscriptionSetting.tsx +++ b/frontend/web/src/pages/SubscriptionSetting.tsx @@ -7,13 +7,14 @@ import { subscriptionServiceClient } from "@/grpcweb"; import { stringifyPlanType } from "@/stores/v1/subscription"; import useWorkspaceStore from "@/stores/v1/workspace"; import { PlanType } from "@/types/proto/api/v2/subscription_service"; +import { Role } from "@/types/proto/api/v2/user_service"; import useUserStore from "../stores/v1/user"; const SubscriptionSetting: React.FC = () => { const workspaceStore = useWorkspaceStore(); const currentUser = useUserStore().getCurrentUser(); const [licenseKey, setLicenseKey] = useState(""); - const isAdmin = currentUser.role === "ADMIN"; + const isAdmin = currentUser.role === Role.ADMIN; const profile = workspaceStore.profile; const handleUpdateLicenseKey = async () => { diff --git a/frontend/web/src/pages/WorkspaceSetting.tsx b/frontend/web/src/pages/WorkspaceSetting.tsx index 10c04e3..f56ecfd 100644 --- a/frontend/web/src/pages/WorkspaceSetting.tsx +++ b/frontend/web/src/pages/WorkspaceSetting.tsx @@ -4,6 +4,7 @@ import { Link } from "react-router-dom"; import Icon from "@/components/Icon"; import { stringifyPlanType } from "@/stores/v1/subscription"; import useWorkspaceStore from "@/stores/v1/workspace"; +import { Role } from "@/types/proto/api/v2/user_service"; import MemberSection from "../components/setting/MemberSection"; import WorkspaceSection from "../components/setting/WorkspaceSection"; import useUserStore from "../stores/v1/user"; @@ -11,7 +12,7 @@ import useUserStore from "../stores/v1/user"; const WorkspaceSetting: React.FC = () => { const workspaceStore = useWorkspaceStore(); const currentUser = useUserStore().getCurrentUser(); - const isAdmin = currentUser.role === "ADMIN"; + const isAdmin = currentUser.role === Role.ADMIN; const profile = workspaceStore.profile; useEffect(() => { diff --git a/frontend/web/src/stores/v1/user.ts b/frontend/web/src/stores/v1/user.ts index 2ffd343..b48c7bd 100644 --- a/frontend/web/src/stores/v1/user.ts +++ b/frontend/web/src/stores/v1/user.ts @@ -1,33 +1,25 @@ import { create } from "zustand"; -import { userSettingServiceClient } from "@/grpcweb"; +import { userServiceClient, userSettingServiceClient } from "@/grpcweb"; +import { User } from "@/types/proto/api/v2/user_service"; import { UserSetting } from "@/types/proto/api/v2/user_setting_service"; -import * as api from "../../helpers/api"; - -const convertResponseModelUser = (user: User): User => { - return { - ...user, - createdTs: user.createdTs * 1000, - updatedTs: user.updatedTs * 1000, - }; -}; interface UserState { - userMapById: Record; - userSettingMapById: Record; - currentUserId?: UserId; + userMapById: Record; + userSettingMapById: Record; + currentUserId?: number; // User related actions. fetchUserList: () => Promise; fetchCurrentUser: () => Promise; - getOrFetchUserById: (id: UserId) => Promise; - getUserById: (id: UserId) => User; + getOrFetchUserById: (id: number) => Promise; + getUserById: (id: number) => User; getCurrentUser: () => User; - createUser: (userCreate: UserCreate) => Promise; - patchUser: (userPatch: UserPatch) => Promise; - deleteUser: (id: UserId) => Promise; + createUser: (create: Partial) => Promise; + patchUser: (userPatch: Partial) => Promise; + deleteUser: (id: number) => Promise; // User setting related actions. - fetchUserSetting: (userId: UserId) => Promise; + fetchUserSetting: (userId: number) => Promise; updateUserSetting: (userSetting: UserSetting, updateMask: string[]) => Promise; getCurrentUserSetting: () => UserSetting; } @@ -36,65 +28,88 @@ const useUserStore = create()((set, get) => ({ userMapById: {}, userSettingMapById: {}, fetchUserList: async () => { - const { data: userList } = await api.getUserList(); + const { users } = await userServiceClient.listUsers({}); const userMap = get().userMapById; - userList.forEach((user) => { - userMap[user.id] = convertResponseModelUser(user); + users.forEach((user) => { + userMap[user.id] = user; }); set(userMap); - return userList; + return users; }, fetchCurrentUser: async () => { - const { data } = await api.getMyselfUser(); - const user = convertResponseModelUser(data); + const userId = localStorage.getItem("userId"); + if (!userId) { + throw new Error("User id not found in localStorage"); + } + const { user } = await userServiceClient.getUser({ + id: Number(userId), + }); + if (!user) { + throw new Error("User not found"); + } const userMap = get().userMapById; userMap[user.id] = user; set({ userMapById: userMap, currentUserId: user.id }); return user; }, - getOrFetchUserById: async (id: UserId) => { + getOrFetchUserById: async (id: number) => { const userMap = get().userMapById; if (userMap[id]) { return userMap[id] as User; } - const { data } = await api.getUserById(id); - const user = convertResponseModelUser(data); + const { user } = await userServiceClient.getUser({ + id: Number(id), + }); + if (!user) { + throw new Error("User not found"); + } userMap[id] = user; set(userMap); return user; }, - createUser: async (userCreate: UserCreate) => { - const { data } = await api.createUser(userCreate); - const user = convertResponseModelUser(data); + createUser: async (userCreate: Partial) => { + const { user } = await userServiceClient.createUser({ + user: userCreate, + }); + if (!user) { + throw new Error("User not found"); + } const userMap = get().userMapById; userMap[user.id] = user; set(userMap); return user; }, - patchUser: async (userPatch: UserPatch) => { - const { data } = await api.patchUser(userPatch); - const user = convertResponseModelUser(data); + patchUser: async (userPatch: Partial) => { + const { user } = await userServiceClient.updateUser({ + user: userPatch, + updateMask: ["email", "nickname"], + }); + if (!user) { + throw new Error("User not found"); + } const userMap = get().userMapById; userMap[user.id] = user; set(userMap); }, - deleteUser: async (userId: UserId) => { - await api.deleteUser(userId); + deleteUser: async (userId: number) => { + await userServiceClient.deleteUser({ + id: userId, + }); const userMap = get().userMapById; delete userMap[userId]; set(userMap); }, - getUserById: (id: UserId) => { + getUserById: (id: number) => { const userMap = get().userMapById; return userMap[id] as User; }, getCurrentUser: () => { const userMap = get().userMapById; const currentUserId = get().currentUserId; - return userMap[currentUserId as UserId]; + return userMap[currentUserId as number]; }, - fetchUserSetting: async (userId: UserId) => { + fetchUserSetting: async (userId: number) => { const userSetting = ( await userSettingServiceClient.getUserSetting({ id: userId, @@ -122,7 +137,7 @@ const useUserStore = create()((set, get) => ({ getCurrentUserSetting: () => { const userSettingMap = get().userSettingMapById; const currentUserId = get().currentUserId; - return userSettingMap[currentUserId as UserId]; + return userSettingMap[currentUserId as number]; }, })); diff --git a/frontend/web/src/stores/v1/view.ts b/frontend/web/src/stores/v1/view.ts index 2b6937b..07fd7f2 100644 --- a/frontend/web/src/stores/v1/view.ts +++ b/frontend/web/src/stores/v1/view.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; +import { User } from "@/types/proto/api/v2/user_service"; export interface Filter { tab?: string; diff --git a/frontend/web/src/types/modules/shortcut.d.ts b/frontend/web/src/types/modules/shortcut.d.ts index 8f53aca..6182891 100644 --- a/frontend/web/src/types/modules/shortcut.d.ts +++ b/frontend/web/src/types/modules/shortcut.d.ts @@ -11,7 +11,7 @@ interface OpenGraphMetadata { interface Shortcut { id: ShortcutId; - creatorId: UserId; + creatorId: number; creator: User; createdTs: TimeStamp; updatedTs: TimeStamp; diff --git a/frontend/web/src/types/modules/user.d.ts b/frontend/web/src/types/modules/user.d.ts index 9448dd0..1cd22fb 100644 --- a/frontend/web/src/types/modules/user.d.ts +++ b/frontend/web/src/types/modules/user.d.ts @@ -1,36 +1,6 @@ -type UserId = number; - -type Role = "ADMIN" | "USER"; - interface User { - id: UserId; - - createdTs: TimeStamp; - updatedTs: TimeStamp; - rowStatus: RowStatus; - + id: number; email: string; nickname: string; - role: Role; -} - -interface UserCreate { - email: string; - nickname: string; - password: string; - role: Role; -} - -interface UserPatch { - id: UserId; - - rowStatus?: RowStatus; - email?: string; - nickname?: string; - password?: string; - role?: Role; -} - -interface UserDelete { - id: UserId; + role: "ADMIN" | "USER"; }