Audit and fix Swagger/OpenAPI accuracy; regenerate served spec #40
@@ -30,7 +30,7 @@ adminRouter.get(
|
||||
// #swagger.tags = ['Admin · Account']
|
||||
// #swagger.summary = 'Get the current account (self)'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
/* #swagger.responses[200] = { description: 'The account', content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } } } */
|
||||
/* #swagger.responses[200] = { description: 'The account', content: { "application/json": { schema: { $ref: "#/components/schemas/AccountStatus" } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
account.getAccount,
|
||||
)
|
||||
@@ -39,7 +39,7 @@ adminRouter.post(
|
||||
// #swagger.tags = ['Admin · Account']
|
||||
// #swagger.summary = 'Begin 2FA enrollment (returns secret + QR)'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
/* #swagger.responses[200] = { description: 'otpauth URL and QR data to scan', content: { "application/json": { schema: { type: "object", properties: { otpauth_url: { type: "string" }, qr: { type: "string" } } } } } } */
|
||||
/* #swagger.responses[200] = { description: 'otpauth URL and QR data to scan', content: { "application/json": { schema: { $ref: "#/components/schemas/TotpSetup" } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[409] = { description: 'Two-factor already enabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
account.totpSetup,
|
||||
@@ -50,7 +50,7 @@ adminRouter.post(
|
||||
// #swagger.summary = 'Enable 2FA by confirming a code'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/TotpCodeRequest" } } } } */
|
||||
/* #swagger.responses[200] = { description: '2FA enabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
||||
/* #swagger.responses[200] = { description: '2FA enabled', content: { "application/json": { schema: { $ref: "#/components/schemas/TotpState" } } } } */
|
||||
/* #swagger.responses[400] = { description: 'Setup not started, or invalid code', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[409] = { description: 'Two-factor already enabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
@@ -64,7 +64,7 @@ adminRouter.post(
|
||||
// #swagger.summary = 'Disable 2FA by confirming a code'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/TotpCodeRequest" } } } } */
|
||||
/* #swagger.responses[200] = { description: '2FA disabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
||||
/* #swagger.responses[200] = { description: '2FA disabled', content: { "application/json": { schema: { $ref: "#/components/schemas/TotpState" } } } } */
|
||||
/* #swagger.responses[400] = { description: 'Not enabled, or invalid code', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
body('code').isString().trim().isLength({ min: 6, max: 8 }),
|
||||
@@ -78,7 +78,7 @@ adminRouter.get(
|
||||
// #swagger.tags = ['Admin · Account']
|
||||
// #swagger.summary = 'List linked SSO identities (self)'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
/* #swagger.responses[200] = { description: 'Linked identities', content: { "application/json": { schema: { type: "array", items: { type: "object", properties: { provider: { type: "string" }, email: { type: "string" } } } } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Linked identities', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/LinkedIdentity" } } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
account.listIdentities,
|
||||
)
|
||||
@@ -88,7 +88,7 @@ adminRouter.delete(
|
||||
// #swagger.summary = 'Unlink an SSO identity (self)'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
// #swagger.parameters['provider'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id.' }
|
||||
/* #swagger.responses[200] = { description: 'Unlinked', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Unlinked', content: { "application/json": { schema: { $ref: "#/components/schemas/UnlinkedFlag" } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[404] = { description: 'No linked account for that provider', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
param('provider').matches(/^[a-z0-9-]+$/),
|
||||
@@ -136,7 +136,7 @@ adminRouter.get(
|
||||
// #swagger.tags = ['Admin · Dashboard']
|
||||
// #swagger.summary = 'Dashboard summary counts'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
/* #swagger.responses[200] = { description: 'Summary counts (posts, wiki, users, site mode)', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Summary: site mode, last change, post/user counts and recent activity', content: { "application/json": { schema: { type: "object", properties: { site_mode: { type: "string", example: "live" }, last_change: { type: "object", properties: { at: { type: "string", nullable: true }, by: { type: "string", nullable: true } } }, counts: { type: "object", properties: { posts: { type: "object", additionalProperties: true }, users: { type: "integer" } } }, recent_activity: { type: "array", items: { type: "object", additionalProperties: true } } } } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
ctrl.dashboard,
|
||||
)
|
||||
@@ -147,7 +147,7 @@ adminRouter.put(
|
||||
// #swagger.description = 'Switch the site between live and maintenance.'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/SiteModeRequest" } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Updated site mode', content: { "application/json": { schema: { type: "object", properties: { mode: { type: "string", example: "maintenance" } } } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Updated site mode', content: { "application/json": { schema: { $ref: "#/components/schemas/SiteModeState" } } } } */
|
||||
/* #swagger.responses[400] = { description: 'Validation error', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
@@ -257,7 +257,7 @@ adminRouter.delete(
|
||||
// #swagger.summary = 'Delete a post'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Post id.' }
|
||||
/* #swagger.responses[200] = { description: 'Deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Deleted (echoes the id)', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedId" } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
param('id').isInt(),
|
||||
@@ -318,7 +318,7 @@ adminRouter.delete(
|
||||
// #swagger.summary = 'Delete a wiki category'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Category id.' }
|
||||
/* #swagger.responses[200] = { description: 'Deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Deleted (echoes the id)', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedId" } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
param('id').isInt(),
|
||||
@@ -457,7 +457,7 @@ adminRouter.delete(
|
||||
// #swagger.summary = 'Delete a wiki page'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
// #swagger.parameters['slug'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Wiki page slug.' }
|
||||
/* #swagger.responses[200] = { description: 'Deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Deleted (echoes the slug)', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedSlug" } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
ctrl.deleteWiki,
|
||||
@@ -521,7 +521,7 @@ adminRouter.post(
|
||||
// #swagger.summary = 'Emergency unban an IP (admin only)'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/UnbanRequest" } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Unbanned', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Unbanned (echoes the ip and whether an entry was cleared)', content: { "application/json": { schema: { $ref: "#/components/schemas/UnbanResult" } } } } */
|
||||
/* #swagger.responses[400] = { description: 'Invalid IP', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
@@ -636,7 +636,7 @@ adminRouter.delete(
|
||||
// #swagger.description = 'Built-in providers cannot be deleted — disable them instead.'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id.' }
|
||||
/* #swagger.responses[200] = { description: 'Deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedFlag" } } } } */
|
||||
/* #swagger.responses[400] = { description: 'Built-in provider cannot be deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
@@ -702,7 +702,7 @@ adminRouter.delete(
|
||||
// #swagger.summary = 'Delete a user (admin only)'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
|
||||
/* #swagger.responses[200] = { description: 'Deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Deleted (echoes the id)', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedId" } } } } */
|
||||
/* #swagger.responses[400] = { description: 'Cannot delete your own account or the last admin', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
|
||||
@@ -73,7 +73,7 @@ ssoRouter.post(
|
||||
// #swagger.summary = 'Complete an SSO login with a TOTP code'
|
||||
// #swagger.description = 'Second step when a linked account has 2FA enabled. Reads the staged pending-TOTP cookie set by the callback plus the current authenticator code, and on success sets the session cookie. Rate limited and behind bot/backoff guards.'
|
||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", required: ["code"], properties: { code: { type: "string" } } } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Session issued', content: { "application/json": { schema: { type: "object", properties: { user: { $ref: "#/components/schemas/User" }, returnTo: { type: "string" } } } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Session issued', content: { "application/json": { schema: { type: "object", properties: { user: { $ref: "#/components/schemas/SafeUser" }, returnTo: { type: "string" } } } } } } */
|
||||
/* #swagger.responses[401] = { description: 'Invalid code or expired challenge', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
/* #swagger.responses[429] = { description: 'Too many attempts (rate limited / backoff)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
...loginGuards,
|
||||
|
||||
@@ -22,7 +22,7 @@ publicRouter.get(
|
||||
// #swagger.tags = ['Public']
|
||||
// #swagger.summary = 'Site mode / status'
|
||||
// #swagger.description = 'Current site mode (live or maintenance) so the client can show the maintenance page.'
|
||||
/* #swagger.responses[200] = { description: 'Site status', content: { "application/json": { schema: { type: "object", properties: { mode: { type: "string", example: "live" } } } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Site status', content: { "application/json": { schema: { $ref: "#/components/schemas/PublicStatus" } } } } */
|
||||
ctrl.getStatus,
|
||||
)
|
||||
publicRouter.post(
|
||||
|
||||
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