feat(adminsite): people, instance members and one password
The members panel is absent for self-hosted instances rather than disabled: the backend refuses those, and a panel rendering controls the server will reject is a panel that lies. /auth/me now reports the caller's account role, so the UI hides what the backend would refuse rather than discovering it in an error toast. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+63
-1
@@ -53,16 +53,52 @@ async function req<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const post = <T,>(path: string, payload?: unknown) =>
|
||||
req<T>(path, { method: "POST", body: payload ? JSON.stringify(payload) : undefined });
|
||||
|
||||
const put = <T,>(path: string, payload?: unknown) =>
|
||||
req<T>(path, { method: "PUT", body: payload ? JSON.stringify(payload) : undefined });
|
||||
|
||||
const del = <T,>(path: string) => req<T>(path, { method: "DELETE" });
|
||||
|
||||
// --- types ---------------------------------------------------------------
|
||||
|
||||
export type Deployment = "cloud" | "self_hosted";
|
||||
export type Tier = "free" | "professional" | "self_hosted";
|
||||
export type InstanceStatus = "awaiting_link" | "active" | "lapsed" | "cancelled" | "deleted";
|
||||
|
||||
/*
|
||||
* Two role vocabularies, same three words. AccountRole governs the HQ account:
|
||||
* who may invite, create instances and grant access. InstanceRole is the role a
|
||||
* projected user holds INSIDE one instance. A person can be an account member
|
||||
* and an instance owner at once — that is normal, not a mistake.
|
||||
*/
|
||||
export type AccountRole = "owner" | "admin" | "member";
|
||||
export type InstanceRole = "owner" | "admin" | "member";
|
||||
|
||||
export interface Session {
|
||||
kind: "staff" | "customer";
|
||||
email: string;
|
||||
account_id?: string;
|
||||
account_role?: AccountRole;
|
||||
}
|
||||
|
||||
export interface AccountUser {
|
||||
user_id: string;
|
||||
account_id: string;
|
||||
email: string;
|
||||
account_role: AccountRole;
|
||||
verified_at?: string | null;
|
||||
hq_sync_failed_at?: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface InstanceMember {
|
||||
member_id: string;
|
||||
account_id: string;
|
||||
instance_id: string;
|
||||
customer_user_id: string;
|
||||
control_user_id: string;
|
||||
role: InstanceRole;
|
||||
email: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Limits {
|
||||
@@ -183,7 +219,9 @@ export const api = {
|
||||
signup: (payload: { name: string; email: string; password: string; website?: string }) =>
|
||||
post<{ pending: boolean }>("/auth/signup", payload),
|
||||
verify: (token: string) =>
|
||||
req<{ verified: boolean }>(`/auth/verify?token=${encodeURIComponent(token)}`),
|
||||
req<{ verified: boolean; needs_password?: boolean }>(
|
||||
`/auth/verify?token=${encodeURIComponent(token)}`,
|
||||
),
|
||||
|
||||
account: () => req<AccountResponse>("/api/account"),
|
||||
link: (instance_id: string, name: string) =>
|
||||
@@ -196,6 +234,30 @@ export const api = {
|
||||
licenseBlobUrl: (id: string) => `${API_BASE}/api/instances/${id}/license/download`,
|
||||
subscriptions: () => req<Subscription[]>("/api/subscriptions"),
|
||||
|
||||
accountUsers: () => req<AccountUser[]>("/api/account/users"),
|
||||
invite: (email: string, role: AccountRole) =>
|
||||
post<{ invited: boolean }>("/api/account/users", { email, role }),
|
||||
setAccountRole: (userId: string, role: AccountRole) =>
|
||||
put<{ ok: boolean }>(`/api/account/users/${userId}/role`, { role }),
|
||||
removeAccountUser: (userId: string) =>
|
||||
del<{ deleted: boolean }>(`/api/account/users/${userId}`),
|
||||
changePassword: (current_password: string, new_password: string) =>
|
||||
put<{ updated: boolean; propagation_pending: boolean }>("/api/account/password", {
|
||||
current_password,
|
||||
new_password,
|
||||
}),
|
||||
acceptInvite: (token: string, password: string) =>
|
||||
post<{ accepted: boolean }>("/auth/accept-invite", { token, password }),
|
||||
|
||||
members: (instanceId: string) =>
|
||||
req<InstanceMember[]>(`/api/instances/${instanceId}/members`),
|
||||
grantMember: (instanceId: string, user_id: string, role: InstanceRole) =>
|
||||
post<InstanceMember>(`/api/instances/${instanceId}/members`, { user_id, role }),
|
||||
setMemberRole: (instanceId: string, userId: string, role: InstanceRole) =>
|
||||
put<{ ok: boolean }>(`/api/instances/${instanceId}/members/${userId}/role`, { role }),
|
||||
revokeMember: (instanceId: string, userId: string) =>
|
||||
del<{ revoked: boolean }>(`/api/instances/${instanceId}/members/${userId}`),
|
||||
|
||||
staff: {
|
||||
accounts: (q?: string) =>
|
||||
req<Account[]>(`/api/staff/accounts${q ? `?q=${encodeURIComponent(q)}` : ""}`),
|
||||
|
||||
Reference in New Issue
Block a user