mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-30 21:31:46 +00:00
🔐 feat(auth): integrate backend session login + 2FA; add HTTP client and route guards
- add lib/http.ts: unified fetch wrapper with: - 30s timeout via AbortController - JSON/text parsing and normalized HttpError - Authorization header from cookie - New-Api-User header from persisted user id - get/post/put/patch/del helpers - add types/api.ts: ApiResponse, UserBasic, UserSelf, LoginResponse, Verify2FAResponse, SelfResponse - add lib/auth.ts: persist/restore user in localStorage (get/set/clear helpers) - refactor stores/auth-store.ts: - implement login (POST /api/user/login?turnstile=...), 2FA verify (POST /api/user/login/2fa) - implement fetchSelf (GET /api/user/self), logout (GET /api/user/logout) - persist user on successful login/verify/self - refactor auth/sign-in form: - switch field to "username" (supports username or email), loosen validation - call auth.login, handle 2FA branch → redirect to /otp - refactor OTP form: - call auth.verify2FA; on success, toast + redirect to home - add route guards: - /_authenticated: beforeLoad redirect to /sign-in if no user; silently refresh session via auth.fetchSelf - /(auth)/sign-in: beforeLoad redirect to / if already logged in - update sign-out dialog: use auth.logout and preserve redirect - cleanup: remove mock auth, no direct axios/fetch usage outside http.ts, no Semi UI imports in new code Notes - Backend expects username or email in "username" field. - Turnstile support: pass token via ?turnstile=... during login when enabled. Security - Ensure New-Api-User header matches logged-in user id per backend middleware. - Session-based auth with optional Bearer token via cookie. No breaking changes.
This commit is contained in:
@@ -13,7 +13,7 @@ export function SignOutDialog({ open, onOpenChange }: SignOutDialogProps) {
|
|||||||
const { auth } = useAuthStore()
|
const { auth } = useAuthStore()
|
||||||
|
|
||||||
const handleSignOut = () => {
|
const handleSignOut = () => {
|
||||||
auth.reset()
|
auth.logout()
|
||||||
// Preserve current location for redirect after sign-in
|
// Preserve current location for redirect after sign-in
|
||||||
const currentPath = location.href
|
const currentPath = location.href
|
||||||
navigate({
|
navigate({
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { z } from 'zod'
|
|||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
import { showSubmittedData } from '@/lib/show-submitted-data'
|
import { toast } from 'sonner'
|
||||||
|
import { useAuthStore } from '@/stores/auth-store'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
@@ -41,14 +42,19 @@ export function OtpForm({ className, ...props }: OtpFormProps) {
|
|||||||
|
|
||||||
const otp = form.watch('otp')
|
const otp = form.watch('otp')
|
||||||
|
|
||||||
function onSubmit(data: z.infer<typeof formSchema>) {
|
const { auth } = useAuthStore()
|
||||||
setIsLoading(true)
|
|
||||||
showSubmittedData(data)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
async function onSubmit(data: z.infer<typeof formSchema>) {
|
||||||
setIsLoading(false)
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
await auth.verify2FA(data.otp)
|
||||||
|
toast.success('登录成功')
|
||||||
navigate({ to: '/' })
|
navigate({ to: '/' })
|
||||||
}, 1000)
|
} catch (e: any) {
|
||||||
|
toast.error(e?.message || '验证失败,请重试')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Loader2, LogIn } from 'lucide-react'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { IconFacebook, IconGithub } from '@/assets/brand-icons'
|
import { IconFacebook, IconGithub } from '@/assets/brand-icons'
|
||||||
import { useAuthStore } from '@/stores/auth-store'
|
import { useAuthStore } from '@/stores/auth-store'
|
||||||
import { sleep, cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -21,13 +21,8 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { PasswordInput } from '@/components/password-input'
|
import { PasswordInput } from '@/components/password-input'
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.email({
|
username: z.string().min(1, 'Please enter your username or email'),
|
||||||
error: (iss) => (iss.input === '' ? 'Please enter your email' : undefined),
|
password: z.string().min(1, 'Please enter your password'),
|
||||||
}),
|
|
||||||
password: z
|
|
||||||
.string()
|
|
||||||
.min(1, 'Please enter your password')
|
|
||||||
.min(7, 'Password must be at least 7 characters long'),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
interface UserAuthFormProps extends React.HTMLAttributes<HTMLFormElement> {
|
interface UserAuthFormProps extends React.HTMLAttributes<HTMLFormElement> {
|
||||||
@@ -45,40 +40,29 @@ export function UserAuthForm({
|
|||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: { username: '', password: '' },
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function onSubmit(data: z.infer<typeof formSchema>) {
|
async function onSubmit(data: z.infer<typeof formSchema>) {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
// Mock successful authentication
|
const res = await auth.login({
|
||||||
const mockUser = {
|
username: data.username,
|
||||||
accountNo: 'ACC001',
|
password: data.password,
|
||||||
email: data.email,
|
})
|
||||||
role: ['user'],
|
if (res.require2FA) {
|
||||||
exp: Date.now() + 24 * 60 * 60 * 1000, // 24 hours from now
|
toast.success('请输入两步验证码')
|
||||||
|
navigate({ to: '/otp', replace: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toast.success(`Welcome back, ${data.username}!`)
|
||||||
|
const targetPath = redirectTo || '/'
|
||||||
|
navigate({ to: targetPath, replace: true })
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error(e?.message || 'Sign in failed')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.promise(sleep(2000), {
|
|
||||||
loading: 'Signing in...',
|
|
||||||
success: () => {
|
|
||||||
setIsLoading(false)
|
|
||||||
|
|
||||||
// Set user and access token
|
|
||||||
auth.setUser(mockUser)
|
|
||||||
auth.setAccessToken('mock-access-token')
|
|
||||||
|
|
||||||
// Redirect to the stored location or default to dashboard
|
|
||||||
const targetPath = redirectTo || '/'
|
|
||||||
navigate({ to: targetPath, replace: true })
|
|
||||||
|
|
||||||
return `Welcome back, ${data.email}!`
|
|
||||||
},
|
|
||||||
error: 'Error',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -90,12 +74,12 @@ export function UserAuthForm({
|
|||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='email'
|
name='username'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Email</FormLabel>
|
<FormLabel>Username or Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder='name@example.com' {...field} />
|
<Input placeholder='your username or email' {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
31
web/src/lib/auth.ts
Normal file
31
web/src/lib/auth.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { User } from '@/types/api'
|
||||||
|
|
||||||
|
const LS_USER_KEY = 'user'
|
||||||
|
|
||||||
|
export function getStoredUser(): User | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LS_USER_KEY)
|
||||||
|
return raw ? (JSON.parse(raw) as User) : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setStoredUser(user: User | null) {
|
||||||
|
try {
|
||||||
|
if (user) localStorage.setItem(LS_USER_KEY, JSON.stringify(user))
|
||||||
|
else localStorage.removeItem(LS_USER_KEY)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStoredUserId(): number | undefined {
|
||||||
|
const user = getStoredUser()
|
||||||
|
if (!user) return undefined
|
||||||
|
return (user as any).id as number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearStoredUser() {
|
||||||
|
setStoredUser(null)
|
||||||
|
}
|
||||||
121
web/src/lib/http.ts
Normal file
121
web/src/lib/http.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// 统一 http 封装:超时、JSON、错误语义、鉴权头
|
||||||
|
import { getStoredUserId } from '@/lib/auth'
|
||||||
|
import { getCookie } from '@/lib/cookies'
|
||||||
|
|
||||||
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||||
|
|
||||||
|
type JsonBody = Record<string, unknown> | Array<unknown> | undefined
|
||||||
|
|
||||||
|
export interface HttpError {
|
||||||
|
status: number
|
||||||
|
message: string
|
||||||
|
details?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
async function safeMessage(res: Response): Promise<string> {
|
||||||
|
try {
|
||||||
|
const data = await res.clone().json()
|
||||||
|
if (typeof data?.message === 'string') return data.message
|
||||||
|
return JSON.stringify(data)
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
return await res.clone().text()
|
||||||
|
} catch {
|
||||||
|
return res.statusText || 'Request failed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_TIMEOUT_MS = 30000
|
||||||
|
const ACCESS_TOKEN = 'thisisjustarandomstring'
|
||||||
|
|
||||||
|
export async function http<T>(
|
||||||
|
input: RequestInfo | URL,
|
||||||
|
init: RequestInit & { timeoutMs?: number; asText?: boolean } = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeout = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
init.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
||||||
|
)
|
||||||
|
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(init.headers || {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 cookie 读取 token,并自动附带
|
||||||
|
const token = getCookie(ACCESS_TOKEN)
|
||||||
|
if (token) {
|
||||||
|
;(headers as Record<string, string>)['Authorization'] =
|
||||||
|
`Bearer ${JSON.parse(token)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 携带 New-Api-User 以通过后端鉴权
|
||||||
|
const userId = getStoredUserId()
|
||||||
|
if (typeof userId === 'number' && userId > 0) {
|
||||||
|
;(headers as Record<string, string>)['New-Api-User'] = String(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers,
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = await safeMessage(response)
|
||||||
|
const error: HttpError = { status: response.status, message }
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (init.asText) {
|
||||||
|
return (await response.text()) as unknown as T
|
||||||
|
}
|
||||||
|
// 缺省按 JSON 解析
|
||||||
|
return (await response.json()) as T
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get<T>(url: string, init?: RequestInit) {
|
||||||
|
return http<T>(url, { ...init, method: 'GET' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function post<T>(
|
||||||
|
url: string,
|
||||||
|
body?: JsonBody,
|
||||||
|
init?: RequestInit
|
||||||
|
) {
|
||||||
|
return http<T>(url, {
|
||||||
|
...init,
|
||||||
|
method: 'POST',
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function put<T>(url: string, body?: JsonBody, init?: RequestInit) {
|
||||||
|
return http<T>(url, {
|
||||||
|
...init,
|
||||||
|
method: 'PUT',
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function patch<T>(
|
||||||
|
url: string,
|
||||||
|
body?: JsonBody,
|
||||||
|
init?: RequestInit
|
||||||
|
) {
|
||||||
|
return http<T>(url, {
|
||||||
|
...init,
|
||||||
|
method: 'PATCH',
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function del<T>(url: string, init?: RequestInit) {
|
||||||
|
return http<T>(url, { ...init, method: 'DELETE' })
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||||
|
import { getStoredUser } from '@/lib/auth'
|
||||||
import { SignIn } from '@/features/auth/sign-in'
|
import { SignIn } from '@/features/auth/sign-in'
|
||||||
|
|
||||||
const searchSchema = z.object({
|
const searchSchema = z.object({
|
||||||
@@ -7,6 +8,10 @@ const searchSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const Route = createFileRoute('/(auth)/sign-in')({
|
export const Route = createFileRoute('/(auth)/sign-in')({
|
||||||
|
beforeLoad: () => {
|
||||||
|
const user = getStoredUser()
|
||||||
|
if (user) throw redirect({ to: '/' })
|
||||||
|
},
|
||||||
component: SignIn,
|
component: SignIn,
|
||||||
validateSearch: searchSchema,
|
validateSearch: searchSchema,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||||
|
import { useAuthStore } from '@/stores/auth-store'
|
||||||
|
import { getStoredUser } from '@/lib/auth'
|
||||||
import { AuthenticatedLayout } from '@/components/layout/authenticated-layout'
|
import { AuthenticatedLayout } from '@/components/layout/authenticated-layout'
|
||||||
|
|
||||||
export const Route = createFileRoute('/_authenticated')({
|
export const Route = createFileRoute('/_authenticated')({
|
||||||
component: AuthenticatedLayout,
|
beforeLoad: async ({ location }) => {
|
||||||
|
const user = getStoredUser()
|
||||||
|
if (!user) {
|
||||||
|
throw redirect({ to: '/sign-in', search: { redirect: location.href } })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
component: () => {
|
||||||
|
// 进入后尝试刷新一次会话信息(静默失败)
|
||||||
|
const { auth } = useAuthStore()
|
||||||
|
auth.fetchSelf().catch(() => {})
|
||||||
|
return <AuthenticatedLayout />
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
|
import type {
|
||||||
|
LoginResponse,
|
||||||
|
Verify2FAResponse,
|
||||||
|
SelfResponse,
|
||||||
|
UserBasic,
|
||||||
|
} from '@/types/api'
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
|
import { clearStoredUser, setStoredUser } from '@/lib/auth'
|
||||||
import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
|
import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
|
||||||
|
import { get, post } from '@/lib/http'
|
||||||
|
|
||||||
const ACCESS_TOKEN = 'thisisjustarandomstring'
|
const ACCESS_TOKEN = 'thisisjustarandomstring'
|
||||||
|
|
||||||
@@ -17,6 +25,14 @@ interface AuthState {
|
|||||||
accessToken: string
|
accessToken: string
|
||||||
setAccessToken: (accessToken: string) => void
|
setAccessToken: (accessToken: string) => void
|
||||||
resetAccessToken: () => void
|
resetAccessToken: () => void
|
||||||
|
login: (payload: {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
turnstile?: string
|
||||||
|
}) => Promise<{ require2FA?: boolean }>
|
||||||
|
verify2FA: (code: string) => Promise<void>
|
||||||
|
fetchSelf: () => Promise<void>
|
||||||
|
logout: () => Promise<void>
|
||||||
reset: () => void
|
reset: () => void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,9 +56,86 @@ export const useAuthStore = create<AuthState>()((set) => {
|
|||||||
removeCookie(ACCESS_TOKEN)
|
removeCookie(ACCESS_TOKEN)
|
||||||
return { ...state, auth: { ...state.auth, accessToken: '' } }
|
return { ...state, auth: { ...state.auth, accessToken: '' } }
|
||||||
}),
|
}),
|
||||||
|
login: async ({ username, password, turnstile }) => {
|
||||||
|
const qs = turnstile
|
||||||
|
? `?turnstile=${encodeURIComponent(turnstile)}`
|
||||||
|
: ''
|
||||||
|
const res = await post<LoginResponse>(`/api/user/login${qs}`, {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
if (!res.success) throw new Error(res.message || '登录失败')
|
||||||
|
const data = res.data
|
||||||
|
if (data && (data as any).require_2fa) {
|
||||||
|
return { require2FA: true }
|
||||||
|
}
|
||||||
|
const user = data as UserBasic
|
||||||
|
setStoredUser(user)
|
||||||
|
set((state) => ({
|
||||||
|
...state,
|
||||||
|
auth: {
|
||||||
|
...state.auth,
|
||||||
|
user: {
|
||||||
|
accountNo: String(user.id),
|
||||||
|
email: user.username,
|
||||||
|
role: ['user'],
|
||||||
|
exp: Date.now() + 24 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
verify2FA: async (code: string) => {
|
||||||
|
const res = await post<Verify2FAResponse>('/api/user/login/2fa', {
|
||||||
|
code,
|
||||||
|
})
|
||||||
|
if (!res.success) throw new Error(res.message || '验证失败')
|
||||||
|
const user = res.data as UserBasic
|
||||||
|
setStoredUser(user)
|
||||||
|
set((state) => ({
|
||||||
|
...state,
|
||||||
|
auth: {
|
||||||
|
...state.auth,
|
||||||
|
user: {
|
||||||
|
accountNo: String(user.id),
|
||||||
|
email: user.username,
|
||||||
|
role: ['user'],
|
||||||
|
exp: Date.now() + 24 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
fetchSelf: async () => {
|
||||||
|
const res = await get<SelfResponse>('/api/user/self')
|
||||||
|
if (!res.success) return
|
||||||
|
const user = res.data!
|
||||||
|
setStoredUser(user)
|
||||||
|
set((state) => ({
|
||||||
|
...state,
|
||||||
|
auth: {
|
||||||
|
...state.auth,
|
||||||
|
user: {
|
||||||
|
accountNo: String(user.id),
|
||||||
|
email: (user as any).email || user.username,
|
||||||
|
role: ['user'],
|
||||||
|
exp: Date.now() + 24 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
logout: async () => {
|
||||||
|
try {
|
||||||
|
await get('/api/user/logout')
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
clearStoredUser()
|
||||||
|
set((state) => ({ ...state, auth: { ...state.auth, user: null } }))
|
||||||
|
},
|
||||||
reset: () =>
|
reset: () =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
removeCookie(ACCESS_TOKEN)
|
removeCookie(ACCESS_TOKEN)
|
||||||
|
clearStoredUser()
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
auth: { ...state.auth, user: null, accessToken: '' },
|
auth: { ...state.auth, user: null, accessToken: '' },
|
||||||
|
|||||||
43
web/src/types/api.ts
Normal file
43
web/src/types/api.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// 统一的后端响应与 DTO 类型
|
||||||
|
|
||||||
|
export type ApiResponse<T> = {
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
data?: T
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基础用户信息(登录返回)
|
||||||
|
export interface UserBasic {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
display_name?: string
|
||||||
|
role: number
|
||||||
|
status: number
|
||||||
|
group?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/user/self 返回会更丰富
|
||||||
|
export interface UserSelf extends UserBasic {
|
||||||
|
email?: string
|
||||||
|
quota?: number
|
||||||
|
used_quota?: number
|
||||||
|
request_count?: number
|
||||||
|
aff_code?: string
|
||||||
|
aff_count?: number
|
||||||
|
aff_quota?: number
|
||||||
|
aff_history_quota?: number
|
||||||
|
inviter_id?: number
|
||||||
|
linux_do_id?: string
|
||||||
|
setting?: unknown
|
||||||
|
stripe_customer?: string
|
||||||
|
sidebar_modules?: unknown
|
||||||
|
permissions?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type User = UserSelf | UserBasic
|
||||||
|
|
||||||
|
export type LoginTwoFAData = { require_2fa: true }
|
||||||
|
export type LoginSuccessData = UserBasic
|
||||||
|
export type LoginResponse = ApiResponse<LoginTwoFAData | LoginSuccessData>
|
||||||
|
export type Verify2FAResponse = ApiResponse<UserBasic>
|
||||||
|
export type SelfResponse = ApiResponse<UserSelf>
|
||||||
Reference in New Issue
Block a user