feat(ECR-006): 落地运营后台 Phase A(admin API + admin-h5)
新增独立鉴权的 /api/v1/admin 与 Vue 控制台;会员授予与审计同事务,并补集成/单测。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/** Thin admin API client → /api/v1/admin (proxied to Go). */
|
||||
|
||||
export type ApiEnvelope<T> = { code: number; message: string; data?: T }
|
||||
|
||||
const TOKEN_KEY = 'yuxingu_admin_token'
|
||||
|
||||
export function getToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
export function setToken(token: string | null) {
|
||||
if (token) localStorage.setItem(TOKEN_KEY, token)
|
||||
else localStorage.removeItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const headers: Record<string, string> = { Accept: 'application/json' }
|
||||
if (body !== undefined) headers['Content-Type'] = 'application/json'
|
||||
const token = getToken()
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
const res = await fetch(`/api/v1/admin${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
})
|
||||
const env = (await res.json()) as ApiEnvelope<T>
|
||||
if (!res.ok || env.code !== 0) {
|
||||
throw new Error(env.message || `HTTP ${res.status}`)
|
||||
}
|
||||
return env.data as T
|
||||
}
|
||||
|
||||
export const adminApi = {
|
||||
login: (username: string, password: string) =>
|
||||
request<{ token: string; admin: { id: string; username: string } }>('POST', '/auth/login', {
|
||||
username,
|
||||
password,
|
||||
}),
|
||||
logout: () => request<{ ok: boolean }>('POST', '/auth/logout'),
|
||||
me: () => request<{ id: string; username: string }>('GET', '/me'),
|
||||
users: (q = '') =>
|
||||
request<{ items: Array<{ id: string; status: string; created_at: string }> }>(
|
||||
'GET',
|
||||
`/users?q=${encodeURIComponent(q)}`,
|
||||
),
|
||||
user: (id: string) => request<UserDetail>('GET', `/users/${id}`),
|
||||
grant: (id: string, plan: string) =>
|
||||
request<{ ok: boolean }>('POST', `/users/${id}/membership/grant`, { plan }),
|
||||
orders: () =>
|
||||
request<{
|
||||
items: Array<{
|
||||
id: string
|
||||
user_id: string
|
||||
kind: string
|
||||
plan?: string
|
||||
amount_cents: number
|
||||
status: string
|
||||
created_at: string
|
||||
}>
|
||||
}>('GET', '/orders'),
|
||||
audit: () =>
|
||||
request<{
|
||||
items: Array<{
|
||||
id: string
|
||||
admin_id: string
|
||||
action: string
|
||||
target_type: string
|
||||
target_id: string
|
||||
meta: unknown
|
||||
created_at: string
|
||||
}>
|
||||
}>('GET', '/audit-logs'),
|
||||
}
|
||||
|
||||
export type UserDetail = {
|
||||
id: string
|
||||
status: string
|
||||
created_at: string
|
||||
profiles: Array<{ id: string; relation: string; display_name: string }>
|
||||
membership: {
|
||||
active: boolean
|
||||
plan?: string
|
||||
status: string
|
||||
expires_at?: string
|
||||
ask_quota_left?: number
|
||||
}
|
||||
recent_orders: Array<{
|
||||
id: string
|
||||
kind: string
|
||||
plan?: string
|
||||
status: string
|
||||
amount_cents: number
|
||||
created_at: string
|
||||
}>
|
||||
}
|
||||
Reference in New Issue
Block a user