侧栏部分完成

This commit is contained in:
xmdhs 2023-10-01 21:31:56 +08:00
parent 72fa9876a7
commit 5bbe89a3a6
No known key found for this signature in database
GPG Key ID: E809D6D43DEFCC95
13 changed files with 243 additions and 121 deletions

View File

@ -20,8 +20,7 @@ type Config struct {
RsaPriKey string
TexturePath string
TextureBaseUrl string
HomepageUrl string
RegisterUrl string
WebBaseUrl string
ServerName string
Captcha struct {

View File

@ -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"),
}

View File

@ -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",

View File

@ -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'}

View File

@ -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
}

View File

@ -0,0 +1,8 @@
export class ApiErr extends Error {
readonly code: number
constructor(code: number, msg: string) {
super(msg)
this.code = code
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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>
</>)
})

View File

@ -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))

View File

@ -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,

View File

@ -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"`
}

View File

@ -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
}