feat(scim): deepen SCIM provisioning — stats, delete, pause/resume
- repository.ts: add ScimConnectorStats interface, getConnectorStats (user/group/event counts), deleteConnector - routes.ts: 4 new endpoints — GET /scim/connectors/:orgId/:id/stats, DELETE /scim/connectors/:orgId/:id (must be paused first), POST /scim/connectors/:orgId/:id/pause, POST /scim/connectors/:orgId/:id/resume - Existing 4 tests unchanged, typecheck clean
This commit is contained in:
parent
20663d7078
commit
d073122a48
@ -91,3 +91,39 @@ export async function listEvents(connectorId: string): Promise<ScimProvisioningE
|
||||
limit: 500,
|
||||
});
|
||||
}
|
||||
|
||||
export interface ScimConnectorStats {
|
||||
connectorId: string;
|
||||
userCount: number;
|
||||
groupCount: number;
|
||||
eventCount: number;
|
||||
provisionedUsers: number;
|
||||
failedUsers: number;
|
||||
provisionedGroups: number;
|
||||
failedGroups: number;
|
||||
lastEventAt: string | null;
|
||||
}
|
||||
|
||||
export async function getConnectorStats(connectorId: string): Promise<ScimConnectorStats> {
|
||||
const [users, groups, events] = await Promise.all([
|
||||
listUserSync(connectorId),
|
||||
listGroupSync(connectorId),
|
||||
listEvents(connectorId),
|
||||
]);
|
||||
|
||||
return {
|
||||
connectorId,
|
||||
userCount: users.length,
|
||||
groupCount: groups.length,
|
||||
eventCount: events.length,
|
||||
provisionedUsers: users.filter(u => u.status === 'provisioned').length,
|
||||
failedUsers: users.filter(u => u.status === 'failed').length,
|
||||
provisionedGroups: groups.filter(g => g.status === 'provisioned').length,
|
||||
failedGroups: groups.filter(g => g.status === 'failed').length,
|
||||
lastEventAt: events[0]?.createdAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteConnector(id: string, orgId: string): Promise<void> {
|
||||
await connectorCollection().delete(id, orgId);
|
||||
}
|
||||
|
||||
@ -175,4 +175,46 @@ export async function scimRoutes(app: FastifyInstance) {
|
||||
};
|
||||
return repo.createEvent(doc);
|
||||
});
|
||||
|
||||
// ── Connector stats ────────────────────────────────────
|
||||
app.get('/scim/connectors/:orgId/:id/stats', async req => {
|
||||
requireAdmin(req);
|
||||
const { orgId, id } = req.params as { orgId: string; id: string };
|
||||
await repo.getConnector(id, orgId);
|
||||
return repo.getConnectorStats(id);
|
||||
});
|
||||
|
||||
// ── Delete connector (paused only) ─────────────────────
|
||||
app.delete('/scim/connectors/:orgId/:id', async req => {
|
||||
requireAdmin(req);
|
||||
const { orgId, id } = req.params as { orgId: string; id: string };
|
||||
const connector = await repo.getConnector(id, orgId);
|
||||
if (connector.status === 'active') {
|
||||
throw new BadRequestError('Pause the connector before deleting it');
|
||||
}
|
||||
await repo.deleteConnector(id, orgId);
|
||||
return { deleted: true };
|
||||
});
|
||||
|
||||
// ── Pause connector ────────────────────────────────────
|
||||
app.post('/scim/connectors/:orgId/:id/pause', async req => {
|
||||
requireAdmin(req);
|
||||
const { orgId, id } = req.params as { orgId: string; id: string };
|
||||
const connector = await repo.getConnector(id, orgId);
|
||||
if (connector.status !== 'active') {
|
||||
throw new BadRequestError(`Cannot pause connector with status '${connector.status}'`);
|
||||
}
|
||||
return repo.updateConnector(id, orgId, { status: 'paused' });
|
||||
});
|
||||
|
||||
// ── Resume connector ───────────────────────────────────
|
||||
app.post('/scim/connectors/:orgId/:id/resume', async req => {
|
||||
requireAdmin(req);
|
||||
const { orgId, id } = req.params as { orgId: string; id: string };
|
||||
const connector = await repo.getConnector(id, orgId);
|
||||
if (connector.status !== 'paused') {
|
||||
throw new BadRequestError(`Cannot resume connector with status '${connector.status}'`);
|
||||
}
|
||||
return repo.updateConnector(id, orgId, { status: 'active' });
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user