From 5bbe89a3a653f32d7554723510874856c31e7377 Mon Sep 17 00:00:00 2001 From: xmdhs Date: Sun, 1 Oct 2023 21:31:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BE=A7=E6=A0=8F=E9=83=A8=E5=88=86=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.go | 3 +- db/ent/schema/user.go | 3 +- frontend/package.json | 3 +- frontend/pnpm-lock.yaml | 11 ++ frontend/src/apis/apis.ts | 15 +- frontend/src/apis/error.ts | 8 + frontend/src/apis/model.ts | 9 +- frontend/src/apis/utils.ts | 11 +- frontend/src/views/Layout.tsx | 274 ++++++++++++++++++++++------------ frontend/src/views/Login.tsx | 2 +- handle/yggdrasil/yggdrasil.go | 7 +- model/model.go | 5 +- service/user.go | 13 +- 13 files changed, 243 insertions(+), 121 deletions(-) create mode 100644 frontend/src/apis/error.ts diff --git a/config/config.go b/config/config.go index 47213ed..3b7ab35 100644 --- a/config/config.go +++ b/config/config.go @@ -20,8 +20,7 @@ type Config struct { RsaPriKey string TexturePath string TextureBaseUrl string - HomepageUrl string - RegisterUrl string + WebBaseUrl string ServerName string Captcha struct { diff --git a/db/ent/schema/user.go b/db/ent/schema/user.go index 7582c96..b7b304f 100644 --- a/db/ent/schema/user.go +++ b/db/ent/schema/user.go @@ -28,7 +28,8 @@ func (User) Fields() []ent.Field { field.String("reg_ip").SchemaType(map[string]string{ dialect.MySQL: "VARCHAR(32)", }), - // 二进制状态位,保留 + // 二进制状态位 + // 第一位为 1 则是 admin field.Int("state"), field.Int64("reg_time"), } diff --git a/frontend/package.json b/frontend/package.json index 25ba0fc..607d6f0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,8 @@ "jotai": "^2.4.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.16.0" + "react-router-dom": "^6.16.0", + "tilg": "^0.1.1" }, "devDependencies": { "@types/node": "^20.6.2", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 89e5e7f..cec0839 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -38,6 +38,9 @@ dependencies: react-router-dom: specifier: ^6.16.0 version: 6.16.0(react-dom@18.2.0)(react@18.2.0) + tilg: + specifier: ^0.1.1 + version: 0.1.1(react@18.2.0) devDependencies: '@types/node': @@ -2107,6 +2110,14 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /tilg@0.1.1(react@18.2.0): + resolution: {integrity: sha512-0uHyTAUM0tJL792LeviRPFkJtCbF6Za3/hbbnRmWGUaicOhbJ0IpvBViXiXTF7nk6R0L6vve2XLesQzn5jEVng==} + peerDependencies: + react: ^18.0.0 || ^17.0.0 + dependencies: + react: 18.2.0 + dev: false + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} diff --git a/frontend/src/apis/apis.ts b/frontend/src/apis/apis.ts index c44c560..d5c5a3f 100644 --- a/frontend/src/apis/apis.ts +++ b/frontend/src/apis/apis.ts @@ -1,5 +1,5 @@ -import type { tokenData, ApiUser } from '@/apis/model' -import { apiWrapGet } from '@/apis/utils' +import type { tokenData, ApiUser, ApiServerInfo } from '@/apis/model' +import { apiGet } from '@/apis/utils' export async function login(username: string, password: string) { const v = await fetch(import.meta.env.VITE_APIADDR + "/api/yggdrasil/authserver/authenticate", { @@ -11,7 +11,7 @@ export async function login(username: string, password: string) { }) const data = await v.json() if (!v.ok) { - throw data?.errorMessage + throw new Error(data?.errorMessage) } return data as tokenData } @@ -26,7 +26,7 @@ export async function register(email: string, username: string, password: string "CaptchaToken": captchaToken }) }) - return await apiWrapGet(v) + return await apiGet(v) } export async function userInfo(token: string) { @@ -36,6 +36,11 @@ export async function userInfo(token: string) { "Authorization": "Bearer " + token } }) - return await apiWrapGet(v) + return await apiGet(v) } + +export async function serverInfo() { + const v = await fetch(import.meta.env.VITE_APIADDR + "/api/yggdrasil") + return await v.json() as ApiServerInfo +} \ No newline at end of file diff --git a/frontend/src/apis/error.ts b/frontend/src/apis/error.ts new file mode 100644 index 0000000..58c962b --- /dev/null +++ b/frontend/src/apis/error.ts @@ -0,0 +1,8 @@ +export class ApiErr extends Error { + readonly code: number + + constructor(code: number, msg: string) { + super(msg) + this.code = code + } +} \ No newline at end of file diff --git a/frontend/src/apis/model.ts b/frontend/src/apis/model.ts index 3c62e73..7c2a650 100644 --- a/frontend/src/apis/model.ts +++ b/frontend/src/apis/model.ts @@ -21,9 +21,14 @@ interface captcha { export type ApiCaptcha = Api -interface user { +export interface ApiUser { uid: string uuid: string + is_admin: boolean } -export type ApiUser = Api +export interface ApiServerInfo { + meta: { + serverName: string + } +} diff --git a/frontend/src/apis/utils.ts b/frontend/src/apis/utils.ts index 6737959..be66411 100644 --- a/frontend/src/apis/utils.ts +++ b/frontend/src/apis/utils.ts @@ -1,7 +1,10 @@ -export async function apiWrapGet(v: Response) { - const data = await v.json() +import { ApiErr } from "./error" + +export async function apiGet(v: Response) { + type api = { data: T, msg: string, code: number } + const data = await v.json() as api if (!v.ok) { - throw data.msg + throw new ApiErr(data.code, data.msg) } - return data as T + return data.data } diff --git a/frontend/src/views/Layout.tsx b/frontend/src/views/Layout.tsx index e57ea2f..e2cf0f5 100644 --- a/frontend/src/views/Layout.tsx +++ b/frontend/src/views/Layout.tsx @@ -20,10 +20,19 @@ import { Outlet } from 'react-router-dom'; import { AccountCircle } from '@mui/icons-material'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; -import { user } from '@/store/store'; -import { useAtom } from 'jotai'; +import { token, user } from '@/store/store'; +import { useAtom, useAtomValue } from 'jotai'; import Button from '@mui/material/Button'; import { useNavigate } from "react-router-dom"; +import { useRequest, useMemoizedFn } from 'ahooks'; +import { serverInfo, userInfo } from '@/apis/apis' +import Snackbar from '@mui/material/Snackbar'; +import Alert from '@mui/material/Alert'; +import { memo } from 'react'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import useTilg from 'tilg' +import { ApiErr } from '@/apis/error'; +import Typography from '@mui/material/Typography'; const drawerWidth = 240; @@ -40,114 +49,181 @@ const DrawerHeader = styled('div')(({ theme }) => ({ export default function Layout() { const theme = useTheme(); + const isLg = useMediaQuery(theme.breakpoints.up('lg')) const [open, setOpen] = React.useState(false); - const [anchorEl, setAnchorEl] = React.useState(null); - const [nowUser, setNowUser] = useAtom(user) + const nowToken = useAtomValue(token) + const [err, setErr] = React.useState(""); const navigate = useNavigate(); - const handleLogOut = () => { - setAnchorEl(null); - setNowUser({ name: "", uuid: "" }) - }; + const userinfo = useRequest(() => userInfo(nowToken), { + refreshDeps: [nowToken], + cacheKey: "/api/v1/user", + cacheTime: 10000, + onError: e => { + if (e instanceof ApiErr && e.code == 5) { + navigate("/login") + } + console.warn(e) + setErr(String(e)) + } + }) + + useTilg(isLg, open) return (<> - - - + + + + + {userinfo.data && ( + setOpen(false)} + > + + setOpen(false)}> + {theme.direction === 'ltr' ? : } + + + + + {['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => ( + + + + {index % 2 === 0 ? : } + + + + + ))} + + {userinfo.data?.is_admin && ( + <> + + + {['All mail', 'Trash', 'Spam'].map((text, index) => ( + + + + {index % 2 === 0 ? : } + + + + + ))} + + )} + + )} + setErr("")} > + setErr("")} severity="error">{err} + + + + + + ) +} + +const MyToolbar = memo((p: { setOpen: (v: boolean) => void }) => { + const [nowUser, setNowUser] = useAtom(user) + const [anchorEl, setAnchorEl] = React.useState(null); + const navigate = useNavigate(); + const [, setToken] = useAtom(token) + + const server = useRequest(serverInfo, { + cacheKey: "/api/yggdrasil", + cacheTime: 100000, + onError: e => { + console.warn(e) + } + }) + + + + const handleLogOut = useMemoizedFn(() => { + setAnchorEl(null); + setNowUser({ name: "", uuid: "" }) + setToken("") + navigate("/") + }) + + + + return ( + <> + + {nowUser.name != "" && (<> setOpen(true)} + sx={{ mr: 2, display: { lg: 'none' } }} + onClick={() => p.setOpen(true)} > - - {nowUser.name != "" && ( -
- setAnchorEl(event.currentTarget)} - color="inherit" - > - - - setAnchorEl(null)} - > - 登出 - -
- )} - {nowUser.name == "" && ( - - )} -
-
-
- - - setOpen(false)}> - {theme.direction === 'ltr' ? : } - - - - - {['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => ( - - - - {index % 2 === 0 ? : } - - - - - ))} - - - - {['All mail', 'Trash', 'Spam'].map((text, index) => ( - - - - {index % 2 === 0 ? : } - - - - - ))} - - - - ) -} \ No newline at end of file + ) + } + + {server.data?.meta.serverName ?? "皮肤站"} + + {nowUser.name != "" && ( +
+ setAnchorEl(event.currentTarget)} + color="inherit" + > + + + setAnchorEl(null)} + > + 登出 + +
+ )} + {nowUser.name == "" && ( + + )} + + ) +}) diff --git a/frontend/src/views/Login.tsx b/frontend/src/views/Login.tsx index afc8e2c..20b276d 100644 --- a/frontend/src/views/Login.tsx +++ b/frontend/src/views/Login.tsx @@ -50,7 +50,7 @@ export default function SignIn() { uuid: v.selectedProfile.uuid, name: v.selectedProfile.name, }) - navigate("/user") + navigate("/") }). catch(v => [setErr(String(v)), console.warn(v)]). finally(() => setLoading(false)) diff --git a/handle/yggdrasil/yggdrasil.go b/handle/yggdrasil/yggdrasil.go index f9c2949..468fe7b 100644 --- a/handle/yggdrasil/yggdrasil.go +++ b/handle/yggdrasil/yggdrasil.go @@ -61,13 +61,16 @@ func (y *Yggdrasil) YggdrasilRoot() httprouter.Handle { return h, err }, r.Host) } + homepage, _ := url.JoinPath(y.config.WebBaseUrl, "/login") + register, _ := url.JoinPath(y.config.WebBaseUrl, "/register") + w.Write(lo.Must1(json.Marshal(yggdrasilM.Yggdrasil{ Meta: yggdrasilM.YggdrasilMeta{ ImplementationName: "authlib-skin", ImplementationVersion: "0.0.1", Links: yggdrasilM.YggdrasilMetaLinks{ - Homepage: y.config.HomepageUrl, - Register: y.config.RegisterUrl, + Homepage: homepage, + Register: register, }, ServerName: y.config.ServerName, EnableProfileKey: true, diff --git a/model/model.go b/model/model.go index 9bc94c9..5c7edf9 100644 --- a/model/model.go +++ b/model/model.go @@ -31,6 +31,7 @@ type Captcha struct { } type UserInfo struct { - UID int `json:"uid"` - UUID string `json:"uuid"` + UID int `json:"uid"` + UUID string `json:"uuid"` + IsAdmin bool `json:"is_admin"` } diff --git a/service/user.go b/service/user.go index e9933d3..cb47e53 100644 --- a/service/user.go +++ b/service/user.go @@ -96,8 +96,17 @@ func (w *WebService) Info(ctx context.Context, token string) (model.UserInfo, er if err != nil { return model.UserInfo{}, fmt.Errorf("Info: %w", err) } + u, err := w.client.User.Query().Where(user.ID(t.UID)).First(ctx) + if err != nil { + return model.UserInfo{}, fmt.Errorf("Info: %w", err) + } + isAdmin := false + if u.State&1 == 1 { + isAdmin = true + } return model.UserInfo{ - UID: t.UID, - UUID: t.Subject, + UID: t.UID, + UUID: t.Subject, + IsAdmin: isAdmin, }, nil }