侧栏部分完成
This commit is contained in:
parent
72fa9876a7
commit
5bbe89a3a6
@ -20,8 +20,7 @@ type Config struct {
|
|||||||
RsaPriKey string
|
RsaPriKey string
|
||||||
TexturePath string
|
TexturePath string
|
||||||
TextureBaseUrl string
|
TextureBaseUrl string
|
||||||
HomepageUrl string
|
WebBaseUrl string
|
||||||
RegisterUrl string
|
|
||||||
ServerName string
|
ServerName string
|
||||||
|
|
||||||
Captcha struct {
|
Captcha struct {
|
||||||
|
@ -28,7 +28,8 @@ func (User) Fields() []ent.Field {
|
|||||||
field.String("reg_ip").SchemaType(map[string]string{
|
field.String("reg_ip").SchemaType(map[string]string{
|
||||||
dialect.MySQL: "VARCHAR(32)",
|
dialect.MySQL: "VARCHAR(32)",
|
||||||
}),
|
}),
|
||||||
// 二进制状态位,保留
|
// 二进制状态位
|
||||||
|
// 第一位为 1 则是 admin
|
||||||
field.Int("state"),
|
field.Int("state"),
|
||||||
field.Int64("reg_time"),
|
field.Int64("reg_time"),
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,8 @@
|
|||||||
"jotai": "^2.4.2",
|
"jotai": "^2.4.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^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": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.6.2",
|
"@types/node": "^20.6.2",
|
||||||
|
11
frontend/pnpm-lock.yaml
generated
11
frontend/pnpm-lock.yaml
generated
@ -38,6 +38,9 @@ dependencies:
|
|||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: ^6.16.0
|
specifier: ^6.16.0
|
||||||
version: 6.16.0(react-dom@18.2.0)(react@18.2.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:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
@ -2107,6 +2110,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
dev: true
|
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:
|
/to-fast-properties@2.0.0:
|
||||||
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { tokenData, ApiUser } from '@/apis/model'
|
import type { tokenData, ApiUser, ApiServerInfo } from '@/apis/model'
|
||||||
import { apiWrapGet } from '@/apis/utils'
|
import { apiGet } from '@/apis/utils'
|
||||||
|
|
||||||
export async function login(username: string, password: string) {
|
export async function login(username: string, password: string) {
|
||||||
const v = await fetch(import.meta.env.VITE_APIADDR + "/api/yggdrasil/authserver/authenticate", {
|
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()
|
const data = await v.json()
|
||||||
if (!v.ok) {
|
if (!v.ok) {
|
||||||
throw data?.errorMessage
|
throw new Error(data?.errorMessage)
|
||||||
}
|
}
|
||||||
return data as tokenData
|
return data as tokenData
|
||||||
}
|
}
|
||||||
@ -26,7 +26,7 @@ export async function register(email: string, username: string, password: string
|
|||||||
"CaptchaToken": captchaToken
|
"CaptchaToken": captchaToken
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return await apiWrapGet(v)
|
return await apiGet(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function userInfo(token: string) {
|
export async function userInfo(token: string) {
|
||||||
@ -36,6 +36,11 @@ export async function userInfo(token: string) {
|
|||||||
"Authorization": "Bearer " + token
|
"Authorization": "Bearer " + token
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return await apiWrapGet<ApiUser>(v)
|
return await apiGet<ApiUser>(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function serverInfo() {
|
||||||
|
const v = await fetch(import.meta.env.VITE_APIADDR + "/api/yggdrasil")
|
||||||
|
return await v.json() as ApiServerInfo
|
||||||
|
}
|
8
frontend/src/apis/error.ts
Normal file
8
frontend/src/apis/error.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export class ApiErr extends Error {
|
||||||
|
readonly code: number
|
||||||
|
|
||||||
|
constructor(code: number, msg: string) {
|
||||||
|
super(msg)
|
||||||
|
this.code = code
|
||||||
|
}
|
||||||
|
}
|
@ -21,9 +21,14 @@ interface captcha {
|
|||||||
|
|
||||||
export type ApiCaptcha = Api<captcha>
|
export type ApiCaptcha = Api<captcha>
|
||||||
|
|
||||||
interface user {
|
export interface ApiUser {
|
||||||
uid: string
|
uid: string
|
||||||
uuid: string
|
uuid: string
|
||||||
|
is_admin: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiUser = Api<user>
|
export interface ApiServerInfo {
|
||||||
|
meta: {
|
||||||
|
serverName: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
export async function apiWrapGet<T>(v: Response) {
|
import { ApiErr } from "./error"
|
||||||
const data = await v.json()
|
|
||||||
|
export async function apiGet<T>(v: Response) {
|
||||||
|
type api = { data: T, msg: string, code: number }
|
||||||
|
const data = await v.json() as api
|
||||||
if (!v.ok) {
|
if (!v.ok) {
|
||||||
throw data.msg
|
throw new ApiErr(data.code, data.msg)
|
||||||
}
|
}
|
||||||
return data as T
|
return data.data
|
||||||
}
|
}
|
||||||
|
@ -20,10 +20,19 @@ import { Outlet } from 'react-router-dom';
|
|||||||
import { AccountCircle } from '@mui/icons-material';
|
import { AccountCircle } from '@mui/icons-material';
|
||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import { user } from '@/store/store';
|
import { token, user } from '@/store/store';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { useNavigate } from "react-router-dom";
|
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;
|
const drawerWidth = 240;
|
||||||
@ -40,32 +49,147 @@ const DrawerHeader = styled('div')(({ theme }) => ({
|
|||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const isLg = useMediaQuery(theme.breakpoints.up('lg'))
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
const nowToken = useAtomValue(token)
|
||||||
const [nowUser, setNowUser] = useAtom(user)
|
const [err, setErr] = React.useState("");
|
||||||
const navigate = useNavigate();
|
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 (<>
|
return (<>
|
||||||
<Box sx={{ flexGrow: 1 }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<AppBar position="static">
|
<AppBar position="fixed"
|
||||||
|
sx={{
|
||||||
|
zIndex: { lg: theme.zIndex.drawer + 1 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MyToolbar setOpen={setOpen}></MyToolbar>
|
||||||
|
</AppBar>
|
||||||
|
{userinfo.data && (
|
||||||
|
<Drawer
|
||||||
|
sx={{
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
width: drawerWidth,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
variant={isLg ? "persistent" : "temporary"}
|
||||||
|
anchor="left"
|
||||||
|
open={open || isLg}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<DrawerHeader>
|
||||||
|
<IconButton onClick={() => setOpen(false)}>
|
||||||
|
{theme.direction === 'ltr' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</DrawerHeader>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
{['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
|
||||||
|
<ListItem key={text} disablePadding>
|
||||||
|
<ListItemButton>
|
||||||
|
<ListItemIcon>
|
||||||
|
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={text} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
{userinfo.data?.is_admin && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
{['All mail', 'Trash', 'Spam'].map((text, index) => (
|
||||||
|
<ListItem key={text} disablePadding>
|
||||||
|
<ListItemButton>
|
||||||
|
<ListItemIcon>
|
||||||
|
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={text} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</>)}
|
||||||
|
</Drawer>
|
||||||
|
)}
|
||||||
|
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={err != ""} onClose={() => setErr("")} >
|
||||||
|
<Alert onClose={() => setErr("")} severity="error">{err}</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
<Box
|
||||||
|
component="main"
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1, bgcolor: 'background.default', p: 3
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Outlet />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MyToolbar = memo((p: { setOpen: (v: boolean) => void }) => {
|
||||||
|
const [nowUser, setNowUser] = useAtom(user)
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(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 (
|
||||||
|
<>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
|
{nowUser.name != "" && (<>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="large"
|
size="large"
|
||||||
edge="start"
|
edge="start"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="menu"
|
aria-label="menu"
|
||||||
sx={{ mr: 2 }}
|
sx={{ mr: 2, display: { lg: 'none' } }}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => p.setOpen(true)}
|
||||||
>
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton >
|
</IconButton >
|
||||||
<Box sx={{ flexGrow: 1 }}></Box>
|
</>)
|
||||||
|
}
|
||||||
|
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||||
|
{server.data?.meta.serverName ?? "皮肤站"}
|
||||||
|
</Typography>
|
||||||
{nowUser.name != "" && (
|
{nowUser.name != "" && (
|
||||||
<div>
|
<div>
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -101,53 +225,5 @@ export default function Layout() {
|
|||||||
<Button color="inherit" onClick={() => navigate("/login")} >登录</Button>
|
<Button color="inherit" onClick={() => navigate("/login")} >登录</Button>
|
||||||
)}
|
)}
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
|
||||||
</Box>
|
|
||||||
<Drawer
|
|
||||||
sx={{
|
|
||||||
width: drawerWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
'& .MuiDrawer-paper': {
|
|
||||||
width: drawerWidth,
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
variant="persistent"
|
|
||||||
anchor="left"
|
|
||||||
open={open}
|
|
||||||
>
|
|
||||||
<DrawerHeader>
|
|
||||||
<IconButton onClick={() => setOpen(false)}>
|
|
||||||
{theme.direction === 'ltr' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
|
|
||||||
</IconButton>
|
|
||||||
</DrawerHeader>
|
|
||||||
<Divider />
|
|
||||||
<List>
|
|
||||||
{['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
|
|
||||||
<ListItem key={text} disablePadding>
|
|
||||||
<ListItemButton>
|
|
||||||
<ListItemIcon>
|
|
||||||
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary={text} />
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
<Divider />
|
|
||||||
<List>
|
|
||||||
{['All mail', 'Trash', 'Spam'].map((text, index) => (
|
|
||||||
<ListItem key={text} disablePadding>
|
|
||||||
<ListItemButton>
|
|
||||||
<ListItemIcon>
|
|
||||||
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary={text} />
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Drawer>
|
|
||||||
<Outlet />
|
|
||||||
</>)
|
</>)
|
||||||
}
|
})
|
||||||
|
@ -50,7 +50,7 @@ export default function SignIn() {
|
|||||||
uuid: v.selectedProfile.uuid,
|
uuid: v.selectedProfile.uuid,
|
||||||
name: v.selectedProfile.name,
|
name: v.selectedProfile.name,
|
||||||
})
|
})
|
||||||
navigate("/user")
|
navigate("/")
|
||||||
}).
|
}).
|
||||||
catch(v => [setErr(String(v)), console.warn(v)]).
|
catch(v => [setErr(String(v)), console.warn(v)]).
|
||||||
finally(() => setLoading(false))
|
finally(() => setLoading(false))
|
||||||
|
@ -61,13 +61,16 @@ func (y *Yggdrasil) YggdrasilRoot() httprouter.Handle {
|
|||||||
return h, err
|
return h, err
|
||||||
}, r.Host)
|
}, r.Host)
|
||||||
}
|
}
|
||||||
|
homepage, _ := url.JoinPath(y.config.WebBaseUrl, "/login")
|
||||||
|
register, _ := url.JoinPath(y.config.WebBaseUrl, "/register")
|
||||||
|
|
||||||
w.Write(lo.Must1(json.Marshal(yggdrasilM.Yggdrasil{
|
w.Write(lo.Must1(json.Marshal(yggdrasilM.Yggdrasil{
|
||||||
Meta: yggdrasilM.YggdrasilMeta{
|
Meta: yggdrasilM.YggdrasilMeta{
|
||||||
ImplementationName: "authlib-skin",
|
ImplementationName: "authlib-skin",
|
||||||
ImplementationVersion: "0.0.1",
|
ImplementationVersion: "0.0.1",
|
||||||
Links: yggdrasilM.YggdrasilMetaLinks{
|
Links: yggdrasilM.YggdrasilMetaLinks{
|
||||||
Homepage: y.config.HomepageUrl,
|
Homepage: homepage,
|
||||||
Register: y.config.RegisterUrl,
|
Register: register,
|
||||||
},
|
},
|
||||||
ServerName: y.config.ServerName,
|
ServerName: y.config.ServerName,
|
||||||
EnableProfileKey: true,
|
EnableProfileKey: true,
|
||||||
|
@ -33,4 +33,5 @@ type Captcha struct {
|
|||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
UID int `json:"uid"`
|
UID int `json:"uid"`
|
||||||
UUID string `json:"uuid"`
|
UUID string `json:"uuid"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
}
|
}
|
||||||
|
@ -96,8 +96,17 @@ func (w *WebService) Info(ctx context.Context, token string) (model.UserInfo, er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return model.UserInfo{}, fmt.Errorf("Info: %w", err)
|
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{
|
return model.UserInfo{
|
||||||
UID: t.UID,
|
UID: t.UID,
|
||||||
UUID: t.Subject,
|
UUID: t.Subject,
|
||||||
|
IsAdmin: isAdmin,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user