Team UI
All checks were successful
Build and Push / build (push) Successful in 1m26s

This commit is contained in:
2026-04-26 23:07:47 +02:00
parent 22fd5fb2cc
commit a31d05b7c2
17 changed files with 1459 additions and 8 deletions

View File

@@ -213,6 +213,143 @@ export async function deleteOrganization(orgId: string): Promise<void> {
await zitadelFetch(`/v2/organizations/${orgId}`, "DELETE");
}
// ---------------------------------------------------------------------------
// Slice 7: search/list APIs for team management
// ---------------------------------------------------------------------------
//
// Two endpoints used by the Team UI:
// - listOrgUsers → POST /v2/users (search with organizationIdQuery)
// - listOrgAuthorizations → Connect RPC to AuthorizationService.ListAuthorizations
//
// Caveats
// -------
// ZITADEL's v2 API surface evolves; the request/response shapes below were
// written against the v2 schema as documented at the time of authoring
// (organizationIdQuery filter on UserService.SearchUsers; ListAuthorizations
// with a ListQuery + filter pair). If your installed ZITADEL version uses
// slightly different field names, parsing here is intentionally tolerant —
// the helpers return [] rather than throwing on shape drift, log a warning,
// and the caller's UI shows an empty team list (which is recoverable).
//
// If you find a discrepancy, fix the request shape here and re-deploy; the
// rest of the team UI doesn't care about the on-the-wire format.
export interface OrgUser {
userId: string;
email: string;
givenName: string;
familyName: string;
displayName: string;
}
/**
* List all users belonging to a given ZITADEL organization. Paginated;
* we cap at 200 per call which is generous for the pilot scale.
*/
export async function listOrgUsers(orgId: string): Promise<OrgUser[]> {
try {
const data = await zitadelFetch<{ result?: any[] }>(
"/v2/users",
"POST",
{
queries: [{ organizationIdQuery: { organizationId: orgId } }],
// Sort by username so the team list is deterministic across reloads
sortingColumn: "USER_FIELD_NAME_USERNAME",
query: { limit: 200, asc: true },
}
);
if (!data?.result || !Array.isArray(data.result)) return [];
return data.result.flatMap((row: any) => {
// ZITADEL distinguishes human and machine users; we only want humans.
const human = row?.human;
if (!human) return [];
const profile = human.profile ?? {};
const email = human.email?.email ?? "";
const userId = row.userId ?? row.id ?? "";
if (!userId) return [];
return [
{
userId,
email,
givenName: profile.givenName ?? "",
familyName: profile.familyName ?? "",
displayName:
profile.displayName ??
`${profile.givenName ?? ""} ${profile.familyName ?? ""}`.trim() ??
email,
} as OrgUser,
];
});
} catch (err) {
console.warn(
`Failed to list users for org ${orgId} (returning empty):`,
err
);
return [];
}
}
export interface OrgAuthorization {
authorizationId: string;
userId: string;
organizationId: string;
projectId: string;
roleKeys: string[];
}
/**
* List authorizations for the OpenClaw Platform project, filtered to a
* single organization. Used by the team UI to render each member's
* effective role.
*
* Connect RPC: zitadel.authorization.v2.AuthorizationService/ListAuthorizations
*
* Returns [] on any error so the team page can render a degraded view
* (members visible, roles blank) rather than blowing up entirely.
*/
export async function listOrgAuthorizations(
orgId: string
): Promise<OrgAuthorization[]> {
try {
const data = await connectRpc<{ authorizations?: any[] }>(
"zitadel.authorization.v2.AuthorizationService",
"ListAuthorizations",
{
filters: [
{ organizationId: orgId },
{ projectId: ZITADEL_PROJECT_ID },
],
// Cap at 500 — far more than a pilot org should ever need
pagination: { limit: 500 },
}
);
if (!data?.authorizations || !Array.isArray(data.authorizations)) {
return [];
}
return data.authorizations.flatMap((row: any) => {
const userId = row?.userId ?? "";
if (!userId) return [];
return [
{
authorizationId: row.id ?? row.authorizationId ?? "",
userId,
organizationId: row.organizationId ?? orgId,
projectId: row.projectId ?? ZITADEL_PROJECT_ID,
roleKeys: Array.isArray(row.roleKeys) ? row.roleKeys : [],
} as OrgAuthorization,
];
});
} catch (err) {
console.warn(
`Failed to list authorizations for org ${orgId} (returning empty):`,
err
);
return [];
}
}
// ---------------------------------------------------------------------------
// Full registration flow
// ---------------------------------------------------------------------------