From e7854c1ed8facc2955ef9ad96f0bb2513041bba9 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 16 Nov 2020 16:30:32 +0800 Subject: ... --- FrontEnd/src/app/App.tsx | 2 +- FrontEnd/src/app/http/user.ts | 46 ++++++++++++++++++++- FrontEnd/src/app/services/timeline.ts | 22 ++++++----- FrontEnd/src/app/services/user.ts | 57 ++++++++++++++++----------- FrontEnd/src/app/views/admin/Admin.tsx | 4 +- FrontEnd/src/app/views/admin/UserAdmin.tsx | 4 +- FrontEnd/src/app/views/common/AppBar.tsx | 4 +- FrontEnd/src/app/views/home/BoardWithUser.tsx | 4 +- 8 files changed, 101 insertions(+), 42 deletions(-) (limited to 'FrontEnd/src') diff --git a/FrontEnd/src/app/App.tsx b/FrontEnd/src/app/App.tsx index 01b1883b..6cdf2434 100644 --- a/FrontEnd/src/app/App.tsx +++ b/FrontEnd/src/app/App.tsx @@ -58,7 +58,7 @@ const App: React.FC = () => { - {user && user.administrator && ( + {user && user.hasAdministrationPermission && ( diff --git a/FrontEnd/src/app/http/user.ts b/FrontEnd/src/app/http/user.ts index a0a02cce..9ba6508f 100644 --- a/FrontEnd/src/app/http/user.ts +++ b/FrontEnd/src/app/http/user.ts @@ -12,10 +12,18 @@ import { convertToNotModified, } from "./common"; +export const kUserPermissionList = [ + "UserManagement", + "AllTimelineManagement", + "HighlightTimelineManagement", +] as const; + +export type UserPermission = typeof kUserPermissionList[number]; + export interface HttpUser { uniqueId: string; username: string; - administrator: boolean; + permissions: UserPermission[]; nickname: string; } @@ -54,6 +62,16 @@ export interface IHttpUserClient { ): Promise; putAvatar(username: string, data: Blob, token: string): Promise; changePassword(req: HttpChangePasswordRequest, token: string): Promise; + putUserPermission( + username: string, + permission: UserPermission, + token: string + ): Promise; + deleteUserPermission( + username: string, + permission: UserPermission, + token: string + ): Promise; } export class HttpUserClient implements IHttpUserClient { @@ -119,6 +137,32 @@ export class HttpUserClient implements IHttpUserClient { .catch(convertToNetworkError) .then(); } + + putUserPermission( + username: string, + permission: UserPermission, + token: string + ): Promise { + return axios + .put( + `${apiBaseUrl}/users/${username}/permissions/${permission}?token=${token}` + ) + .catch(convertToNetworkError) + .then(); + } + + deleteUserPermission( + username: string, + permission: UserPermission, + token: string + ): Promise { + return axios + .delete( + `${apiBaseUrl}/users/${username}/permissions/${permission}?token=${token}` + ) + .catch(convertToNetworkError) + .then(); + } } let client: IHttpUserClient = new HttpUserClient(); diff --git a/FrontEnd/src/app/services/timeline.ts b/FrontEnd/src/app/services/timeline.ts index 2cbbffab..c58516fc 100644 --- a/FrontEnd/src/app/services/timeline.ts +++ b/FrontEnd/src/app/services/timeline.ts @@ -29,11 +29,11 @@ export type { TimelineVisibility } from "@/http/timeline"; import { dataStorage, throwIfNotNetworkError, BlobOrStatus } from "./common"; import { DataHub, WithSyncStatus } from "./DataHub"; import { - UserAuthInfo, checkLogin, userService, userInfoService, User, + AuthUser, } from "./user"; export type TimelineInfo = HttpTimelineInfo; @@ -608,10 +608,11 @@ export class TimelineService { } hasReadPermission( - user: UserAuthInfo | null | undefined, + user: AuthUser | null | undefined, timeline: TimelineInfo ): boolean { - if (user != null && user.administrator) return true; + if (user != null && user.hasAllTimelineAdministrationPermission) + return true; const { visibility } = timeline; if (visibility === "Public") { @@ -631,10 +632,11 @@ export class TimelineService { } hasPostPermission( - user: UserAuthInfo | null | undefined, + user: AuthUser | null | undefined, timeline: TimelineInfo ): boolean { - if (user != null && user.administrator) return true; + if (user != null && user.hasAllTimelineAdministrationPermission) + return true; return ( user != null && @@ -644,20 +646,22 @@ export class TimelineService { } hasManagePermission( - user: UserAuthInfo | null | undefined, + user: AuthUser | null | undefined, timeline: TimelineInfo ): boolean { - if (user != null && user.administrator) return true; + if (user != null && user.hasAllTimelineAdministrationPermission) + return true; return user != null && user.username == timeline.owner.username; } hasModifyPostPermission( - user: UserAuthInfo | null | undefined, + user: AuthUser | null | undefined, timeline: TimelineInfo, post: TimelinePostInfo ): boolean { - if (user != null && user.administrator) return true; + if (user != null && user.hasAllTimelineAdministrationPermission) + return true; return ( user != null && diff --git a/FrontEnd/src/app/services/user.ts b/FrontEnd/src/app/services/user.ts index cd6d1c15..0166bce0 100644 --- a/FrontEnd/src/app/services/user.ts +++ b/FrontEnd/src/app/services/user.ts @@ -14,6 +14,7 @@ import { getHttpUserClient, HttpUserNotExistError, HttpUser, + UserPermission, } from "@/http/user"; import { dataStorage, throwIfNotNetworkError } from "./common"; @@ -22,13 +23,26 @@ import { pushAlert } from "./alert"; export type User = HttpUser; -export interface UserAuthInfo { +export class AuthUser implements User { + constructor(user: User, public token: string) { + this.uniqueId = user.uniqueId; + this.username = user.username; + this.permissions = user.permissions; + this.nickname = user.nickname; + } + + uniqueId: string; username: string; - administrator: boolean; -} + permissions: UserPermission[]; + nickname: string; -export interface UserWithToken extends User { - token: string; + get hasAdministrationPermission(): boolean { + return this.permissions.length !== 0; + } + + get hasAllTimelineAdministrationPermission(): boolean { + return this.permissions.includes("AllTimelineManagement"); + } } export interface LoginCredentials { @@ -43,24 +57,24 @@ export class BadCredentialError { const USER_STORAGE_KEY = "currentuser"; export class UserService { - private userSubject = new BehaviorSubject( + private userSubject = new BehaviorSubject( undefined ); - get user$(): Observable { + get user$(): Observable { return this.userSubject; } - get currentUser(): UserWithToken | null | undefined { + get currentUser(): AuthUser | null | undefined { return this.userSubject.value; } - async checkLoginState(): Promise { + async checkLoginState(): Promise { if (this.currentUser !== undefined) { console.warn("Already checked user. Can't check twice."); } - const savedUser = await dataStorage.getItem( + const savedUser = await dataStorage.getItem( USER_STORAGE_KEY ); @@ -74,8 +88,8 @@ export class UserService { const savedToken = savedUser.token; try { const res = await getHttpTokenClient().verify({ token: savedToken }); - const user: UserWithToken = { ...res.user, token: savedToken }; - await dataStorage.setItem(USER_STORAGE_KEY, user); + const user = new AuthUser(res.user, savedToken); + await dataStorage.setItem(USER_STORAGE_KEY, user); this.userSubject.next(user); pushAlert({ type: "success", @@ -116,12 +130,9 @@ export class UserService { ...credentials, expire: 30, }); - const user: UserWithToken = { - ...res.user, - token: res.token, - }; + const user = new AuthUser(res.user, res.token); if (rememberMe) { - await dataStorage.setItem(USER_STORAGE_KEY, user); + await dataStorage.setItem(USER_STORAGE_KEY, user); } this.userSubject.next(user); } catch (e) { @@ -169,8 +180,8 @@ export class UserService { export const userService = new UserService(); -export function useRawUser(): UserWithToken | null | undefined { - const [user, setUser] = useState( +export function useRawUser(): AuthUser | null | undefined { + const [user, setUser] = useState( userService.currentUser ); useEffect(() => { @@ -182,8 +193,8 @@ export function useRawUser(): UserWithToken | null | undefined { return user; } -export function useUser(): UserWithToken | null { - const [user, setUser] = useState(() => { +export function useUser(): AuthUser | null { + const [user, setUser] = useState(() => { const initUser = userService.currentUser; if (initUser === undefined) { throw new UiLogicError( @@ -208,7 +219,7 @@ export function useUser(): UserWithToken | null { return user; } -export function useUserLoggedIn(): UserWithToken { +export function useUserLoggedIn(): AuthUser { const user = useUser(); if (user == null) { throw new UiLogicError("You assert user has logged in but actually not."); @@ -216,7 +227,7 @@ export function useUserLoggedIn(): UserWithToken { return user; } -export function checkLogin(): UserWithToken { +export function checkLogin(): AuthUser { const user = userService.currentUser; if (user == null) { throw new UiLogicError("You must login to perform the operation."); diff --git a/FrontEnd/src/app/views/admin/Admin.tsx b/FrontEnd/src/app/views/admin/Admin.tsx index 9c0250e7..a64a9bc0 100644 --- a/FrontEnd/src/app/views/admin/Admin.tsx +++ b/FrontEnd/src/app/views/admin/Admin.tsx @@ -8,12 +8,12 @@ import { } from "react-router"; import { Nav } from "react-bootstrap"; -import { UserWithToken } from "@/services/user"; +import { AuthUser } from "@/services/user"; import UserAdmin from "./UserAdmin"; interface AdminProps { - user: UserWithToken; + user: AuthUser; } const Admin: React.FC = (props) => { diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx index 0f5f8796..4ad9ed09 100644 --- a/FrontEnd/src/app/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx @@ -10,7 +10,7 @@ import { } from "react-bootstrap"; import OperationDialog from "../common/OperationDialog"; -import { User, UserWithToken } from "@/services/user"; +import { User, AuthUser } from "@/services/user"; const apiBaseUrl = "/api"; @@ -285,7 +285,7 @@ const UserChangePermissionDialog: React.FC = ( }; interface UserAdminProps { - user: UserWithToken; + user: AuthUser; } const UserAdmin: React.FC = (props) => { diff --git a/FrontEnd/src/app/views/common/AppBar.tsx b/FrontEnd/src/app/views/common/AppBar.tsx index 8f35b482..11d3de04 100644 --- a/FrontEnd/src/app/views/common/AppBar.tsx +++ b/FrontEnd/src/app/views/common/AppBar.tsx @@ -15,7 +15,7 @@ const AppBar: React.FC = (_) => { const { t } = useTranslation(); - const isAdministrator = user && user.administrator; + const hasAdministrationPermission = user && user.hasAdministrationPermission; const [expand, setExpand] = React.useState(false); const collapse = (): void => setExpand(false); @@ -56,7 +56,7 @@ const AppBar: React.FC = (_) => { {t("nav.about")} - {isAdministrator && ( + {hasAdministrationPermission && ( = ({ user }) => { +const BoardWithUser: React.FC<{ user: AuthUser }> = ({ user }) => { const { t } = useTranslation(); const [ownTimelines, setOwnTimelines] = React.useState< -- cgit v1.2.3 From 8f5ffab46b18eb30dfebeb8407435dd85dc35232 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 16 Nov 2020 17:20:42 +0800 Subject: ... --- FrontEnd/src/app/http/user.ts | 8 +++ FrontEnd/src/app/views/admin/Admin.tsx | 64 ++++----------------- FrontEnd/src/app/views/admin/AdminSubPage.tsx | 44 +++++++++++++++ FrontEnd/src/app/views/admin/UserAdmin.tsx | 80 ++++++--------------------- 4 files changed, 80 insertions(+), 116 deletions(-) create mode 100644 FrontEnd/src/app/views/admin/AdminSubPage.tsx (limited to 'FrontEnd/src') diff --git a/FrontEnd/src/app/http/user.ts b/FrontEnd/src/app/http/user.ts index 9ba6508f..92a6433e 100644 --- a/FrontEnd/src/app/http/user.ts +++ b/FrontEnd/src/app/http/user.ts @@ -49,6 +49,7 @@ export class HttpChangePasswordBadCredentialError extends Error { } export interface IHttpUserClient { + list(): Promise; get(username: string): Promise; patch( username: string, @@ -75,6 +76,13 @@ export interface IHttpUserClient { } export class HttpUserClient implements IHttpUserClient { + list(): Promise { + return axios + .get(`${apiBaseUrl}/users`) + .then(extractResponseData) + .catch(convertToNetworkError); + } + get(username: string): Promise { return axios .get(`${apiBaseUrl}/users/${username}`) diff --git a/FrontEnd/src/app/views/admin/Admin.tsx b/FrontEnd/src/app/views/admin/Admin.tsx index a64a9bc0..e17da5bc 100644 --- a/FrontEnd/src/app/views/admin/Admin.tsx +++ b/FrontEnd/src/app/views/admin/Admin.tsx @@ -1,12 +1,5 @@ import React, { Fragment } from "react"; -import { - Redirect, - Route, - Switch, - useRouteMatch, - useHistory, -} from "react-router"; -import { Nav } from "react-bootstrap"; +import { Redirect, Route, Switch, useRouteMatch, match } from "react-router"; import { AuthUser } from "@/services/user"; @@ -16,57 +9,22 @@ interface AdminProps { user: AuthUser; } -const Admin: React.FC = (props) => { +const Admin: React.FC = ({ user }) => { const match = useRouteMatch(); - const history = useHistory(); - type TabNames = "users" | "more"; - - const tabName = history.location.pathname.replace(match.path + "/", ""); - - function toggle(newTab: TabNames): void { - history.push(`${match.url}/${newTab}`); - } - - const createRoute = ( - name: string, - body: React.ReactNode - ): React.ReactNode => { - return ( - -
- - {body} - - ); - }; return ( - {createRoute("users", )} - {createRoute("more",
More Page Works
)} + + {(p) => { + const match = p.match as match<{ name: string }>; + const name = match.params["name"]; + if (name === "users") { + return ; + } + }} +
); diff --git a/FrontEnd/src/app/views/admin/AdminSubPage.tsx b/FrontEnd/src/app/views/admin/AdminSubPage.tsx new file mode 100644 index 00000000..5d2df13c --- /dev/null +++ b/FrontEnd/src/app/views/admin/AdminSubPage.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { Nav } from "react-bootstrap"; +import { useHistory, useRouteMatch } from "react-router"; + +const AdminSubPage: React.FC = ({ children }) => { + const match = useRouteMatch<{ name: string }>(); + const history = useHistory(); + + const name = match.params.name; + + function toggle(newTab: string): void { + history.push(`/admin/${newTab}`); + } + + return ( + <> + + {children} + + ); +}; + +export default AdminSubPage; diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx index 4ad9ed09..856e6136 100644 --- a/FrontEnd/src/app/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx @@ -10,19 +10,16 @@ import { } from "react-bootstrap"; import OperationDialog from "../common/OperationDialog"; +import AdminSubPage from "./AdminSubPage"; + import { User, AuthUser } from "@/services/user"; +import { getHttpUserClient } from "@/http/user"; const apiBaseUrl = "/api"; -async function fetchUserList(_token: string): Promise { - const res = await axios.get(`${apiBaseUrl}/users`); - return res.data; -} - interface CreateUserInfo { username: string; password: string; - administrator: boolean; } async function createUser(user: CreateUserInfo, token: string): Promise { @@ -57,16 +54,6 @@ function changePassword( }); } -function changePermission( - username: string, - newPermission: boolean, - token: string -): Promise { - return axios.patch(`${apiBaseUrl}/users/${username}?token=${token}`, { - administrator: newPermission, - }); -} - const kChangeUsername = "changeusername"; const kChangePassword = "changepassword"; const kChangePermission = "changepermission"; @@ -103,9 +90,9 @@ const UserItem: React.FC = (props) => {

{user.username}

- {user.administrator ? "administrator" : "user"} + {user.permissions ? "administrator" : "user"} @@ -156,14 +143,12 @@ const CreateUserDialog: React.FC = (props) => { [ { type: "text", label: "Username" }, { type: "text", label: "Password" }, - { type: "bool", label: "Administrator" }, ] as const } - onProcess={([username, password, administrator]) => + onProcess={([username, password]) => props.process({ username: username, password: password, - administrator: administrator, }) } close={props.close} @@ -316,15 +301,17 @@ const UserAdmin: React.FC = (props) => { useEffect(() => { let subscribe = true; - void fetchUserList(props.user.token).then((us) => { - if (subscribe) { - setUsers(us); - } - }); + void getHttpUserClient() + .list() + .then((us) => { + if (subscribe) { + setUsers(us); + } + }); return () => { subscribe = false; }; - }, [props.user]); + }, []); let dialogNode: React.ReactNode; if (dialog) @@ -389,26 +376,6 @@ const UserAdmin: React.FC = (props) => { ); break; case kChangePermission: { - const newPermission = dialog.newPermission; - dialogNode = ( - setDialog(null)} - username={dialog.username} - newPermission={newPermission} - process={async () => { - await changePermission(dialog.username, newPermission, token); - setUsers((oldUsers) => { - const users = (oldUsers ?? []).slice(); - const findedUser = users.find( - (u) => u.username === dialog.username - ); - if (findedUser) findedUser.administrator = newPermission; - return users; - }); - }} - /> - ); break; } } @@ -419,26 +386,13 @@ const UserAdmin: React.FC = (props) => { { - setDialog( - item === kChangePermission - ? { - type: kChangePermission, - username: user.username, - newPermission: !user.administrator, - } - : { - type: item, - username: user.username, - } - ); - }} + onContextMenu={(item) => {}} /> ); }); return ( - <> + {userComponents} {dialogNode} - + ); } else { return ; -- cgit v1.2.3 From 2493232c8491b51c26e3841f99ae5a18d23f8560 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 17 Nov 2020 17:31:42 +0800 Subject: ... --- FrontEnd/src/app/http/user.ts | 25 ++ FrontEnd/src/app/views/admin/AdminSubPage.tsx | 18 +- FrontEnd/src/app/views/admin/UserAdmin.tsx | 352 +++++++--------------- FrontEnd/src/app/views/common/OperationDialog.tsx | 31 +- 4 files changed, 163 insertions(+), 263 deletions(-) (limited to 'FrontEnd/src') diff --git a/FrontEnd/src/app/http/user.ts b/FrontEnd/src/app/http/user.ts index 92a6433e..243846d1 100644 --- a/FrontEnd/src/app/http/user.ts +++ b/FrontEnd/src/app/http/user.ts @@ -28,6 +28,8 @@ export interface HttpUser { } export interface HttpUserPatchRequest { + username?: string; + password?: string; nickname?: string; } @@ -36,6 +38,11 @@ export interface HttpChangePasswordRequest { newPassword: string; } +export interface HttpCreateUserRequest { + username: string; + password: string; +} + export class HttpUserNotExistError extends Error { constructor(public innerError?: AxiosError) { super(); @@ -56,6 +63,7 @@ export interface IHttpUserClient { req: HttpUserPatchRequest, token: string ): Promise; + delete(username: string, token: string): Promise; getAvatar(username: string): Promise; getAvatar( username: string, @@ -73,6 +81,8 @@ export interface IHttpUserClient { permission: UserPermission, token: string ): Promise; + + createUser(req: HttpCreateUserRequest, token: string): Promise; } export class HttpUserClient implements IHttpUserClient { @@ -102,6 +112,13 @@ export class HttpUserClient implements IHttpUserClient { .catch(convertToNetworkError); } + delete(username: string, token: string): Promise { + return axios + .delete(`${apiBaseUrl}/users/${username}?token=${token}`) + .catch(convertToNetworkError) + .then(); + } + getAvatar(username: string): Promise; getAvatar( username: string, @@ -171,6 +188,14 @@ export class HttpUserClient implements IHttpUserClient { .catch(convertToNetworkError) .then(); } + + createUser(req: HttpCreateUserRequest, token: string): Promise { + return axios + .post(`${apiBaseUrl}/userop/createuser?token=${token}`, req) + .then(extractResponseData) + .catch(convertToNetworkError) + .then(); + } } let client: IHttpUserClient = new HttpUserClient(); diff --git a/FrontEnd/src/app/views/admin/AdminSubPage.tsx b/FrontEnd/src/app/views/admin/AdminSubPage.tsx index 5d2df13c..5efe1ee3 100644 --- a/FrontEnd/src/app/views/admin/AdminSubPage.tsx +++ b/FrontEnd/src/app/views/admin/AdminSubPage.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Nav } from "react-bootstrap"; +import { Container, Nav } from "react-bootstrap"; import { useHistory, useRouteMatch } from "react-router"; const AdminSubPage: React.FC = ({ children }) => { @@ -13,8 +13,8 @@ const AdminSubPage: React.FC = ({ children }) => { } return ( - <> -
); }; diff --git a/FrontEnd/src/app/views/admin/admin.sass b/FrontEnd/src/app/views/admin/admin.sass index e69de29b..8eac7e49 100644 --- a/FrontEnd/src/app/views/admin/admin.sass +++ b/FrontEnd/src/app/views/admin/admin.sass @@ -0,0 +1,19 @@ +.admin-user-item + position: relative + + .edit-mask + position: absolute + top: 0 + left: 0 + bottom: 0 + right: 0 + + background: #ffffffc5 + position: absolute + + display: flex + justify-content: center + align-items: center + + button + margin: 0 2em -- cgit v1.2.3 From cf06a813e77dae460e6540706bc0c90338ea9ed3 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 20 Nov 2020 20:14:12 +0800 Subject: ... --- FrontEnd/src/app/http/user.ts | 10 +- FrontEnd/src/app/views/admin/UserAdmin.tsx | 213 +++++++++++++++++++++-------- 2 files changed, 165 insertions(+), 58 deletions(-) (limited to 'FrontEnd/src') diff --git a/FrontEnd/src/app/http/user.ts b/FrontEnd/src/app/http/user.ts index 243846d1..929956d0 100644 --- a/FrontEnd/src/app/http/user.ts +++ b/FrontEnd/src/app/http/user.ts @@ -12,10 +12,14 @@ import { convertToNotModified, } from "./common"; +export const kUserManagement = "UserManagement"; +export const kAllTimelineManagement = "AllTimelineManagement"; +export const kHighlightTimelineManagement = "HighlightTimelineManagement"; + export const kUserPermissionList = [ - "UserManagement", - "AllTimelineManagement", - "HighlightTimelineManagement", + kUserManagement, + kAllTimelineManagement, + kHighlightTimelineManagement, ] as const; export type UserPermission = typeof kUserPermissionList[number]; diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx index 65cd3b74..bd60381f 100644 --- a/FrontEnd/src/app/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx @@ -1,64 +1,20 @@ import React, { useState, useEffect } from "react"; +import clsx from "clsx"; import { ListGroup, Row, Col, Spinner, Button } from "react-bootstrap"; import InlineSVG from "react-inlinesvg"; import PencilSquareIcon from "bootstrap-icons/icons/pencil-square.svg"; -import OperationDialog from "../common/OperationDialog"; +import OperationDialog, { + OperationBoolInputInfo, +} from "../common/OperationDialog"; import { User, AuthUser } from "@/services/user"; -import { getHttpUserClient, HttpUser } from "@/http/user"; -import clsx from "clsx"; - -const kModify = "modify"; -const kDelete = "delete"; - -type TModify = typeof kModify; -type TDelete = typeof kDelete; - -type ContextMenuItem = TModify | TDelete; - -interface UserCardProps { - on: { [key in ContextMenuItem]: () => void }; - user: User; -} - -const UserItem: React.FC = ({ user, on }) => { - const [editMaskVisible, setEditMaskVisible] = React.useState(false); - - return ( - - setEditMaskVisible(true)} - /> -

{user.username}

-
nickname: {user.nickname}
-
unique id: {user.uniqueId}
-
- permissions:{" "} - {user.permissions.map((permission) => { - return ( - - {permission}{" "} - - ); - })} -
-
setEditMaskVisible(false)} - > - - -
-
- ); -}; +import { + getHttpUserClient, + HttpUser, + kUserPermissionList, + UserPermission, +} from "@/http/user"; interface DialogProps { open: boolean; @@ -167,6 +123,127 @@ const UserModifyDialog: React.FC> = ({ open, close, token, data: { username, permissions }, onSuccess }) => { + const oldPermissionBoolList: boolean[] = kUserPermissionList.map( + (permission) => permissions.includes(permission) + ); + + return ( + ( + <> + You are modify permission of user + {username} ! + + )} + inputScheme={kUserPermissionList.map( + (permission, index) => ({ + type: "bool", + label: permission, + initValue: oldPermissionBoolList[index], + }) + )} + onProcess={async (newPermissionBoolList): Promise => { + for (let index = 0; index < kUserPermissionList.length; index++) { + const oldValue = oldPermissionBoolList[index]; + const newValue = newPermissionBoolList[index]; + const permission = kUserPermissionList[index]; + if (oldValue === newValue) continue; + if (newValue) { + await getHttpUserClient().putUserPermission( + username, + permission, + token + ); + } else { + await getHttpUserClient().deleteUserPermission( + username, + permission, + token + ); + } + } + return newPermissionBoolList; + }} + onSuccessAndClose={(newPermissionBoolList: boolean[]) => { + const permissions: UserPermission[] = []; + for (let index = 0; index < kUserPermissionList.length; index++) { + if (newPermissionBoolList[index]) { + permissions.push(kUserPermissionList[index]); + } + } + onSuccess(permissions); + }} + /> + ); +}; + +const kModify = "modify"; +const kModifyPermission = "permission"; +const kDelete = "delete"; + +type TModify = typeof kModify; +type TModifyPermission = typeof kModifyPermission; +type TDelete = typeof kDelete; + +type ContextMenuItem = TModify | TModifyPermission | TDelete; + +interface UserItemProps { + on: { [key in ContextMenuItem]: () => void }; + user: User; +} + +const UserItem: React.FC = ({ user, on }) => { + const [editMaskVisible, setEditMaskVisible] = React.useState(false); + + return ( + + setEditMaskVisible(true)} + /> +

{user.username}

+
nickname: {user.nickname}
+
unique id: {user.uniqueId}
+
+ permissions:{" "} + {user.permissions.map((permission) => { + return ( + + {permission}{" "} + + ); + })} +
+
setEditMaskVisible(false)} + > + + + +
+
+ ); +}; + interface UserAdminProps { user: AuthUser; } @@ -181,6 +258,11 @@ const UserAdmin: React.FC = (props) => { type: TModify; user: HttpUser; } + | { + type: TModifyPermission; + username: string; + permissions: UserPermission[]; + } | { type: TDelete; username: string }; const [users, setUsers] = useState(null); @@ -220,7 +302,7 @@ const UserAdmin: React.FC = (props) => { /> ); break; - case "delete": + case kDelete: dialogNode = ( = (props) => { /> ); break; + case kModifyPermission: + dialogNode = ( + setDialog(null)} + token={token} + data={{ + username: dialog.username, + permissions: dialog.permissions, + }} + onSuccess={updateUsers} + /> + ); + break; } } @@ -258,6 +354,13 @@ const UserAdmin: React.FC = (props) => { user, }); }, + permission: () => { + setDialog({ + type: kModifyPermission, + username: user.username, + permissions: user.permissions, + }); + }, delete: () => { setDialog({ type: "delete", -- cgit v1.2.3 From 8482b649e13a9f470801d3c0a87672099da86a4e Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 20 Nov 2020 20:33:01 +0800 Subject: ... --- FrontEnd/src/app/locales/en/admin.json | 16 +++++++++++++++- FrontEnd/src/app/locales/en/translation.json | 3 ++- FrontEnd/src/app/locales/zh/admin.json | 16 +++++++++++++++- FrontEnd/src/app/locales/zh/translation.json | 3 ++- FrontEnd/src/app/views/admin/Admin.tsx | 4 ++++ FrontEnd/src/app/views/admin/AdminNav.tsx | 7 +++++-- FrontEnd/src/app/views/admin/UserAdmin.tsx | 25 ++++++++++++++++++------- FrontEnd/src/app/views/common/AppBar.tsx | 2 +- 8 files changed, 62 insertions(+), 14 deletions(-) (limited to 'FrontEnd/src') diff --git a/FrontEnd/src/app/locales/en/admin.json b/FrontEnd/src/app/locales/en/admin.json index 69a88e3b..a0290206 100644 --- a/FrontEnd/src/app/locales/en/admin.json +++ b/FrontEnd/src/app/locales/en/admin.json @@ -1 +1,15 @@ -{} +{ + "nav": { + "users": "Users", + "highlightTimelines": "Highlight Timelines" + }, + "create": "Create", + "user": { + "nickname": "Nickname: ", + "uniqueId": "Unique ID: ", + "permissions": "Permissions: ", + "modify": "Modify", + "modifyPermissions": "Modify Permissions", + "delete": "Delete" + } +} diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json index 662a1aac..cdb6da37 100644 --- a/FrontEnd/src/app/locales/en/translation.json +++ b/FrontEnd/src/app/locales/en/translation.json @@ -13,7 +13,8 @@ "nav": { "settings": "Settings", "login": "Login", - "about": "About" + "about": "About", + "administration": "Administration" }, "chooseImage": "Choose a image", "loadImageError": "Failed to load image.", diff --git a/FrontEnd/src/app/locales/zh/admin.json b/FrontEnd/src/app/locales/zh/admin.json index 69a88e3b..d6895206 100644 --- a/FrontEnd/src/app/locales/zh/admin.json +++ b/FrontEnd/src/app/locales/zh/admin.json @@ -1 +1,15 @@ -{} +{ + "nav": { + "users": "用户", + "highlightTimelines": "高光时间线" + }, + "create": "创建", + "user": { + "nickname": "昵称:", + "uniqueId": "唯一ID:", + "permissions": "权限:", + "modify": "修改", + "modifyPermissions": "修改权限", + "delete": "删除" + } +} diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json index ecd1df4b..5d28f694 100644 --- a/FrontEnd/src/app/locales/zh/translation.json +++ b/FrontEnd/src/app/locales/zh/translation.json @@ -13,7 +13,8 @@ "nav": { "settings": "设置", "login": "登陆", - "about": "关于" + "about": "关于", + "administration": "管理" }, "chooseImage": "选择一个图片", "loadImageError": "加载图片失败", diff --git a/FrontEnd/src/app/views/admin/Admin.tsx b/FrontEnd/src/app/views/admin/Admin.tsx index ac640700..94e97713 100644 --- a/FrontEnd/src/app/views/admin/Admin.tsx +++ b/FrontEnd/src/app/views/admin/Admin.tsx @@ -4,10 +4,14 @@ import { Container } from "react-bootstrap"; import { AuthUser } from "@/services/user"; +import i18n from "@/i18n"; + import AdminNav from "./AdminNav"; import UserAdmin from "./UserAdmin"; import HighlightTimelineAdmin from "./HighlightTimelineAdmin"; +void i18n.loadNamespaces("admin"); + interface AdminProps { user: AuthUser; } diff --git a/FrontEnd/src/app/views/admin/AdminNav.tsx b/FrontEnd/src/app/views/admin/AdminNav.tsx index 040b479f..f376beda 100644 --- a/FrontEnd/src/app/views/admin/AdminNav.tsx +++ b/FrontEnd/src/app/views/admin/AdminNav.tsx @@ -1,11 +1,14 @@ import React from "react"; import { Nav } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; import { useHistory, useRouteMatch } from "react-router"; const AdminNav: React.FC = () => { const match = useRouteMatch<{ name: string }>(); const history = useHistory(); + const { t } = useTranslation(); + const name = match.params.name; function toggle(newTab: string): void { @@ -21,7 +24,7 @@ const AdminNav: React.FC = () => { toggle("users"); }} > - Users + {t("admin:nav.users")} @@ -31,7 +34,7 @@ const AdminNav: React.FC = () => { toggle("highlighttimelines"); }} > - Highlight Timelines + {t("admin:nav.highlightTimelines")} diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx index bd60381f..8fd9b351 100644 --- a/FrontEnd/src/app/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx @@ -15,6 +15,7 @@ import { kUserPermissionList, UserPermission, } from "@/http/user"; +import { useTranslation } from "react-i18next"; interface DialogProps { open: boolean; @@ -204,6 +205,8 @@ interface UserItemProps { } const UserItem: React.FC = ({ user, on }) => { + const { t } = useTranslation(); + const [editMaskVisible, setEditMaskVisible] = React.useState(false); return ( @@ -214,10 +217,16 @@ const UserItem: React.FC = ({ user, on }) => { onClick={() => setEditMaskVisible(true)} />

{user.username}

-
nickname: {user.nickname}
-
unique id: {user.uniqueId}
- permissions:{" "} + {t("admin:user.nickname")} + {user.nickname} +
+
+ {t("admin:user.uniqueId")} + {user.uniqueId} +
+
+ {t("admin:user.permissions")} {user.permissions.map((permission) => { return ( @@ -231,13 +240,13 @@ const UserItem: React.FC = ({ user, on }) => { onClick={() => setEditMaskVisible(false)} >
@@ -249,6 +258,8 @@ interface UserAdminProps { } const UserAdmin: React.FC = (props) => { + const { t } = useTranslation(); + type DialogInfo = | null | { @@ -384,7 +395,7 @@ const UserAdmin: React.FC = (props) => { }) } > - Create + {t("admin:create")} diff --git a/FrontEnd/src/app/views/common/AppBar.tsx b/FrontEnd/src/app/views/common/AppBar.tsx index 11d3de04..c862a6d3 100644 --- a/FrontEnd/src/app/views/common/AppBar.tsx +++ b/FrontEnd/src/app/views/common/AppBar.tsx @@ -63,7 +63,7 @@ const AppBar: React.FC = (_) => { activeClassName="active" onClick={collapse} > - Administration + {t("nav.administration")}
)} -- cgit v1.2.3 From aa8f5e0edfc136bf39b8be4bc545eb8c6f5420f3 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 20 Nov 2020 20:49:05 +0800 Subject: ... --- FrontEnd/src/app/locales/en/admin.json | 22 +++++++++- FrontEnd/src/app/locales/zh/admin.json | 22 +++++++++- FrontEnd/src/app/views/admin/UserAdmin.tsx | 50 +++++++++++++---------- FrontEnd/src/app/views/common/OperationDialog.tsx | 3 +- 4 files changed, 72 insertions(+), 25 deletions(-) (limited to 'FrontEnd/src') diff --git a/FrontEnd/src/app/locales/en/admin.json b/FrontEnd/src/app/locales/en/admin.json index a0290206..098ffb1f 100644 --- a/FrontEnd/src/app/locales/en/admin.json +++ b/FrontEnd/src/app/locales/en/admin.json @@ -5,11 +5,31 @@ }, "create": "Create", "user": { + "username": "Username: ", + "password": "Password: ", "nickname": "Nickname: ", "uniqueId": "Unique ID: ", "permissions": "Permissions: ", "modify": "Modify", "modifyPermissions": "Modify Permissions", - "delete": "Delete" + "delete": "Delete", + "dialog": { + "create": { + "title": "Create User", + "prompt": "You are creating a new user." + }, + "delete": { + "title": "Delete user", + "prompt": "You are deleting <1>username . Caution: This can't be undo." + }, + "modify": { + "title": "Modify User", + "prompt": "You are modifying user <1>username ." + }, + "modifyPermissions": { + "title": "Modify User Permissions", + "prompt": "You are modifying permissions of user <1>username ." + } + } } } diff --git a/FrontEnd/src/app/locales/zh/admin.json b/FrontEnd/src/app/locales/zh/admin.json index d6895206..fed39b2d 100644 --- a/FrontEnd/src/app/locales/zh/admin.json +++ b/FrontEnd/src/app/locales/zh/admin.json @@ -5,11 +5,31 @@ }, "create": "创建", "user": { + "username": "用户名:", + "password": "密码:", "nickname": "昵称:", "uniqueId": "唯一ID:", "permissions": "权限:", "modify": "修改", "modifyPermissions": "修改权限", - "delete": "删除" + "delete": "删除", + "dialog": { + "create": { + "title": "创建用户", + "prompt": "您正在创建一个新用户。" + }, + "delete": { + "title": "删除用户", + "prompt": "您正在删除用户 <1>username 。注意:此操作不可撤销。" + }, + "modify": { + "title": "修改用户", + "prompt": "您正在修改用户 <1>username 。" + }, + "modifyPermissions": { + "title": "修改用户权限", + "prompt": "您正在修改用户 <1>username 的权限。" + } + } } } diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx index 8fd9b351..3cfd5880 100644 --- a/FrontEnd/src/app/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx @@ -15,7 +15,7 @@ import { kUserPermissionList, UserPermission, } from "@/http/user"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; interface DialogProps { open: boolean; @@ -33,13 +33,13 @@ const CreateUserDialog: React.FC> = ({ }) => { return ( @@ -70,12 +70,12 @@ const UserDeleteDialog: React.FC ( - <> - You are deleting user {username} ! - + + 0{username}2 + )} onProcess={() => getHttpUserClient().delete(username, token)} onSuccessAndClose={onSuccess} @@ -93,19 +93,26 @@ const UserModifyDialog: React.FC ( - <> - You are change the password of user - {oldUser.username} ! - + + 0{oldUser.username}2 + )} inputScheme={ [ - { type: "text", label: "New Username", initValue: oldUser.username }, - { type: "text", label: "New Password" }, - { type: "text", label: "New Nickname", initValue: oldUser.nickname }, + { + type: "text", + label: "admin:user.username", + initValue: oldUser.username, + }, + { type: "text", label: "admin:user.password" }, + { + type: "text", + label: "admin:user.nickname", + initValue: oldUser.nickname, + }, ] as const } onProcess={([username, password, nickname]) => @@ -139,13 +146,12 @@ const UserPermissionModifyDialog: React.FC ( - <> - You are modify permission of user - {username} ! - + + 0{username}2 + )} inputScheme={kUserPermissionList.map( (permission, index) => ({ diff --git a/FrontEnd/src/app/views/common/OperationDialog.tsx b/FrontEnd/src/app/views/common/OperationDialog.tsx index 08baf93a..bca5cb87 100644 --- a/FrontEnd/src/app/views/common/OperationDialog.tsx +++ b/FrontEnd/src/app/views/common/OperationDialog.tsx @@ -120,7 +120,8 @@ const OperationDialog = < >( props: OperationDialogProps ): React.ReactElement => { - const inputScheme = props.inputScheme as readonly OperationInputInfo[]; + const inputScheme = (props.inputScheme ?? + []) as readonly OperationInputInfo[]; const { t } = useTranslation(); -- cgit v1.2.3 From 5c1cad30c608b279502ab8fd641b0b95066b6808 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 20 Nov 2020 20:56:59 +0800 Subject: ... --- FrontEnd/src/app/views/admin/UserAdmin.tsx | 8 ++++---- FrontEnd/src/app/views/common/OperationDialog.tsx | 13 +++---------- FrontEnd/src/app/views/home/TimelineCreateDialog.tsx | 2 +- FrontEnd/src/app/views/settings/index.tsx | 2 +- .../views/timeline-common/TimelinePropertyChangeDialog.tsx | 1 - FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx | 2 +- FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx | 1 - 7 files changed, 10 insertions(+), 19 deletions(-) (limited to 'FrontEnd/src') diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx index 3cfd5880..948cbb25 100644 --- a/FrontEnd/src/app/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx @@ -34,7 +34,7 @@ const CreateUserDialog: React.FC> = ({ return ( ( 0{username}2 @@ -94,7 +94,7 @@ const UserModifyDialog: React.FC ( 0{oldUser.username}2 @@ -147,7 +147,7 @@ const UserPermissionModifyDialog: React.FC ( 0{username}2 diff --git a/FrontEnd/src/app/views/common/OperationDialog.tsx b/FrontEnd/src/app/views/common/OperationDialog.tsx index bca5cb87..77ed851f 100644 --- a/FrontEnd/src/app/views/common/OperationDialog.tsx +++ b/FrontEnd/src/app/views/common/OperationDialog.tsx @@ -99,7 +99,7 @@ export interface OperationDialogProps< open: boolean; close: () => void; title: I18nText | (() => React.ReactNode); - titleColor?: "default" | "dangerous" | "create" | string; + themeColor?: "danger" | "success" | string; onProcess: ( inputs: MapOperationInputInfoValueTypeList ) => Promise; @@ -313,7 +313,7 @@ const OperationDialog = < {t("operationDialog.cancel")} { @@ -362,14 +362,7 @@ const OperationDialog = < {title} diff --git a/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx b/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx index 786ebb5d..12bbfb54 100644 --- a/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx +++ b/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx @@ -18,7 +18,7 @@ const TimelineCreateDialog: React.FC = (props) => { = (props) => { return ( = (props) => { open={props.open} close={props.close} title="timeline.deleteDialog.title" - titleColor="danger" + themeColor="danger" inputPrompt={() => { return ( diff --git a/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx b/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx index 0e95b05b..f319ac37 100644 --- a/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx @@ -13,7 +13,6 @@ const ChangeNicknameDialog: React.FC = (props) => { Date: Fri, 20 Nov 2020 21:00:55 +0800 Subject: ... --- FrontEnd/src/app/views/admin/admin.sass | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'FrontEnd/src') diff --git a/FrontEnd/src/app/views/admin/admin.sass b/FrontEnd/src/app/views/admin/admin.sass index 8eac7e49..1ce010f8 100644 --- a/FrontEnd/src/app/views/admin/admin.sass +++ b/FrontEnd/src/app/views/admin/admin.sass @@ -15,5 +15,8 @@ justify-content: center align-items: center + @include media-breakpoint-down(xs) + flex-direction: column + button - margin: 0 2em + margin: 0.5em 2em -- cgit v1.2.3 From f1aabc06f1005b26bd1c0c5f36c98c28a62fc31e Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 22 Nov 2020 09:49:58 +0800 Subject: fix: Fix i18n. --- FrontEnd/src/app/views/admin/Admin.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'FrontEnd/src') diff --git a/FrontEnd/src/app/views/admin/Admin.tsx b/FrontEnd/src/app/views/admin/Admin.tsx index 94e97713..446cd36d 100644 --- a/FrontEnd/src/app/views/admin/Admin.tsx +++ b/FrontEnd/src/app/views/admin/Admin.tsx @@ -1,22 +1,21 @@ import React, { Fragment } from "react"; import { Redirect, Route, Switch, useRouteMatch, match } from "react-router"; import { Container } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; import { AuthUser } from "@/services/user"; -import i18n from "@/i18n"; - import AdminNav from "./AdminNav"; import UserAdmin from "./UserAdmin"; import HighlightTimelineAdmin from "./HighlightTimelineAdmin"; -void i18n.loadNamespaces("admin"); - interface AdminProps { user: AuthUser; } const Admin: React.FC = ({ user }) => { + useTranslation("admin"); + const match = useRouteMatch(); return ( -- cgit v1.2.3