Audit and fix Swagger/OpenAPI accuracy; regenerate served spec
The route-level annotations were 100% present, but the committed/served
spec (swagger-output.json) was stale and several response schemas had
drifted from the controllers. This aligns the docs with actual behavior
and regenerates the spec.
Served spec was stale (64/67 operations). Regenerating picks up three
routes that were added after the last generation:
- POST /api/v1/auth/sso/totp
- GET /api/v1/admin/discord-bot/config
- PUT /api/v1/admin/discord-bot/config
plus a stale /auth/logout summary.
Response-shape corrections (annotation now matches controller output):
- Mutation endpoints do NOT return the generic { message } envelope.
Deletes echo { id } / { slug }; toggles return { deleted },
{ unlinked }, { totp_enabled }, or { ip, removed }. Documented as-is
via new DeletedId/DeletedSlug/DeletedFlag/UnlinkedFlag/TotpState/
UnbanResult components. (The API is intentionally inconsistent here;
recorded rather than normalized — see follow-up note.)
- POST /account/totp/setup: otpauth_url -> otpauthUrl (TotpSetup)
- PUT /admin/site-mode: { mode } -> { site_mode, changed_at, changed_by }
- GET /account: full User -> AccountStatus (id/username/role/totp_enabled)
- GET /account/identities: add linked_at (LinkedIdentity)
- GET /public/status: add status_message (PublicStatus)
- POST /auth/sso/totp: user is SafeUser, not full User
- GET /dashboard: description/shape corrected (posts+users, no wiki)
Schema completeness:
- Provider (public discovery): { id, name, icon, loginUrl, priority },
not { id, name, kind }
- ProviderConfig: add hasSecret, builtin, health (ProviderHealth)
- Post: add excerpt, author_id, published_at
- MobileTokenResponse.expiresIn: duration string ("15m"), not integer
Config: declare the Admin · Discord Bot tag (was used but undeclared).
Auth model and the internal/external boundary were verified correct and
left unchanged: cookie + bearer are both accepted on session routes (dual
security annotations are accurate), and /internal/* runs on a separate
listener already excluded from the scan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,7 @@ const doc = {
|
||||
{ name: 'Admin · Settings', description: 'Site settings (admin only)' },
|
||||
{ name: 'Admin · Activity', description: 'Admin activity log' },
|
||||
{ name: 'Admin · Bot Activity', description: 'Bot-scoring/ban state and emergency unban (admin only)' },
|
||||
{ name: 'Admin · Discord Bot', description: 'Discord bot token/config and live status (admin only)' },
|
||||
{ name: 'Admin · Auth Providers', description: 'SSO provider configuration (admin only)' },
|
||||
{ name: 'Admin · Users', description: 'User management (admin only)' },
|
||||
],
|
||||
@@ -144,7 +145,11 @@ const doc = {
|
||||
properties: {
|
||||
accessToken: { type: 'string', description: 'Short-lived bearer JWT.' },
|
||||
refreshToken: { type: 'string', description: 'Long-lived, revocable refresh token.' },
|
||||
expiresIn: { type: 'integer', description: 'Access token lifetime in seconds.', example: 900 },
|
||||
expiresIn: {
|
||||
type: 'string',
|
||||
description: 'Access token lifetime as a duration string (zeit/ms format, e.g. "15m").',
|
||||
example: '15m',
|
||||
},
|
||||
user: { $ref: '#/components/schemas/SafeUser' },
|
||||
},
|
||||
},
|
||||
@@ -173,27 +178,59 @@ const doc = {
|
||||
name: { type: 'string', maxLength: 100, example: 'Lord British' },
|
||||
},
|
||||
},
|
||||
// Public discovery shape (GET /auth/providers) — enough for the login page
|
||||
// to render a button and start the flow. Never exposes secrets or endpoints.
|
||||
Provider: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', example: 'google' },
|
||||
name: { type: 'string', example: 'Google' },
|
||||
kind: { type: 'string', enum: ['oidc', 'oauth2'], example: 'oidc' },
|
||||
icon: {
|
||||
type: 'string',
|
||||
description: "Icon hint — the provider kind ('google' | 'discord' | 'oidc' | 'oauth2').",
|
||||
example: 'google',
|
||||
},
|
||||
loginUrl: {
|
||||
type: 'string',
|
||||
description: 'Relative URL to begin the redirect flow.',
|
||||
example: '/api/v1/auth/sso/google/start',
|
||||
},
|
||||
priority: { type: 'integer', description: 'Sort order (ascending).', example: 1 },
|
||||
},
|
||||
},
|
||||
// Admin-facing provider config (GET/POST/PUT /admin/auth/providers). The
|
||||
// client secret is write-only and NEVER returned — `hasSecret` reports
|
||||
// whether one is stored. `builtin` marks google/discord (fixed kind/name),
|
||||
// and `health` is the config-completeness check used to gate visibility.
|
||||
ProviderConfig: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', example: 'okta' },
|
||||
kind: { type: 'string', enum: ['oidc', 'oauth2'], example: 'oidc' },
|
||||
kind: { type: 'string', enum: ['google', 'discord', 'oidc', 'oauth2'], example: 'oidc' },
|
||||
name: { type: 'string', example: 'Okta' },
|
||||
enabled: { type: 'boolean', example: true },
|
||||
clientId: { type: 'string' },
|
||||
hasSecret: { type: 'boolean', description: 'Whether a client secret is stored (the secret itself is never returned).', example: true },
|
||||
authorizeUrl: { type: 'string', format: 'uri' },
|
||||
tokenUrl: { type: 'string', format: 'uri' },
|
||||
userinfoUrl: { type: 'string', format: 'uri' },
|
||||
scopes: { type: 'string', example: 'openid email profile' },
|
||||
priority: { type: 'integer', example: 10 },
|
||||
builtin: { type: 'boolean', description: 'True for the fixed google/discord providers.', example: false },
|
||||
health: { $ref: '#/components/schemas/ProviderHealth' },
|
||||
},
|
||||
},
|
||||
ProviderHealth: {
|
||||
type: 'object',
|
||||
description: 'Config-completeness check that gates whether a provider is offered to end users.',
|
||||
properties: {
|
||||
valid: { type: 'boolean', example: true },
|
||||
missing: {
|
||||
type: 'array',
|
||||
description: 'Names of required config fields that are still missing.',
|
||||
items: { type: 'string' },
|
||||
example: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
ProviderCreateRequest: {
|
||||
@@ -220,11 +257,14 @@ const doc = {
|
||||
category: { type: 'string', example: 'news' },
|
||||
title: { type: 'string', example: 'Server maintenance this weekend' },
|
||||
slug: { type: 'string', example: 'server-maintenance-this-weekend' },
|
||||
body: { type: 'string' },
|
||||
image_url: { type: 'string', example: '/uploads/1700000000-abcd.png' },
|
||||
excerpt: { type: 'string', nullable: true },
|
||||
body: { type: 'string', nullable: true },
|
||||
image_url: { type: 'string', nullable: true, example: '/uploads/1700000000-abcd.png' },
|
||||
published: { type: 'boolean', example: true },
|
||||
author_id: { type: 'integer', nullable: true, example: 1 },
|
||||
created_at: { type: 'string', format: 'date-time' },
|
||||
updated_at: { type: 'string', format: 'date-time' },
|
||||
published_at: { type: 'string', format: 'date-time', nullable: true },
|
||||
},
|
||||
},
|
||||
PostCreateRequest: {
|
||||
@@ -330,6 +370,84 @@ const doc = {
|
||||
required: ['ip'],
|
||||
properties: { ip: { type: 'string', example: '203.0.113.5' } },
|
||||
},
|
||||
// ── Actual mutation-response shapes ─────────────────────────────────────
|
||||
// These endpoints do NOT return the generic { message } envelope; they echo
|
||||
// the affected resource id/slug or a boolean flag. Documented here as-is so
|
||||
// the spec matches the controllers. (The shapes are intentionally recorded
|
||||
// rather than normalized — see the audit note if standardizing later.)
|
||||
AccountStatus: {
|
||||
type: 'object',
|
||||
description: 'Self-service account security status (GET /admin/account).',
|
||||
properties: {
|
||||
id: { type: 'integer', example: 1 },
|
||||
username: { type: 'string', example: 'admin' },
|
||||
role: { type: 'string', enum: ['admin', 'editor'], example: 'admin' },
|
||||
totp_enabled: { type: 'boolean', example: true },
|
||||
},
|
||||
},
|
||||
TotpSetup: {
|
||||
type: 'object',
|
||||
description: 'Enrollment material returned by POST /account/totp/setup.',
|
||||
properties: {
|
||||
otpauthUrl: { type: 'string', example: 'otpauth://totp/UOMysticmoon:admin?secret=...' },
|
||||
qr: { type: 'string', description: 'QR code as a data: URL.', example: 'data:image/png;base64,iVBORw0KGgo...' },
|
||||
},
|
||||
},
|
||||
TotpState: {
|
||||
type: 'object',
|
||||
description: 'Result of enabling/disabling 2FA.',
|
||||
properties: { totp_enabled: { type: 'boolean', example: true } },
|
||||
},
|
||||
LinkedIdentity: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
provider: { type: 'string', example: 'google' },
|
||||
email: { type: 'string', format: 'email', nullable: true, example: 'user@example.com' },
|
||||
linked_at: { type: 'string', format: 'date-time' },
|
||||
},
|
||||
},
|
||||
SiteModeState: {
|
||||
type: 'object',
|
||||
description: 'Result of PUT /admin/site-mode.',
|
||||
properties: {
|
||||
site_mode: { type: 'string', enum: ['live', 'maintenance'], example: 'maintenance' },
|
||||
changed_at: { type: 'string', format: 'date-time' },
|
||||
changed_by: { type: 'string', example: 'admin' },
|
||||
},
|
||||
},
|
||||
PublicStatus: {
|
||||
type: 'object',
|
||||
description: 'Public site status (GET /public/status).',
|
||||
properties: {
|
||||
mode: { type: 'string', enum: ['live', 'maintenance'], example: 'live' },
|
||||
status_message: { type: 'string', example: '' },
|
||||
},
|
||||
},
|
||||
// Delete/mutation acknowledgements — each echoes the affected resource key
|
||||
// or a boolean flag rather than a { message } string.
|
||||
DeletedId: {
|
||||
type: 'object',
|
||||
properties: { id: { type: 'integer', example: 12 } },
|
||||
},
|
||||
DeletedSlug: {
|
||||
type: 'object',
|
||||
properties: { slug: { type: 'string', example: 'getting-started' } },
|
||||
},
|
||||
DeletedFlag: {
|
||||
type: 'object',
|
||||
properties: { deleted: { type: 'boolean', example: true } },
|
||||
},
|
||||
UnlinkedFlag: {
|
||||
type: 'object',
|
||||
properties: { unlinked: { type: 'boolean', example: true } },
|
||||
},
|
||||
UnbanResult: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ip: { type: 'string', example: '203.0.113.5' },
|
||||
removed: { type: 'boolean', description: 'Whether the IP had an entry that was cleared.', example: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user