From d9f983b08e3572fe65521a2224077570227b8e46 Mon Sep 17 00:00:00 2001 From: Hampus Date: Sun, 6 Sep 2026 18:38:46 +0200 Subject: [PATCH] fix(api): return the terminated count from terminate sessions (#2539) --- .../services/AdminUserSecurityService.ts | 3 ++- fluxer_api/src/api/auth/AuthSession.ts | 5 ++-- fluxer_docs/scripts/VerifyDocsSchemas.ts | 23 ++----------------- .../src/content/docs/admin-api/users.mdx | 10 +++++--- 4 files changed, 14 insertions(+), 27 deletions(-) diff --git a/fluxer_api/src/api/admin/services/AdminUserSecurityService.ts b/fluxer_api/src/api/admin/services/AdminUserSecurityService.ts index e142affa0..3d8ab5e68 100644 --- a/fluxer_api/src/api/admin/services/AdminUserSecurityService.ts +++ b/fluxer_api/src/api/admin/services/AdminUserSecurityService.ts @@ -315,7 +315,7 @@ export class AdminUserSecurityService { if (!user) { throw new UnknownUserError(); } - await AuthSession.terminateAllUserSessions(this.deps.apiContext, userId); + const terminatedCount = await AuthSession.terminateAllUserSessions(this.deps.apiContext, userId); await auditService.createAuditLog({ adminUserId, targetType: 'user', @@ -324,6 +324,7 @@ export class AdminUserSecurityService { auditLogReason, metadata: new Map(), }); + return {terminated_count: terminatedCount}; } async setUserAcls( diff --git a/fluxer_api/src/api/auth/AuthSession.ts b/fluxer_api/src/api/auth/AuthSession.ts index 5435bd885..f519992a6 100644 --- a/fluxer_api/src/api/auth/AuthSession.ts +++ b/fluxer_api/src/api/auth/AuthSession.ts @@ -178,18 +178,19 @@ export async function logoutAuthSessions( }); } -export async function terminateAllUserSessions(ctx: ApiContext, userId: UserID): Promise { +export async function terminateAllUserSessions(ctx: ApiContext, userId: UserID): Promise { const {users, gateway} = ctx.services; const authSessions = await users.listAuthSessions(userId); await users.deleteAllPushSubscriptions(userId); await gateway.invalidatePushSubscriptions({userId}); - if (authSessions.length === 0) return; + if (authSessions.length === 0) return 0; const hashes = authSessions.map((s) => s.sessionIdHash); await users.deleteAuthSessions(userId, hashes); await gateway.terminateSession({ userId, sessionIdHashes: authSessions.map((s) => Buffer.from(s.sessionIdHash).toString('base64url')), }); + return authSessions.length; } export async function replaceCurrentAuthSession( diff --git a/fluxer_docs/scripts/VerifyDocsSchemas.ts b/fluxer_docs/scripts/VerifyDocsSchemas.ts index f19964ed7..d7b13afa9 100644 --- a/fluxer_docs/scripts/VerifyDocsSchemas.ts +++ b/fluxer_docs/scripts/VerifyDocsSchemas.ts @@ -41,8 +41,6 @@ const STREAMED_RESPONSE_ROUTES = new Map([ ['POST /admin/system/heap-snapshots', "'Content-Type': 'application/octet-stream'"], ]); -const EMPTY_RESPONSE_ROUTES = new Map([['DELETE /admin/users/{}/sessions', 'terminated_count']]); - const MERGED_SCHEMA_ROUTES = new Map([ ['POST /auth/sessions/logout', "Validator('json', LogoutAuthSessionsRequest.merge(SudoVerificationSchema))"], [ @@ -513,7 +511,7 @@ for (const file of await walk(DOCS_ROOT)) { const resolvedTarget = resolveRef(spec, target); const responseIsUnion = resolvedTarget != null && ((resolvedTarget.oneOf ?? []).length > 0 || (resolvedTarget.anyOf ?? []).length > 0); - if (!STREAMED_RESPONSE_ROUTES.has(key) && !EMPTY_RESPONSE_ROUTES.has(key)) { + if (!STREAMED_RESPONSE_ROUTES.has(key)) { const referencedTypes = new Map(); for (const anchor of referenced) { for (const [field, type] of anchorTypes.get(anchor) ?? []) { @@ -542,12 +540,7 @@ for (const file of await walk(DOCS_ROOT)) { }); } } - if ( - responseProperties.size > 0 && - !responseIsUnion && - !STREAMED_RESPONSE_ROUTES.has(key) && - !EMPTY_RESPONSE_ROUTES.has(key) - ) { + if (responseProperties.size > 0 && !responseIsUnion && !STREAMED_RESPONSE_ROUTES.has(key)) { responsesChecked += 1; for (const field of responseProperties) { if (pageFields.has(field) || referencedFields.has(field)) { @@ -707,15 +700,6 @@ for (const [route, anchor] of STREAMED_RESPONSE_ROUTES) { staleExemptions.push(`${route}: no longer streams a file, drop this exemption`); } } -const adminUserSecuritySource = await readFile( - path.join(REPO_ROOT, 'fluxer_api/src/api/admin/services/AdminUserSecurityService.ts'), - 'utf8', -); -for (const [route, marker] of EMPTY_RESPONSE_ROUTES) { - if (adminUserSecuritySource.includes(marker)) { - staleExemptions.push(`${route}: the handler now emits ${marker}, drop this exemption`); - } -} for (const [route, anchor] of MERGED_SCHEMA_ROUTES) { const present = authSource.includes(anchor) || guildSource.includes(anchor); if (!present) { @@ -743,9 +727,6 @@ console.log(`success response schemas checked: ${responsesChecked.toString()}`); console.log( `streamed-response exemptions active: ${STREAMED_RESPONSE_ROUTES.size.toString()} (the spec declares JSON, the implementation streams a file)`, ); -console.log( - `empty-response exemptions active: ${EMPTY_RESPONSE_ROUTES.size.toString()} (the spec declares a body, the handler returns none)`, -); console.log(`response fields found documented on the page: ${responseFieldsFound.toString()}`); console.log(`request body field types compared: ${typesCompared.toString()}`); console.log(`request body optionality compared: ${optionalityCompared.toString()}`); diff --git a/fluxer_docs/src/content/docs/admin-api/users.mdx b/fluxer_docs/src/content/docs/admin-api/users.mdx index 9060c9866..cec4a410a 100644 --- a/fluxer_docs/src/content/docs/admin-api/users.mdx +++ b/fluxer_docs/src/content/docs/admin-api/users.mdx @@ -1838,12 +1838,16 @@ There is no operation that revokes one session. The account's Admin API keys, bo | Status | Body | Condition | | --- | --- | --- | -| 2001 | empty2 | Active sessions were terminated | +| 2001 | [terminate sessions response](#terminate-sessions-response) | Active sessions were terminated | | 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist | -1 An account with no live session is accepted and still records the audit entry +1 An account with no live session is accepted, returns `terminated_count` 0, and still records the audit entry -2 The handler emits no body at all, so a client MUST NOT read the `terminated_count` member the spec declares +#### Terminate sessions response + +| Field | Type | Description | +| --- | --- | --- | +| terminated_count | integer | The number of sessions that were terminated | :::caution[A terminated session cannot be restored] The tombstone stays visible through [List user sessions](#list-user-sessions), but the session itself is gone.