forked from baron/baron-sso
1406 lines
33 KiB
TypeScript
1406 lines
33 KiB
TypeScript
import { fetchAllCursorPages } from "../../../common/core/pagination";
|
|
import apiClient from "./apiClient";
|
|
import { userManager } from "./auth";
|
|
|
|
export type AuditLog = {
|
|
event_id: string;
|
|
timestamp: string;
|
|
user_id: string;
|
|
event_type: string;
|
|
status: string;
|
|
ip_address: string;
|
|
user_agent: string;
|
|
device_id?: string;
|
|
details?: string;
|
|
};
|
|
|
|
export type AuditLogListResponse = {
|
|
items: AuditLog[];
|
|
limit: number;
|
|
cursor?: string;
|
|
next_cursor?: string;
|
|
};
|
|
|
|
export type TenantSummary = {
|
|
id: string;
|
|
type: string; // 허용 타입: PERSONAL, COMPANY, COMPANY_GROUP, ORGANIZATION, USER_GROUP
|
|
name: string;
|
|
slug: string;
|
|
description: string;
|
|
status: string;
|
|
domains?: string[];
|
|
parentId?: string;
|
|
config?: Record<string, unknown>;
|
|
memberCount: number; // 해당 테넌트 직접 소속 인원
|
|
totalMemberCount?: number; // 하위 테넌트 포함 전체 인원
|
|
userPermissions?: {
|
|
view: boolean;
|
|
manage: boolean;
|
|
manage_admins: boolean;
|
|
view_profile?: boolean;
|
|
manage_profile?: boolean;
|
|
view_permissions?: boolean;
|
|
manage_permissions?: boolean;
|
|
view_organization?: boolean;
|
|
manage_organization?: boolean;
|
|
view_schema?: boolean;
|
|
manage_schema?: boolean;
|
|
};
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type TenantCreateRequest = {
|
|
name: string;
|
|
type?: string;
|
|
slug?: string;
|
|
parentId?: string;
|
|
description?: string;
|
|
status?: string;
|
|
domains?: string[];
|
|
forceDomainConflicts?: string[];
|
|
config?: Record<string, unknown>;
|
|
};
|
|
|
|
export type TenantListResponse = {
|
|
items: TenantSummary[];
|
|
limit: number;
|
|
offset: number;
|
|
total: number;
|
|
cursor?: string;
|
|
nextCursor?: string;
|
|
next_cursor?: string;
|
|
};
|
|
|
|
export type TenantUpdateRequest = {
|
|
name?: string;
|
|
type?: string;
|
|
slug?: string;
|
|
parentId?: string;
|
|
description?: string;
|
|
status?: string;
|
|
domains?: string[];
|
|
forceDomainConflicts?: string[];
|
|
config?: Record<string, unknown>;
|
|
};
|
|
|
|
export type TenantImportDetail = {
|
|
row: number;
|
|
slug: string;
|
|
name: string;
|
|
success: boolean;
|
|
action: "created" | "updated" | "failed" | "skipped";
|
|
message: string;
|
|
modifiedFields?: string[];
|
|
};
|
|
|
|
export type TenantImportResult = {
|
|
created: number;
|
|
updated: number;
|
|
failed: number;
|
|
errors: string[];
|
|
details: TenantImportDetail[];
|
|
};
|
|
|
|
export type ApiKeySummary = {
|
|
id: string;
|
|
name: string;
|
|
client_id: string;
|
|
scopes: string[];
|
|
status: string;
|
|
lastUsedAt?: string;
|
|
createdAt: string;
|
|
};
|
|
|
|
export type ApiKeyListResponse = {
|
|
items: ApiKeySummary[];
|
|
total: number;
|
|
};
|
|
|
|
export type RoleSummary = {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
permissions: string[];
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type RoleListResponse = {
|
|
items: RoleSummary[];
|
|
total: number;
|
|
};
|
|
|
|
export type RPUsageDailyMetric = {
|
|
date: string;
|
|
tenantId: string;
|
|
tenantType: string;
|
|
tenantName?: string;
|
|
clientId: string;
|
|
clientName: string;
|
|
loginRequests: number;
|
|
otherRequests: number;
|
|
uniqueSubjects: number;
|
|
};
|
|
|
|
export type RPUsagePeriod = "day" | "week" | "month";
|
|
|
|
export type RPUsageDailyResponse = {
|
|
items: RPUsageDailyMetric[];
|
|
days: number;
|
|
period: RPUsagePeriod;
|
|
tenantId?: string;
|
|
};
|
|
|
|
export type AdminOverviewStats = {
|
|
totalTenants: number;
|
|
totalUsers: number;
|
|
oidcClients: number;
|
|
auditEvents24h: number;
|
|
};
|
|
|
|
export type IdentityCacheStatus = {
|
|
status: string;
|
|
redisReady: boolean;
|
|
mirrorVersion?: string;
|
|
observedCount: number;
|
|
keyCount: number;
|
|
lastRefreshedAt?: string;
|
|
lastError?: string;
|
|
updatedAt?: string;
|
|
};
|
|
|
|
export type OrySSOTSystemStatus = {
|
|
identityCache: IdentityCacheStatus;
|
|
};
|
|
|
|
export type IdentityCacheFlushResult = {
|
|
status: string;
|
|
flushedKeys: number;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type DataIntegrityStatus = "pass" | "warning" | "fail";
|
|
|
|
export type DataIntegrityCheck = {
|
|
key: string;
|
|
label: string;
|
|
description: string;
|
|
status: DataIntegrityStatus;
|
|
severity: "info" | "warning" | "error" | string;
|
|
count: number;
|
|
};
|
|
|
|
export type DataIntegritySection = {
|
|
key: string;
|
|
label: string;
|
|
status: DataIntegrityStatus;
|
|
checks: DataIntegrityCheck[];
|
|
};
|
|
|
|
export type DataIntegrityReport = {
|
|
status: DataIntegrityStatus;
|
|
checkedAt: string;
|
|
summary: {
|
|
totalChecks: number;
|
|
passed: number;
|
|
warnings: number;
|
|
failures: number;
|
|
};
|
|
sections: DataIntegritySection[];
|
|
};
|
|
|
|
export type OrphanUserLoginID = {
|
|
id: string;
|
|
userId: string;
|
|
userEmail?: string;
|
|
userDeletedAt?: string;
|
|
tenantId: string;
|
|
tenantSlug?: string;
|
|
tenantDeletedAt?: string;
|
|
fieldKey: string;
|
|
loginId: string;
|
|
reasons: string[];
|
|
};
|
|
|
|
export type OrphanUserLoginIDListResponse = {
|
|
items: OrphanUserLoginID[];
|
|
total: number;
|
|
};
|
|
|
|
export type DeleteOrphanUserLoginIDsResult = {
|
|
deletedCount: number;
|
|
deleted: OrphanUserLoginID[];
|
|
skippedIds: string[];
|
|
};
|
|
|
|
export async function fetchAuditLogs(
|
|
limit = 50,
|
|
cursor?: string,
|
|
search?: string,
|
|
status?: string,
|
|
) {
|
|
const { data } = await apiClient.get<AuditLogListResponse>("/v1/audit", {
|
|
params: { limit, cursor, search, status },
|
|
});
|
|
return data;
|
|
}
|
|
|
|
export async function fetchAdminOverviewStats() {
|
|
const { data } = await apiClient.get<AdminOverviewStats>("/v1/admin/stats");
|
|
return data;
|
|
}
|
|
|
|
export async function fetchDataIntegrityReport() {
|
|
const { data } = await apiClient.get<DataIntegrityReport>(
|
|
"/v1/admin/integrity",
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function fetchOrphanUserLoginIDs() {
|
|
const { data } = await apiClient.get<OrphanUserLoginIDListResponse>(
|
|
"/v1/admin/integrity/orphan-user-login-ids",
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function deleteOrphanUserLoginIDs(ids: string[]) {
|
|
const { data } = await apiClient.delete<DeleteOrphanUserLoginIDsResult>(
|
|
"/v1/admin/integrity/orphan-user-login-ids",
|
|
{ data: { ids } },
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function fetchOrySSOTSystemStatus() {
|
|
const { data } =
|
|
await apiClient.get<OrySSOTSystemStatus>("/v1/admin/ory/ssot");
|
|
return data;
|
|
}
|
|
|
|
export async function flushIdentityCache() {
|
|
const { data } = await apiClient.post<IdentityCacheFlushResult>(
|
|
"/v1/admin/ory/ssot/identity-cache/flush",
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function fetchAdminRPUsageDaily({
|
|
days = 14,
|
|
period = "day",
|
|
tenantId,
|
|
}: {
|
|
days?: number;
|
|
period?: RPUsagePeriod;
|
|
tenantId?: string;
|
|
} = {}) {
|
|
const { data } = await apiClient.get<RPUsageDailyResponse>(
|
|
"/v1/admin/rp-usage/daily",
|
|
{
|
|
params: { days, period, tenantId: tenantId || undefined },
|
|
},
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function fetchTenants(
|
|
limit = 50,
|
|
offset = 0,
|
|
parentId?: string,
|
|
cursor?: string,
|
|
search?: string,
|
|
) {
|
|
const { data } = await apiClient.get<TenantListResponse>(
|
|
"/v1/admin/tenants",
|
|
{
|
|
params: { limit, offset, parentId, cursor, search },
|
|
},
|
|
);
|
|
return data;
|
|
}
|
|
|
|
function getAdminApiBaseUrl() {
|
|
if (
|
|
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })
|
|
._IS_TEST_MODE
|
|
) {
|
|
return "http://playwright-mock/api";
|
|
}
|
|
|
|
return import.meta.env.VITE_ADMIN_API_BASE ?? "/api";
|
|
}
|
|
|
|
async function buildAdminRequestHeaders() {
|
|
const headers: Record<string, string> = {};
|
|
const user = await userManager.getUser();
|
|
const sessionToken =
|
|
user?.access_token || window.localStorage.getItem("admin_session");
|
|
|
|
if (sessionToken) {
|
|
headers.Authorization = `Bearer ${sessionToken}`;
|
|
}
|
|
|
|
const tenantId = window.localStorage.getItem("admin_tenant");
|
|
if (tenantId) {
|
|
headers["X-Tenant-ID"] = tenantId;
|
|
}
|
|
|
|
const isMockRoleEnabled =
|
|
window.localStorage.getItem("X-Mock-Role-Enabled") === "true";
|
|
const mockRole = window.localStorage.getItem("X-Mock-Role");
|
|
if (isMockRoleEnabled && mockRole) {
|
|
headers["X-Test-Role"] = mockRole;
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
export async function fetchAllTenants({
|
|
pageSize = 100,
|
|
parentId,
|
|
}: {
|
|
pageSize?: number;
|
|
parentId?: string;
|
|
} = {}) {
|
|
return fetchAllCursorPages<TenantSummary>({
|
|
baseUrl: getAdminApiBaseUrl(),
|
|
path: "/v1/admin/tenants",
|
|
pageSize,
|
|
params: { parentId },
|
|
headers: await buildAdminRequestHeaders(),
|
|
}) as Promise<TenantListResponse>;
|
|
}
|
|
|
|
export async function fetchTenant(tenantId: string) {
|
|
const { data } = await apiClient.get<TenantSummary>(
|
|
`/v1/admin/tenants/${tenantId}`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function createTenant(payload: TenantCreateRequest) {
|
|
const { data } = await apiClient.post<TenantSummary>(
|
|
"/v1/admin/tenants",
|
|
payload,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function updateTenant(
|
|
tenantId: string,
|
|
payload: TenantUpdateRequest,
|
|
) {
|
|
const { data } = await apiClient.put<TenantSummary>(
|
|
`/v1/admin/tenants/${tenantId}`,
|
|
payload,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function deleteTenant(tenantId: string) {
|
|
await apiClient.delete(`/v1/admin/tenants/${tenantId}`);
|
|
}
|
|
|
|
export async function deleteTenantsBulk(ids: string[]) {
|
|
await apiClient.delete("/v1/admin/tenants/bulk", {
|
|
data: { ids },
|
|
});
|
|
}
|
|
|
|
export async function exportTenantsCSV(includeIds = false, parentId?: string) {
|
|
const response = await apiClient.get<Blob>("/v1/admin/tenants/export", {
|
|
params: { includeIds, parentId: parentId || undefined },
|
|
responseType: "blob",
|
|
});
|
|
const dispositionHeader = response.headers["content-disposition"];
|
|
const disposition = Array.isArray(dispositionHeader)
|
|
? dispositionHeader[0]
|
|
: String(dispositionHeader ?? "");
|
|
const filenameMatch = disposition?.match(/filename="?([^"]+)"?/i);
|
|
return {
|
|
blob: response.data,
|
|
filename: filenameMatch?.[1] ?? "tenants.csv",
|
|
};
|
|
}
|
|
|
|
export async function importTenantsCSV(file: File) {
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
const { data } = await apiClient.post<TenantImportResult>(
|
|
"/v1/admin/tenants/import",
|
|
formData,
|
|
{
|
|
headers: {
|
|
"Content-Type": "multipart/form-data",
|
|
},
|
|
},
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function approveTenant(tenantId: string) {
|
|
const { data } = await apiClient.post<TenantSummary>(
|
|
`/v1/admin/tenants/${tenantId}/approve`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export type TenantAdmin = {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
};
|
|
|
|
export async function fetchTenantAdmins(tenantId: string) {
|
|
const { data } = await apiClient.get<TenantAdmin[]>(
|
|
`/v1/admin/tenants/${tenantId}/admins`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function addTenantAdmin(tenantId: string, userId: string) {
|
|
await apiClient.post(`/v1/admin/tenants/${tenantId}/admins/${userId}`);
|
|
}
|
|
|
|
export async function removeTenantAdmin(tenantId: string, userId: string) {
|
|
await apiClient.delete(`/v1/admin/tenants/${tenantId}/admins/${userId}`);
|
|
}
|
|
|
|
export async function fetchTenantOwners(tenantId: string) {
|
|
const { data } = await apiClient.get<TenantAdmin[]>(
|
|
`/v1/admin/tenants/${tenantId}/owners`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function addTenantOwner(tenantId: string, userId: string) {
|
|
await apiClient.post(`/v1/admin/tenants/${tenantId}/owners/${userId}`);
|
|
}
|
|
|
|
export async function removeTenantOwner(tenantId: string, userId: string) {
|
|
await apiClient.delete(`/v1/admin/tenants/${tenantId}/owners/${userId}`);
|
|
}
|
|
|
|
export type TenantRelation = {
|
|
userId: string;
|
|
name: string;
|
|
email: string;
|
|
relations: string[];
|
|
};
|
|
|
|
export async function fetchTenantRelations(tenantId: string) {
|
|
const { data } = await apiClient.get<{ items: TenantRelation[] }>(
|
|
`/v1/admin/tenants/${tenantId}/relations`,
|
|
);
|
|
return data.items;
|
|
}
|
|
|
|
export async function addTenantRelation(
|
|
tenantId: string,
|
|
userId: string,
|
|
relation: string,
|
|
) {
|
|
await apiClient.post(`/v1/admin/tenants/${tenantId}/relations`, {
|
|
userId,
|
|
relation,
|
|
});
|
|
}
|
|
|
|
export async function removeTenantRelation(
|
|
tenantId: string,
|
|
userId: string,
|
|
relation: string,
|
|
) {
|
|
await apiClient.delete(`/v1/admin/tenants/${tenantId}/relations`, {
|
|
data: { userId, relation },
|
|
});
|
|
}
|
|
|
|
export async function fetchSystemRelations() {
|
|
const { data } = await apiClient.get<{ items: TenantRelation[] }>(
|
|
`/v1/admin/system/relations`,
|
|
);
|
|
return data.items;
|
|
}
|
|
|
|
export async function addSystemRelation(userId: string, relation: string) {
|
|
await apiClient.post(`/v1/admin/system/relations`, {
|
|
userId,
|
|
relation,
|
|
});
|
|
}
|
|
|
|
export async function removeSystemRelation(userId: string, relation: string) {
|
|
await apiClient.delete(`/v1/admin/system/relations`, {
|
|
data: { userId, relation },
|
|
});
|
|
}
|
|
|
|
// Group Management
|
|
export type GroupMember = {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
};
|
|
|
|
export type GroupSummary = {
|
|
id: string;
|
|
tenantId: string;
|
|
parentId?: string;
|
|
name: string;
|
|
description?: string;
|
|
unitType?: string;
|
|
members?: GroupMember[];
|
|
createdAt?: string;
|
|
updatedAt?: string;
|
|
};
|
|
|
|
export type GroupCreateRequest = {
|
|
name: string;
|
|
parentId?: string;
|
|
description?: string;
|
|
unitType?: string;
|
|
};
|
|
|
|
export async function fetchGroups(tenantId: string) {
|
|
const { data } = await apiClient.get<GroupSummary[]>(
|
|
`/v1/admin/tenants/${tenantId}/organization`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function fetchGroup(tenantId: string, groupId: string) {
|
|
const { data } = await apiClient.get<GroupSummary>(
|
|
`/v1/admin/tenants/${tenantId}/organization/${groupId}`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function createGroup(
|
|
tenantId: string,
|
|
payload: GroupCreateRequest,
|
|
) {
|
|
const { data } = await apiClient.post<GroupSummary>(
|
|
`/v1/admin/tenants/${tenantId}/organization`,
|
|
payload,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function deleteGroup(tenantId: string, groupId: string) {
|
|
await apiClient.delete(
|
|
`/v1/admin/tenants/${tenantId}/organization/${groupId}`,
|
|
);
|
|
}
|
|
|
|
export async function addGroupMember(
|
|
tenantId: string,
|
|
groupId: string,
|
|
userId: string,
|
|
) {
|
|
await apiClient.post(
|
|
`/v1/admin/tenants/${tenantId}/organization/${groupId}/members`,
|
|
{ userId },
|
|
);
|
|
}
|
|
|
|
export async function removeGroupMember(
|
|
tenantId: string,
|
|
groupId: string,
|
|
userId: string,
|
|
) {
|
|
await apiClient.delete(
|
|
`/v1/admin/tenants/${tenantId}/organization/${groupId}/members/${userId}`,
|
|
);
|
|
}
|
|
|
|
export type GroupRole = {
|
|
tenantId: string;
|
|
tenantName: string;
|
|
relation: string;
|
|
};
|
|
|
|
export async function fetchGroupRoles(tenantId: string, groupId: string) {
|
|
const { data } = await apiClient.get<GroupRole[]>(
|
|
`/v1/admin/tenants/${tenantId}/organization/${groupId}/roles`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function assignGroupRole(
|
|
tenantId: string,
|
|
groupId: string,
|
|
targetTenantId: string,
|
|
relation: string,
|
|
) {
|
|
await apiClient.post(
|
|
`/v1/admin/tenants/${tenantId}/organization/${groupId}/roles`,
|
|
{ tenantId: targetTenantId, relation },
|
|
);
|
|
}
|
|
|
|
export async function removeGroupRole(
|
|
tenantId: string,
|
|
groupId: string,
|
|
targetTenantId: string,
|
|
relation: string,
|
|
) {
|
|
await apiClient.delete(
|
|
`/v1/admin/tenants/${tenantId}/organization/${groupId}/roles/${targetTenantId}/${relation}`,
|
|
);
|
|
}
|
|
|
|
// API Key Management (M2M)
|
|
export type ApiKeyCreateRequest = {
|
|
name: string;
|
|
scopes: string[];
|
|
};
|
|
|
|
export type ApiKeyCreateResponse = {
|
|
apiKey: ApiKeySummary;
|
|
clientSecret: string;
|
|
};
|
|
|
|
export type ApiKeyUpdateScopesRequest = {
|
|
scopes: string[];
|
|
};
|
|
|
|
export async function fetchApiKeys(limit = 50, offset = 0) {
|
|
const { data } = await apiClient.get<ApiKeyListResponse>(
|
|
"/v1/admin/api-keys",
|
|
{
|
|
params: { limit, offset },
|
|
},
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function createApiKey(payload: ApiKeyCreateRequest) {
|
|
const { data } = await apiClient.post<ApiKeyCreateResponse>(
|
|
"/v1/admin/api-keys",
|
|
payload,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function updateApiKeyScopes(
|
|
apiKeyId: string,
|
|
payload: ApiKeyUpdateScopesRequest,
|
|
) {
|
|
const { data } = await apiClient.patch<ApiKeySummary>(
|
|
`/v1/admin/api-keys/${apiKeyId}`,
|
|
payload,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function rotateApiKeySecret(apiKeyId: string) {
|
|
const { data } = await apiClient.post<ApiKeyCreateResponse>(
|
|
`/v1/admin/api-keys/${apiKeyId}/secret/rotate`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function deleteApiKey(apiKeyId: string) {
|
|
await apiClient.delete(`/v1/admin/api-keys/${apiKeyId}`);
|
|
}
|
|
|
|
// User Management
|
|
export type UserSummary = {
|
|
id: string;
|
|
email: string;
|
|
loginId?: string;
|
|
name: string;
|
|
phone?: string;
|
|
role: string;
|
|
status: string;
|
|
tenantSlug?: string;
|
|
companyCode?: string;
|
|
tenant?: TenantSummary;
|
|
joinedTenants?: TenantSummary[]; // [New] 다중 소속 테넌트 목록
|
|
metadata?: Record<string, unknown>;
|
|
department?: string;
|
|
grade?: string;
|
|
position?: string;
|
|
jobTitle?: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type UserListResponse = {
|
|
items: UserSummary[];
|
|
limit: number;
|
|
offset: number;
|
|
total: number;
|
|
next_cursor?: string;
|
|
nextCursor?: string;
|
|
};
|
|
|
|
export type UserCreateRequest = {
|
|
email: string;
|
|
loginId?: string;
|
|
password?: string;
|
|
name: string;
|
|
phone?: string;
|
|
role?: string;
|
|
tenantSlug?: string;
|
|
department?: string;
|
|
grade?: string;
|
|
position?: string;
|
|
jobTitle?: string;
|
|
primaryTenantId?: string;
|
|
primaryTenantName?: string;
|
|
primaryTenantIsOwner?: boolean;
|
|
additionalAppointments?: UserAppointment[];
|
|
metadata?: Record<string, unknown>;
|
|
};
|
|
|
|
export type UserCreateResponse = UserSummary & {
|
|
initialPassword?: string;
|
|
};
|
|
|
|
export type UserUpdateRequest = {
|
|
email?: string;
|
|
loginId?: string;
|
|
password?: string;
|
|
name?: string;
|
|
phone?: string;
|
|
role?: string;
|
|
status?: string;
|
|
tenantSlug?: string;
|
|
isPrimaryTenant?: boolean;
|
|
isAddTenant?: boolean;
|
|
isRemoveTenant?: boolean;
|
|
department?: string;
|
|
grade?: string;
|
|
position?: string;
|
|
jobTitle?: string;
|
|
primaryTenantId?: string;
|
|
primaryTenantName?: string;
|
|
primaryTenantIsOwner?: boolean;
|
|
additionalAppointments?: UserAppointment[];
|
|
metadata?: Record<string, unknown>;
|
|
};
|
|
|
|
export type GlobalCustomClaimPermission = "admin_only" | "user_and_admin";
|
|
|
|
export type GlobalCustomClaimDefinition = {
|
|
key: string;
|
|
label: string;
|
|
valueType:
|
|
| "text"
|
|
| "number"
|
|
| "boolean"
|
|
| "array"
|
|
| "object"
|
|
| "date"
|
|
| "datetime";
|
|
readPermission: GlobalCustomClaimPermission;
|
|
writePermission: GlobalCustomClaimPermission;
|
|
description?: string;
|
|
};
|
|
|
|
export type GlobalCustomClaimDefinitionsResponse = {
|
|
items: GlobalCustomClaimDefinition[];
|
|
};
|
|
|
|
export type UserAppointment = {
|
|
tenantId: string;
|
|
tenantSlug?: string;
|
|
tenantName: string;
|
|
isPrimary?: boolean;
|
|
isOwner?: boolean;
|
|
isAdmin?: boolean;
|
|
isManager?: boolean;
|
|
jobTitle?: string;
|
|
grade?: string;
|
|
position?: string;
|
|
};
|
|
|
|
export type BulkUserAppointment = {
|
|
tenantId?: string;
|
|
tenantSlug?: string;
|
|
tenantName?: string;
|
|
isPrimary?: boolean;
|
|
isOwner?: boolean;
|
|
isAdmin?: boolean;
|
|
isManager?: boolean;
|
|
department?: string;
|
|
grade?: string;
|
|
position?: string;
|
|
jobTitle?: string;
|
|
metadata?: Record<string, unknown>;
|
|
};
|
|
|
|
export type BulkUserItem = {
|
|
email: string;
|
|
loginId?: string;
|
|
name: string;
|
|
phone?: string;
|
|
role?: string;
|
|
tenantId?: string;
|
|
tenantSlug?: string;
|
|
emailDomain?: string;
|
|
department?: string;
|
|
grade?: string;
|
|
position?: string;
|
|
jobTitle?: string;
|
|
additionalAppointments?: BulkUserAppointment[];
|
|
tenantImport?: {
|
|
sourceTenantId?: string;
|
|
slug?: string;
|
|
name?: string;
|
|
type?: string;
|
|
parentTenantId?: string;
|
|
parentTenantSlug?: string;
|
|
parentTenantName?: string;
|
|
memo?: string;
|
|
emailDomain?: string;
|
|
};
|
|
metadata: Record<string, unknown>;
|
|
importErrors?: string[];
|
|
};
|
|
|
|
export type BulkUserResult = {
|
|
email: string;
|
|
originalEmail?: string;
|
|
suggestedEmail?: string;
|
|
status?: string;
|
|
warnings?: string[];
|
|
success: boolean;
|
|
message?: string;
|
|
userId?: string;
|
|
modifiedFields?: string[];
|
|
};
|
|
|
|
export type BulkUserResponse = {
|
|
results: BulkUserResult[];
|
|
};
|
|
|
|
export type WorksmobileOutboxItem = {
|
|
id: string;
|
|
resourceType: string;
|
|
resourceId: string;
|
|
action: string;
|
|
payload?: Record<string, unknown>;
|
|
status: string;
|
|
retryCount: number;
|
|
lastError?: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type WorksmobileOverview = {
|
|
tenant: TenantSummary;
|
|
config: {
|
|
enabled: boolean;
|
|
domainMappings?: Record<string, number>;
|
|
tokenConfigured: boolean;
|
|
adminTenantId?: string;
|
|
};
|
|
recentJobs: WorksmobileOutboxItem[];
|
|
};
|
|
|
|
export type WorksmobileCredentialBatch = {
|
|
batchId: string;
|
|
operation?: string;
|
|
userCount: number;
|
|
pendingCount?: number;
|
|
processingCount?: number;
|
|
processedCount?: number;
|
|
failedCount?: number;
|
|
hasPasswords: boolean;
|
|
deletedAt?: string;
|
|
failures?: WorksmobileCredentialBatchFailure[];
|
|
createdAt?: string;
|
|
updatedAt?: string;
|
|
};
|
|
|
|
export type WorksmobileCredentialBatchFailure = {
|
|
userId?: string;
|
|
email?: string;
|
|
status: string;
|
|
retryCount: number;
|
|
lastError?: string;
|
|
updatedAt?: string;
|
|
};
|
|
|
|
export type WorksmobilePendingJobDeleteResult = {
|
|
deletedCount: number;
|
|
};
|
|
|
|
export type WorksmobileComparisonItem = {
|
|
resourceType: string;
|
|
baronId?: string;
|
|
baronSlug?: string;
|
|
baronName?: string;
|
|
baronEmail?: string;
|
|
baronPhone?: string;
|
|
baronEmployeeNumber?: string;
|
|
baronPrimaryOrgId?: string;
|
|
baronPrimaryOrgSlug?: string;
|
|
baronPrimaryOrgName?: string;
|
|
baronParentId?: string;
|
|
baronParentSlug?: string;
|
|
baronParentName?: string;
|
|
worksmobileId?: string;
|
|
externalKey?: string;
|
|
worksmobileName?: string;
|
|
worksmobileEmail?: string;
|
|
worksmobilePhone?: string;
|
|
worksmobileEmployeeNumber?: string;
|
|
worksmobileAccountStatus?: string;
|
|
worksmobileLevelId?: string;
|
|
worksmobileLevelName?: string;
|
|
worksmobileTask?: string;
|
|
worksmobileDomainId?: number;
|
|
worksmobileDomainName?: string;
|
|
worksmobilePrimaryOrgId?: string;
|
|
worksmobilePrimaryOrgName?: string;
|
|
worksmobilePrimaryOrgPositionId?: string;
|
|
worksmobilePrimaryOrgPositionName?: string;
|
|
worksmobilePrimaryOrgIsManager?: boolean;
|
|
baronParentWorksmobileId?: string;
|
|
baronParentWorksmobileName?: string;
|
|
baronParentWorksmobileEmail?: string;
|
|
worksmobileParentId?: string;
|
|
worksmobileParentName?: string;
|
|
worksmobileParentEmail?: string;
|
|
worksmobileParentExternalKey?: string;
|
|
worksmobileJobStatus?: string;
|
|
worksmobileJobRetryCount?: number;
|
|
worksmobileLastError?: string;
|
|
worksmobileLastAttemptAt?: string;
|
|
updateReasons?: string[];
|
|
status: string;
|
|
};
|
|
|
|
export type WorksmobileComparison = {
|
|
users: WorksmobileComparisonItem[];
|
|
groups: WorksmobileComparisonItem[];
|
|
};
|
|
|
|
export async function fetchUsers(
|
|
limit = 50,
|
|
offset = 0,
|
|
search?: string,
|
|
tenantSlug?: string,
|
|
cursor?: string,
|
|
) {
|
|
const { data } = await apiClient.get<UserListResponse>("/v1/admin/users", {
|
|
params: { limit, offset, search, tenantSlug, cursor },
|
|
});
|
|
return data;
|
|
}
|
|
|
|
export async function fetchUser(userId: string) {
|
|
const { data } = await apiClient.get<UserSummary>(
|
|
`/v1/admin/users/${userId}`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function fetchGlobalCustomClaimDefinitions() {
|
|
const { data } = await apiClient.get<GlobalCustomClaimDefinitionsResponse>(
|
|
"/v1/admin/global-custom-claims",
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function updateGlobalCustomClaimDefinitions(
|
|
payload: GlobalCustomClaimDefinitionsResponse,
|
|
) {
|
|
const { data } = await apiClient.put<GlobalCustomClaimDefinitionsResponse>(
|
|
"/v1/admin/global-custom-claims",
|
|
payload,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function createUser(payload: UserCreateRequest) {
|
|
const { data } = await apiClient.post<UserCreateResponse>(
|
|
"/v1/admin/users",
|
|
payload,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function exportUsersCSV(
|
|
search?: string,
|
|
tenantSlug?: string,
|
|
includeIds = false,
|
|
) {
|
|
const response = await apiClient.get<Blob>("/v1/admin/users/export", {
|
|
params: { search, tenantSlug, includeIds },
|
|
responseType: "blob",
|
|
});
|
|
const dispositionHeader = response.headers["content-disposition"];
|
|
const disposition = Array.isArray(dispositionHeader)
|
|
? dispositionHeader[0]
|
|
: String(dispositionHeader ?? "");
|
|
const filenameMatch = disposition?.match(/filename="?([^"]+)"?/i);
|
|
return {
|
|
blob: response.data,
|
|
filename: filenameMatch?.[1] ?? "users.csv",
|
|
};
|
|
}
|
|
|
|
export async function bulkCreateUsers(users: BulkUserItem[]) {
|
|
const { data } = await apiClient.post<BulkUserResponse>(
|
|
"/v1/admin/users/bulk",
|
|
{ users },
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function fetchWorksmobileOverview(tenantId: string) {
|
|
const { data } = await apiClient.get<WorksmobileOverview>(
|
|
`/v1/admin/tenants/${tenantId}/worksmobile`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function fetchWorksmobileComparison(
|
|
tenantId: string,
|
|
includeMatched = false,
|
|
) {
|
|
const { data } = await apiClient.get<WorksmobileComparison>(
|
|
`/v1/admin/tenants/${tenantId}/worksmobile/comparison`,
|
|
{
|
|
params: { includeMatched },
|
|
},
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function fetchWorksmobileCredentialBatches(tenantId: string) {
|
|
const { data } = await apiClient.get<WorksmobileCredentialBatch[]>(
|
|
`/v1/admin/tenants/${tenantId}/worksmobile/credential-batches`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function downloadWorksmobileInitialPasswordsCSV(
|
|
tenantId: string,
|
|
batchId?: string,
|
|
) {
|
|
const trimmedBatchId = batchId?.trim();
|
|
const response = await apiClient.get<Blob>(
|
|
`/v1/admin/tenants/${tenantId}/worksmobile/initial-passwords.csv`,
|
|
{
|
|
...(trimmedBatchId ? { params: { batchId: trimmedBatchId } } : {}),
|
|
responseType: "blob",
|
|
},
|
|
);
|
|
const dispositionHeader = response.headers["content-disposition"];
|
|
const disposition = Array.isArray(dispositionHeader)
|
|
? dispositionHeader[0]
|
|
: String(dispositionHeader ?? "");
|
|
const filenameMatch = disposition?.match(/filename="?([^"]+)"?/i);
|
|
return {
|
|
blob: response.data,
|
|
filename: filenameMatch?.[1] ?? "worksmobile_initial_passwords.csv",
|
|
};
|
|
}
|
|
|
|
export async function deleteWorksmobileCredentialBatchPasswords(
|
|
tenantId: string,
|
|
batchId: string,
|
|
) {
|
|
const { data } = await apiClient.delete<WorksmobileCredentialBatch>(
|
|
`/v1/admin/tenants/${tenantId}/worksmobile/credential-batches/${encodeURIComponent(batchId)}/passwords`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function deleteWorksmobilePendingJobs(tenantId: string) {
|
|
const { data } = await apiClient.delete<WorksmobilePendingJobDeleteResult>(
|
|
`/v1/admin/tenants/${tenantId}/worksmobile/jobs/pending`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function enqueueWorksmobileBackfillDryRun(tenantId: string) {
|
|
const { data } = await apiClient.post(
|
|
`/v1/admin/tenants/${tenantId}/worksmobile/backfill/dry-run`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function enqueueWorksmobileOrgUnitSync(
|
|
tenantId: string,
|
|
orgUnitId: string,
|
|
) {
|
|
const { data } = await apiClient.post<WorksmobileOutboxItem>(
|
|
`/v1/admin/tenants/${tenantId}/worksmobile/orgunits/${encodeURIComponent(orgUnitId)}/sync`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function enqueueWorksmobileOrgUnitDelete(
|
|
tenantId: string,
|
|
orgUnitId: string,
|
|
) {
|
|
const { data } = await apiClient.post<WorksmobileOutboxItem>(
|
|
`/v1/admin/tenants/${tenantId}/worksmobile/orgunits/${encodeURIComponent(orgUnitId)}/delete`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function enqueueWorksmobileUserSync(
|
|
tenantId: string,
|
|
userId: string,
|
|
credentialBatchId?: string,
|
|
initialPassword?: string,
|
|
) {
|
|
const trimmedBatchId = credentialBatchId?.trim();
|
|
const trimmedInitialPassword = initialPassword?.trim();
|
|
const path = `/v1/admin/tenants/${tenantId}/worksmobile/users/${userId}/sync`;
|
|
const body = {
|
|
...(trimmedBatchId ? { credentialBatchId: trimmedBatchId } : {}),
|
|
...(trimmedInitialPassword
|
|
? { initialPassword: trimmedInitialPassword }
|
|
: {}),
|
|
};
|
|
const { data } =
|
|
Object.keys(body).length > 0
|
|
? await apiClient.post<WorksmobileOutboxItem>(path, body)
|
|
: await apiClient.post<WorksmobileOutboxItem>(path);
|
|
return data;
|
|
}
|
|
|
|
export async function resetWorksmobileUserPassword(
|
|
tenantId: string,
|
|
userId: string,
|
|
credentialBatchId?: string,
|
|
) {
|
|
const trimmedBatchId = credentialBatchId?.trim();
|
|
const path = `/v1/admin/tenants/${tenantId}/worksmobile/users/${userId}/password/reset`;
|
|
const { data } = trimmedBatchId
|
|
? await apiClient.post<WorksmobileOutboxItem>(path, {
|
|
credentialBatchId: trimmedBatchId,
|
|
})
|
|
: await apiClient.post<WorksmobileOutboxItem>(path);
|
|
return data;
|
|
}
|
|
|
|
export async function retryWorksmobileJob(tenantId: string, jobId: string) {
|
|
const { data } = await apiClient.post(
|
|
`/v1/admin/tenants/${tenantId}/worksmobile/jobs/${jobId}/retry`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function bulkUpdateUsers(payload: {
|
|
userIds: string[];
|
|
status?: string;
|
|
role?: string;
|
|
tenantSlug?: string;
|
|
isPrimaryTenant?: boolean;
|
|
isAddTenant?: boolean;
|
|
department?: string;
|
|
position?: string;
|
|
grade?: string;
|
|
jobTitle?: string;
|
|
}) {
|
|
const { data } = await apiClient.put<BulkUserResponse>(
|
|
"/v1/admin/users/bulk",
|
|
payload,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function bulkDeleteUsers(userIds: string[]) {
|
|
const { data } = await apiClient.delete("/v1/admin/users/bulk", {
|
|
data: { userIds },
|
|
});
|
|
return data;
|
|
}
|
|
|
|
export async function updateUser(userId: string, payload: UserUpdateRequest) {
|
|
const { data } = await apiClient.put<UserSummary>(
|
|
`/v1/admin/users/${userId}`,
|
|
payload,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export type PasswordPolicyResponse = {
|
|
minLength?: number;
|
|
lowercase?: boolean;
|
|
uppercase?: boolean;
|
|
number?: boolean;
|
|
nonAlphanumeric?: boolean;
|
|
minCharacterTypes?: number;
|
|
};
|
|
|
|
export async function fetchPasswordPolicy() {
|
|
const { data } = await apiClient.get<PasswordPolicyResponse>(
|
|
"/v1/auth/password/policy",
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function deleteUser(userId: string) {
|
|
await apiClient.delete(`/v1/admin/users/${userId}`);
|
|
}
|
|
|
|
export type UserRpHistoryItem = {
|
|
client_id: string;
|
|
client_name: string;
|
|
lastLoginAt: string;
|
|
status: string;
|
|
};
|
|
|
|
export async function fetchUserRpHistory(userId: string) {
|
|
const { data } = await apiClient.get<UserRpHistoryItem[]>(
|
|
`/v1/admin/users/${userId}/rp-history`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export type SystemPermissions = {
|
|
overview: boolean;
|
|
tenants: boolean;
|
|
org_chart: boolean;
|
|
worksmobile: boolean;
|
|
ory_ssot: boolean;
|
|
data_integrity: boolean;
|
|
users: boolean;
|
|
permissions_direct: boolean;
|
|
auth_guard: boolean;
|
|
api_keys: boolean;
|
|
audit_logs: boolean;
|
|
|
|
manage_overview?: boolean;
|
|
manage_tenants?: boolean;
|
|
manage_org_chart?: boolean;
|
|
manage_worksmobile?: boolean;
|
|
manage_ory_ssot?: boolean;
|
|
manage_data_integrity?: boolean;
|
|
manage_users?: boolean;
|
|
manage_permissions_direct?: boolean;
|
|
manage_auth_guard?: boolean;
|
|
manage_api_keys?: boolean;
|
|
manage_audit_logs?: boolean;
|
|
};
|
|
|
|
export type UserProfileResponse = {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
phone: string;
|
|
role: string;
|
|
department: string;
|
|
affiliationType: string;
|
|
tenantSlug?: string;
|
|
tenantId?: string;
|
|
metadata?: Record<string, unknown>;
|
|
tenant?: TenantSummary;
|
|
manageableTenants?: TenantSummary[];
|
|
systemPermissions?: SystemPermissions;
|
|
};
|
|
|
|
export async function fetchMe() {
|
|
const { data } = await apiClient.get<UserProfileResponse>("/v1/user/me");
|
|
return data;
|
|
}
|
|
|
|
// Relying Party Management
|
|
export type RelyingParty = {
|
|
clientId: string;
|
|
tenantId: string;
|
|
name: string;
|
|
description: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type HydraClientReq = {
|
|
client_id?: string;
|
|
client_name: string;
|
|
client_secret?: string;
|
|
redirect_uris: string[];
|
|
scope?: string;
|
|
token_endpoint_auth_method?: string;
|
|
grant_types?: string[];
|
|
response_types?: string[];
|
|
metadata?: Record<string, unknown>;
|
|
};
|
|
|
|
export async function fetchRelyingParties(tenantId: string) {
|
|
const { data } = await apiClient.get<RelyingParty[]>(
|
|
`/v1/admin/tenants/${tenantId}/relying-parties`,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function fetchAllRelyingParties() {
|
|
const { data } = await apiClient.get<RelyingParty[]>(
|
|
"/v1/admin/relying-parties",
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function createRelyingParty(
|
|
tenantId: string,
|
|
payload: HydraClientReq,
|
|
) {
|
|
const { data } = await apiClient.post<RelyingParty>(
|
|
`/v1/admin/tenants/${tenantId}/relying-parties`,
|
|
payload,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function fetchRelyingParty(id: string) {
|
|
const { data } = await apiClient.get<{
|
|
relyingParty: RelyingParty;
|
|
oauth2Config: HydraClientReq;
|
|
}>(`/v1/admin/relying-parties/${id}`);
|
|
return data;
|
|
}
|
|
|
|
export async function updateRelyingParty(id: string, payload: HydraClientReq) {
|
|
const { data } = await apiClient.put<RelyingParty>(
|
|
`/v1/admin/relying-parties/${id}`,
|
|
payload,
|
|
);
|
|
return data;
|
|
}
|
|
|
|
export async function deleteRelyingParty(id: string) {
|
|
await apiClient.delete(`/v1/admin/relying-parties/${id}`);
|
|
}
|
|
|
|
export type RPOwner = {
|
|
subject: string;
|
|
|
|
name?: string;
|
|
|
|
email?: string;
|
|
|
|
type: string;
|
|
};
|
|
|
|
export async function fetchRPOwners(clientId: string) {
|
|
const { data } = await apiClient.get<RPOwner[]>(
|
|
`/v1/admin/relying-parties/${clientId}/owners`,
|
|
);
|
|
|
|
return data;
|
|
}
|
|
|
|
export async function addRPOwner(clientId: string, subject: string) {
|
|
await apiClient.post(
|
|
`/v1/admin/relying-parties/${clientId}/owners/${subject}`,
|
|
);
|
|
}
|
|
|
|
export async function removeRPOwner(clientId: string, subject: string) {
|
|
await apiClient.delete(
|
|
`/v1/admin/relying-parties/${clientId}/owners/${subject}`,
|
|
);
|
|
}
|