This commit is contained in:
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user