113 lines
5.3 KiB
TypeScript
113 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL ?? "/api/v1";
|
|
|
|
export type AdminUser = {
|
|
id: string;
|
|
email: string;
|
|
phone: string | null;
|
|
role: "user" | "admin";
|
|
plan: string;
|
|
effective_plan: string;
|
|
status: "active" | "disabled";
|
|
email_verified: boolean;
|
|
phone_verified: boolean;
|
|
created_at: string;
|
|
last_login_at: string | null;
|
|
};
|
|
export type AdminUserDetail = {
|
|
user: AdminUser;
|
|
effective_plan: string;
|
|
effective_grant: { id: string; plan: string; starts_at: string; expires_at: string | null; reason: string } | null;
|
|
quota: { period_start: string; period_end: string; limit: number; adjustment: number; used: number; reserved: number; available: number };
|
|
membership_history: Array<{ id: string; plan: string; starts_at: string; expires_at: string | null; revoked_at: string | null; reason: string; created_at: string }>;
|
|
adjustment_records: Array<{ id: string; amount: number; idempotency_key: string; created_at: string; reason: string | null; actor_id: string | null }>;
|
|
};
|
|
|
|
export type Page<T> = { items: T[]; total: number; page: number; limit: number; pages: number };
|
|
|
|
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
const response = await fetch(`${apiBase}${path}`, {
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json", ...(options.headers ?? {}) },
|
|
...options,
|
|
});
|
|
if (response.status === 401 || response.status === 403) {
|
|
if (response.status === 403) throw new Error("无权执行此操作");
|
|
throw new Error("请登录");
|
|
}
|
|
if (!response.ok) {
|
|
const body = await response.json().catch(() => ({}));
|
|
const message = body.error?.message ?? body.detail ?? "请求失败";
|
|
const msg = Array.isArray(message) ? (message as Array<{ msg?: string }>).map((m) => m.msg ?? "").join("; ") : (message as string);
|
|
throw new Error(msg || "请求失败");
|
|
}
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
export async function fetchCsrf(): Promise<string> {
|
|
const response = await fetch(`${apiBase}/auth/csrf`, { credentials: "include" });
|
|
const body = await response.json();
|
|
return body.csrf_token ?? "";
|
|
}
|
|
|
|
export async function getMe(): Promise<{ email: string; role: string; plan: string; status: string }> {
|
|
return request("/auth/me");
|
|
}
|
|
|
|
export async function listUsers(params: { email?: string; phone?: string; status?: string; page: number; limit: number }): Promise<Page<AdminUser>> {
|
|
const q = new URLSearchParams();
|
|
if (params.email) q.set("email", params.email);
|
|
if (params.phone) q.set("phone", params.phone);
|
|
if (params.status) q.set("status", params.status);
|
|
q.set("page", String(params.page));
|
|
q.set("limit", String(params.limit));
|
|
return request(`/admin/users?${q.toString()}`);
|
|
}
|
|
|
|
export async function getUserDetail(id: string): Promise<AdminUserDetail> {
|
|
return request(`/admin/users/${id}`);
|
|
}
|
|
|
|
export async function changeUserStatus(id: string, status: "active" | "disabled", reason: string, csrf: string): Promise<{ id: string; status: string; sessions_revoked: number }> {
|
|
return request(`/admin/users/${id}/status`, { method: "PATCH", headers: { "X-CSRF-Token": csrf }, body: JSON.stringify({ status, reason }) });
|
|
}
|
|
|
|
export async function grantMembership(id: string, plan: "free" | "vip", expiresAt: string | null, reason: string, csrf: string) {
|
|
return request(`/admin/users/${id}/membership`, { method: "PUT", headers: { "X-CSRF-Token": csrf }, body: JSON.stringify({ plan, expires_at: expiresAt, reason }) });
|
|
}
|
|
|
|
export async function revokeMembership(id: string, reason: string, csrf: string) {
|
|
return request(`/admin/users/${id}/membership/revoke`, { method: "POST", headers: { "X-CSRF-Token": csrf }, body: JSON.stringify({ reason }) });
|
|
}
|
|
|
|
export async function getUserQuota(id: string) {
|
|
return request(`/admin/users/${id}/quota`);
|
|
}
|
|
|
|
export async function adjustQuota(id: string, amount: number, reason: string, idempotencyKey: string, csrf: string) {
|
|
return request(`/admin/users/${id}/quota-adjustments`, { method: "POST", headers: { "X-CSRF-Token": csrf }, body: JSON.stringify({ amount, reason, idempotency_key: idempotencyKey }) });
|
|
}
|
|
|
|
export async function listTtsTasks(params: { user_id?: string; status?: string; created_after?: string; created_before?: string; page: number; limit: number }) {
|
|
const q = new URLSearchParams();
|
|
if (params.user_id) q.set("user_id", params.user_id);
|
|
if (params.status) q.set("status", params.status);
|
|
if (params.created_after) q.set("created_after", params.created_after);
|
|
if (params.created_before) q.set("created_before", params.created_before);
|
|
q.set("page", String(params.page));
|
|
q.set("limit", String(params.limit));
|
|
return request<Page<Record<string, unknown>>>(`/admin/tts/tasks?${q.toString()}`);
|
|
}
|
|
export async function getUsageSummary(): Promise<{ quota: { used: number; reserved: number; adjustment: number }; ledger_total: number; tasks: { total: number; by_status: Record<string, number>; failed: number } }> {
|
|
return request("/admin/usage/summary");
|
|
}
|
|
|
|
export async function listAuditLogs(params: { target_type?: string; page: number; limit: number }) {
|
|
const q = new URLSearchParams();
|
|
if (params.target_type) q.set("target_type", params.target_type);
|
|
q.set("page", String(params.page));
|
|
q.set("limit", String(params.limit));
|
|
return request<Page<Record<string, unknown>>>(`/admin/audit-logs?${q.toString()}`);
|
|
}
|