feat(admin): 「租户 & 用户」页接真实成员管理(删掉 mock)
此前整页是硬编码 mock(假租户/假用户/假 API key/假日预算)。重写为真数据: - 左:租户目录(listTenants:名称/slug/plan/成员数/积分余额)+ 建租户表单 (名称/slug/可选 owner 邮箱)。 - 右:选定租户成员表(listMembers)——按邮箱加成员+选角色、行内改角色、移除; owner 受保护(不可改角色/移除)。api 加 listTenants/createTenant/listMembers/ addMember/setMemberRole/removeMember。 live 验证(preview):14 个真实租户带成员数/余额;UI 加 demoB→Dexter「2 成员」、 成员表 owner 保护 + 成员可改角色/移除、移除后回 1 成员,全过;tsc + 41 测试全过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -438,3 +438,65 @@ export async function adminUsage(params?: { tenant?: string; from?: string; to?:
|
||||
const d = (await res.json()) as UsageReport;
|
||||
return { ...d, trend: d.trend ?? [], tenants: d.tenants ?? [] }; // Go 空切片可能序列化为 null
|
||||
}
|
||||
|
||||
// —— 多成员租户:租户目录 + 成员管理 ——
|
||||
export interface TenantRow {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
plan: string;
|
||||
status: string;
|
||||
credit_balance_micro: number;
|
||||
members: number;
|
||||
}
|
||||
export interface Member {
|
||||
user_id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: string; // owner / admin / member / viewer / billing_admin
|
||||
status: string;
|
||||
joined_at: string;
|
||||
}
|
||||
|
||||
export async function listTenants(): Promise<TenantRow[]> {
|
||||
const res = guard(await fetch(`${ADMIN}/tenants`, { headers: authHeaders() }));
|
||||
if (!res.ok) throw new Error(`list tenants failed: ${res.status}`);
|
||||
return ((await res.json()) as { tenants?: TenantRow[] }).tenants ?? [];
|
||||
}
|
||||
|
||||
export async function createTenant(name: string, slug: string, ownerEmail?: string): Promise<void> {
|
||||
const res = guard(await fetch(`${ADMIN}/tenants`, { method: "POST", headers: authHeaders(true), body: JSON.stringify({ name, slug, owner_email: ownerEmail ?? "" }) }));
|
||||
const d = (await res.json().catch(() => ({}))) as { error?: string; warn?: string };
|
||||
if (!res.ok) throw new Error(d.error ?? `create tenant failed: ${res.status}`);
|
||||
if (d.warn) throw new Error(d.warn); // 租户已建但指定 owner 失败 → 当提示
|
||||
}
|
||||
|
||||
export async function listMembers(tenantId: string): Promise<Member[]> {
|
||||
const res = guard(await fetch(`${ADMIN}/tenants/${tenantId}/members`, { headers: authHeaders() }));
|
||||
if (!res.ok) throw new Error(`list members failed: ${res.status}`);
|
||||
return ((await res.json()) as { members?: Member[] }).members ?? [];
|
||||
}
|
||||
|
||||
export async function addMember(tenantId: string, email: string, role: string): Promise<void> {
|
||||
const res = guard(await fetch(`${ADMIN}/tenants/${tenantId}/members`, { method: "POST", headers: authHeaders(true), body: JSON.stringify({ email, role }) }));
|
||||
if (!res.ok) {
|
||||
const d = (await res.json().catch(() => ({}))) as { error?: string };
|
||||
throw new Error(d.error ?? `add member failed: ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setMemberRole(tenantId: string, uid: string, role: string): Promise<void> {
|
||||
const res = guard(await fetch(`${ADMIN}/tenants/${tenantId}/members/${uid}`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify({ role }) }));
|
||||
if (!res.ok) {
|
||||
const d = (await res.json().catch(() => ({}))) as { error?: string };
|
||||
throw new Error(d.error ?? `set role failed: ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeMember(tenantId: string, uid: string): Promise<void> {
|
||||
const res = guard(await fetch(`${ADMIN}/tenants/${tenantId}/members/${uid}`, { method: "DELETE", headers: authHeaders() }));
|
||||
if (!res.ok) {
|
||||
const d = (await res.json().catch(() => ({}))) as { error?: string };
|
||||
throw new Error(d.error ?? `remove member failed: ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user