侧栏部分完成
This commit is contained in:
parent
72fa9876a7
commit
5bbe89a3a6
@ -20,8 +20,7 @@ type Config struct {
|
||||
RsaPriKey string
|
||||
TexturePath string
|
||||
TextureBaseUrl string
|
||||
HomepageUrl string
|
||||
RegisterUrl string
|
||||
WebBaseUrl string
|
||||
ServerName string
|
||||
|
||||
Captcha struct {
|
||||
|
@ -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"),
|
||||
}
|
||||
|
@ -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",
|
||||
|
11
frontend/pnpm-lock.yaml
generated
11
frontend/pnpm-lock.yaml
generated
@ -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'}
|
||||
|
@ -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<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>
|
||||
|
||||
interface user {
|
||||
export interface ApiUser {
|
||||
uid: 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) {
|
||||
const data = await v.json()
|
||||
import { ApiErr } from "./error"
|
||||
|
||||
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) {
|
||||
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 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 | HTMLElement>(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 (<>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<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>
|
||||
{nowUser.name != "" && (<>
|
||||
<IconButton
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={() => setOpen(true)}
|
||||
sx={{ mr: 2, display: { lg: 'none' } }}
|
||||
onClick={() => p.setOpen(true)}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton >
|
||||
<Box sx={{ flexGrow: 1 }}></Box>
|
||||
{nowUser.name != "" && (
|
||||
<div>
|
||||
<IconButton
|
||||
size="large"
|
||||
aria-label="account of current user"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={event => setAnchorEl(event.currentTarget)}
|
||||
color="inherit"
|
||||
>
|
||||
<AccountCircle />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
>
|
||||
<MenuItem onClick={handleLogOut}>登出</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
)}
|
||||
{nowUser.name == "" && (
|
||||
<Button color="inherit" onClick={()=> navigate("/login")} >登录</Button>
|
||||
)}
|
||||
</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 />
|
||||
</>)
|
||||
}
|
||||
</>)
|
||||
}
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
{server.data?.meta.serverName ?? "皮肤站"}
|
||||
</Typography>
|
||||
{nowUser.name != "" && (
|
||||
<div>
|
||||
<IconButton
|
||||
size="large"
|
||||
aria-label="account of current user"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={event => setAnchorEl(event.currentTarget)}
|
||||
color="inherit"
|
||||
>
|
||||
<AccountCircle />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
>
|
||||
<MenuItem onClick={handleLogOut}>登出</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
)}
|
||||
{nowUser.name == "" && (
|
||||
<Button color="inherit" onClick={() => navigate("/login")} >登录</Button>
|
||||
)}
|
||||
</Toolbar>
|
||||
</>)
|
||||
})
|
||||
|
@ -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))
|
||||
|
@ -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,
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user