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:
saravanakumardb1 2026-03-20 00:44:49 -07:00
parent 20663d7078
commit d073122a48
2 changed files with 78 additions and 0 deletions

View File

@ -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);
}

View File

@ -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' });
});
}