- admin/uoLink.controller.js: GET /admin/uo-link/config (masked config + live health + ingestion stats from the socket/broadcaster); PUT to save base/ws URL + write-only token + protocol + enabled, which (re)starts or stops the WS ingest client and activity-logs the change; POST/DELETE /uo-link/towncrier to publish/remove town-crier messages; GET /uo-link/stream (admin SSE channel, full feed incl. audit/cheat). Mounted adminOnly with express-validator guards + #swagger annotations (new "Admin · Shard" tag, TownCrierRequest schema). - server.js: startup probe (checkUoLink) that logs reachability and warns loudly on a protocol mismatch when the integration is enabled. - client: api.admin uo-link methods; ShardAdmin.jsx control panel (status panel with ingestion stats, config form, town crier) modeled on DiscordBotAdmin; wired into AdminLayout nav/titles + the /admin/shard route. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3
608 lines
27 KiB
JavaScript
608 lines
27 KiB
JavaScript
// ── OpenAPI spec generator (swagger-autogen) ───────────────────────────────
|
|
//
|
|
// Static-analyzes the Express routers and emits `swagger-output.json`, which is
|
|
// served by swagger-ui-express at /api/docs (see src/app.js). Per-endpoint
|
|
// details — tags, summaries, parameters, request bodies, security and response
|
|
// codes — live as `#swagger.*` comments next to each route in
|
|
// src/router/**. This file supplies everything shared: API metadata, servers,
|
|
// tag descriptions, the two auth schemes (session cookie + mobile bearer), and
|
|
// the reusable component schemas the annotations reference.
|
|
//
|
|
// Regenerate with: npm run swagger (from the server/ directory)
|
|
// The generated JSON is committed so the docs work without a build step.
|
|
|
|
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' })
|
|
const pkg = require('../package.json')
|
|
|
|
const outputFile = './swagger/swagger-output.json'
|
|
|
|
// Entry point of the routing graph. swagger-autogen follows the `app.use(...)`
|
|
// mount chain from here (/api → /v1 → auth|public|admin), so generated paths are
|
|
// fully-qualified (e.g. /api/v1/auth/login).
|
|
const routes = ['./src/app.js']
|
|
|
|
const doc = {
|
|
info: {
|
|
title: 'UOMysticmoon API',
|
|
version: pkg.version,
|
|
description:
|
|
'REST API for the UOMysticmoon website, wiki and admin panel — a private ' +
|
|
'Ultima Online shard.\n\n' +
|
|
'### Authentication\n' +
|
|
'- **Web / admin panel** uses an httpOnly session cookie (`uomm_token`) issued by ' +
|
|
'`POST /api/v1/auth/login` (plus `/login/totp` when 2FA is enabled).\n' +
|
|
'- **Native / mobile clients** use bearer access tokens from ' +
|
|
'`POST /api/v1/auth/mobile/login`, refreshed via `/auth/mobile/refresh`.\n\n' +
|
|
'Endpoints under `/api/v1/admin/**` require a valid session; some are further ' +
|
|
'restricted to the `admin` role (editors are limited to content).',
|
|
},
|
|
servers: [
|
|
{ url: '/', description: 'Same-origin (current host)' },
|
|
{ url: 'http://localhost:3000', description: 'Local development' },
|
|
],
|
|
tags: [
|
|
{ name: 'Health', description: 'Liveness probe' },
|
|
{ name: 'Auth', description: 'Web session login/logout (cookie + TOTP)' },
|
|
{ name: 'Auth · Mobile', description: 'Native bearer-token login, refresh and logout' },
|
|
{ name: 'Auth · SSO', description: 'OAuth2 / OIDC provider discovery and redirect flow' },
|
|
{ name: 'Public', description: 'Unauthenticated site content (settings, posts, wiki, contact)' },
|
|
{ name: 'Public · Shard', description: 'Live shard data ingested from the uo-link sidecar (status, feed, economy, IDOC, characters)' },
|
|
{ name: 'Admin · Account', description: 'Self-service account security (2FA, linked identities)' },
|
|
{ name: 'Player', description: 'Self-service player accounts (register, credentials, 2FA, linked identities)' },
|
|
{ name: 'Player · Shard', description: 'Link an in-game account and read its roster / vendors (uo-link)' },
|
|
{ name: 'Admin · Dashboard', description: 'Dashboard summary and site mode' },
|
|
{ name: 'Admin · Posts', description: 'News / five-on-friday / newsletter / screenshots + uploads' },
|
|
{ name: 'Admin · Wiki', description: 'Wiki pages, categories, tags and revisions' },
|
|
{ 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 · Shard', description: 'uo-link sidecar connection config, live status and town crier (admin only)' },
|
|
{ name: 'Admin · Auth Providers', description: 'SSO provider configuration (admin only)' },
|
|
{ name: 'Admin · Users', description: 'User management (admin only)' },
|
|
],
|
|
components: {
|
|
securitySchemes: {
|
|
// Web/admin session — httpOnly cookie set by the login endpoints.
|
|
cookieAuth: {
|
|
type: 'apiKey',
|
|
in: 'cookie',
|
|
name: 'uomm_token',
|
|
description: 'Session JWT set as an httpOnly cookie by POST /api/v1/auth/login.',
|
|
},
|
|
// Native/mobile clients — Authorization: Bearer <accessToken>.
|
|
bearerAuth: {
|
|
type: 'http',
|
|
scheme: 'bearer',
|
|
bearerFormat: 'JWT',
|
|
description: 'Access token from POST /api/v1/auth/mobile/login (or /refresh).',
|
|
},
|
|
},
|
|
schemas: {
|
|
Error: {
|
|
type: 'object',
|
|
properties: { message: { type: 'string', example: 'Not found' } },
|
|
},
|
|
ValidationError: {
|
|
type: 'object',
|
|
properties: {
|
|
errors: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
type: { type: 'string', example: 'field' },
|
|
msg: { type: 'string', example: 'Invalid value' },
|
|
path: { type: 'string', example: 'username' },
|
|
location: { type: 'string', example: 'body' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
SafeUser: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'integer', example: 1 },
|
|
username: { type: 'string', example: 'admin' },
|
|
role: { type: 'string', enum: ['admin', 'editor'], example: 'admin' },
|
|
},
|
|
},
|
|
LoginRequest: {
|
|
type: 'object',
|
|
required: ['username', 'password'],
|
|
properties: {
|
|
username: { type: 'string', example: 'admin' },
|
|
password: { type: 'string', format: 'password', example: 'super-secret' },
|
|
company: { type: 'string', description: 'Honeypot — must be empty for humans.', example: '' },
|
|
},
|
|
},
|
|
RegisterRequest: {
|
|
type: 'object',
|
|
required: ['username', 'password'],
|
|
properties: {
|
|
username: { type: 'string', minLength: 3, maxLength: 32, example: 'newplayer' },
|
|
password: { type: 'string', format: 'password', minLength: 8, maxLength: 64 },
|
|
email: { type: 'string', format: 'email', nullable: true, example: 'player@example.com' },
|
|
company: { type: 'string', description: 'Honeypot — must be empty for humans.', example: '' },
|
|
},
|
|
},
|
|
LoginResponse: {
|
|
type: 'object',
|
|
description:
|
|
'Either a session (user) or, for 2FA accounts, a TOTP challenge to complete at /login/totp.',
|
|
properties: {
|
|
user: { $ref: '#/components/schemas/SafeUser' },
|
|
totpRequired: { type: 'boolean', example: true },
|
|
challenge: { type: 'string', description: 'Signed challenge token for the TOTP step.' },
|
|
},
|
|
},
|
|
TotpLoginRequest: {
|
|
type: 'object',
|
|
required: ['challenge', 'code'],
|
|
properties: {
|
|
challenge: { type: 'string', description: 'Token returned by /login when totpRequired.' },
|
|
code: { type: 'string', example: '123456' },
|
|
},
|
|
},
|
|
MobileLoginRequest: {
|
|
type: 'object',
|
|
required: ['username', 'password'],
|
|
properties: {
|
|
username: { type: 'string', example: 'admin' },
|
|
password: { type: 'string', format: 'password', example: 'super-secret' },
|
|
code: { type: 'string', description: 'TOTP code (only when 2FA is enabled).', example: '123456' },
|
|
},
|
|
},
|
|
MobileTokenResponse: {
|
|
type: 'object',
|
|
properties: {
|
|
accessToken: { type: 'string', description: 'Short-lived bearer JWT.' },
|
|
refreshToken: { type: 'string', description: 'Long-lived, revocable refresh token.' },
|
|
expiresIn: {
|
|
type: 'string',
|
|
description: 'Access token lifetime as a duration string (zeit/ms format, e.g. "15m").',
|
|
example: '15m',
|
|
},
|
|
user: { $ref: '#/components/schemas/SafeUser' },
|
|
},
|
|
},
|
|
MobileRefreshRequest: {
|
|
type: 'object',
|
|
required: ['refreshToken'],
|
|
properties: { refreshToken: { type: 'string' } },
|
|
},
|
|
MobileLogoutRequest: {
|
|
type: 'object',
|
|
properties: {
|
|
refreshToken: { type: 'string', description: 'Revoke a single session.' },
|
|
all: { type: 'boolean', description: 'Revoke every session for the user.', example: false },
|
|
},
|
|
},
|
|
Message: {
|
|
type: 'object',
|
|
properties: { message: { type: 'string', example: 'Logged out.' } },
|
|
},
|
|
ContactRequest: {
|
|
type: 'object',
|
|
required: ['message'],
|
|
properties: {
|
|
message: { type: 'string', maxLength: 5000, example: 'When does the shard launch?' },
|
|
email: { type: 'string', format: 'email', example: 'player@example.com' },
|
|
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' },
|
|
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: ['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: {
|
|
type: 'object',
|
|
required: ['id', 'kind', 'name'],
|
|
properties: {
|
|
id: { type: 'string', pattern: '^[a-z0-9-]+$', example: 'okta' },
|
|
kind: { type: 'string', enum: ['oidc', 'oauth2'], example: 'oidc' },
|
|
name: { type: 'string', maxLength: 80, example: 'Okta' },
|
|
enabled: { type: 'boolean', example: true },
|
|
clientId: { type: 'string' },
|
|
secret: { type: 'string', format: 'password' },
|
|
authorizeUrl: { type: 'string', format: 'uri' },
|
|
tokenUrl: { type: 'string', format: 'uri' },
|
|
userinfoUrl: { type: 'string', format: 'uri' },
|
|
scopes: { type: 'string', maxLength: 500, example: 'openid email profile' },
|
|
priority: { type: 'integer', example: 10 },
|
|
},
|
|
},
|
|
Post: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'integer', example: 12 },
|
|
category: { type: 'string', example: 'news' },
|
|
title: { type: 'string', example: 'Server maintenance this weekend' },
|
|
slug: { type: 'string', example: 'server-maintenance-this-weekend' },
|
|
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: {
|
|
type: 'object',
|
|
required: ['category', 'title'],
|
|
properties: {
|
|
category: { type: 'string', example: 'news' },
|
|
title: { type: 'string', maxLength: 200, example: 'Server maintenance this weekend' },
|
|
body: { type: 'string' },
|
|
image_url: { type: 'string', description: 'Required for the screenshots category.' },
|
|
published: { type: 'boolean', example: false },
|
|
},
|
|
},
|
|
PublishRequest: {
|
|
type: 'object',
|
|
required: ['published'],
|
|
properties: { published: { type: 'boolean', example: true } },
|
|
},
|
|
UploadResponse: {
|
|
type: 'object',
|
|
properties: { url: { type: 'string', example: '/uploads/1700000000-abcd.png' } },
|
|
},
|
|
WikiPage: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'integer', example: 3 },
|
|
slug: { type: 'string', example: 'getting-started' },
|
|
title: { type: 'string', example: 'Getting Started' },
|
|
excerpt: { type: 'string' },
|
|
body: { type: 'string' },
|
|
category_id: { type: 'integer', nullable: true, example: 2 },
|
|
published: { type: 'boolean', example: true },
|
|
tags: { type: 'array', items: { type: 'string' }, example: ['newbie', 'guide'] },
|
|
created_at: { type: 'string', format: 'date-time' },
|
|
updated_at: { type: 'string', format: 'date-time' },
|
|
},
|
|
},
|
|
WikiPageCreateRequest: {
|
|
type: 'object',
|
|
required: ['slug', 'title'],
|
|
properties: {
|
|
slug: { type: 'string', pattern: '^[a-z0-9-]+$', example: 'getting-started' },
|
|
title: { type: 'string', maxLength: 200, example: 'Getting Started' },
|
|
excerpt: { type: 'string', maxLength: 400 },
|
|
body: { type: 'string' },
|
|
category_id: { type: 'integer', nullable: true },
|
|
published: { type: 'boolean', example: false },
|
|
tags: { type: 'array', items: { type: 'string' } },
|
|
},
|
|
},
|
|
WikiCategory: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'integer', example: 2 },
|
|
slug: { type: 'string', example: 'guides' },
|
|
title: { type: 'string', example: 'Guides' },
|
|
description: { type: 'string' },
|
|
sort_order: { type: 'integer', example: 0 },
|
|
},
|
|
},
|
|
WikiCategoryCreateRequest: {
|
|
type: 'object',
|
|
required: ['slug', 'title'],
|
|
properties: {
|
|
slug: { type: 'string', pattern: '^[a-z0-9-]+$', example: 'guides' },
|
|
title: { type: 'string', maxLength: 200, example: 'Guides' },
|
|
description: { type: 'string', maxLength: 400 },
|
|
sort_order: { type: 'integer', example: 0 },
|
|
},
|
|
},
|
|
User: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'integer', example: 1 },
|
|
username: { type: 'string', example: 'admin' },
|
|
role: { type: 'string', enum: ['admin', 'editor', 'moderator', 'player'], example: 'admin' },
|
|
status: { type: 'string', enum: ['active', 'disabled', 'banned', 'pending'], example: 'active' },
|
|
email: { type: 'string', format: 'email', nullable: true },
|
|
email_verified: { type: 'boolean', example: false },
|
|
totp_enabled: { type: 'boolean', example: true },
|
|
last_login_at: { type: 'string', format: 'date-time', nullable: true },
|
|
created_at: { type: 'string', format: 'date-time' },
|
|
},
|
|
},
|
|
UserCreateRequest: {
|
|
type: 'object',
|
|
required: ['username', 'password'],
|
|
properties: {
|
|
username: { type: 'string', minLength: 3, maxLength: 32, example: 'editor1' },
|
|
password: { type: 'string', format: 'password', minLength: 8, maxLength: 64 },
|
|
role: { type: 'string', enum: ['admin', 'editor', 'moderator', 'player'], example: 'editor' },
|
|
status: { type: 'string', enum: ['active', 'disabled', 'banned', 'pending'], example: 'active' },
|
|
email: { type: 'string', format: 'email', nullable: true },
|
|
},
|
|
},
|
|
// Player self-service credential changes (/api/v1/player/account/*).
|
|
ChangeUsernameRequest: {
|
|
type: 'object',
|
|
required: ['username'],
|
|
properties: {
|
|
username: { type: 'string', minLength: 3, maxLength: 32, example: 'newname' },
|
|
},
|
|
},
|
|
ChangePasswordRequest: {
|
|
type: 'object',
|
|
required: ['newPassword'],
|
|
properties: {
|
|
newPassword: { type: 'string', format: 'password', minLength: 8, maxLength: 64 },
|
|
currentPassword: {
|
|
type: 'string',
|
|
format: 'password',
|
|
description:
|
|
'Required when the account already has a password. Omit only for an SSO-provisioned account setting its first password.',
|
|
},
|
|
},
|
|
},
|
|
PlayerAccount: {
|
|
type: 'object',
|
|
description: 'Self-service player account (GET /player/account).',
|
|
properties: {
|
|
id: { type: 'integer', example: 42 },
|
|
username: { type: 'string', example: 'newplayer' },
|
|
role: { type: 'string', enum: ['player'], example: 'player' },
|
|
email: { type: 'string', format: 'email', nullable: true, example: 'player@example.com' },
|
|
status: { type: 'string', enum: ['active', 'disabled', 'banned', 'pending'], example: 'active' },
|
|
totp_enabled: { type: 'boolean', example: false },
|
|
has_password: {
|
|
type: 'boolean',
|
|
description: 'False for an SSO-provisioned account that has not set a password yet.',
|
|
example: true,
|
|
},
|
|
},
|
|
},
|
|
OkFlag: {
|
|
type: 'object',
|
|
properties: { ok: { type: 'boolean', example: true } },
|
|
},
|
|
TotpCodeRequest: {
|
|
type: 'object',
|
|
required: ['code'],
|
|
properties: { code: { type: 'string', example: '123456' } },
|
|
},
|
|
SiteModeRequest: {
|
|
type: 'object',
|
|
required: ['mode'],
|
|
properties: { mode: { type: 'string', enum: ['live', 'maintenance'], example: 'live' } },
|
|
},
|
|
UnbanRequest: {
|
|
type: 'object',
|
|
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 },
|
|
},
|
|
},
|
|
// ── uo-link shard data ──────────────────────────────────────────────
|
|
ShardStatus: {
|
|
type: 'object',
|
|
description: 'Public shard status (GET /public/shard/status).',
|
|
properties: {
|
|
enabled: { type: 'boolean', example: true },
|
|
status: { type: 'string', example: 'connected', description: 'connected | reconnecting | disconnected | error' },
|
|
pluginConnected: { type: 'boolean', description: 'Is the shard link up right now?', example: true },
|
|
lastEventAt: { type: 'string', format: 'date-time', nullable: true },
|
|
onlineCount: { type: 'integer', example: 12 },
|
|
economy: { $ref: '#/components/schemas/ShardEconomyPoint' },
|
|
},
|
|
},
|
|
ShardEvent: {
|
|
type: 'object',
|
|
description: 'A logged shard event.',
|
|
properties: {
|
|
id: { type: 'integer', example: 4821 },
|
|
kind: { type: 'string', example: 'vendor.sale' },
|
|
t: { type: 'integer', description: 'Event time, epoch ms.', example: 1783720195626 },
|
|
bootId: { type: 'string', nullable: true, example: 'boot-abc123' },
|
|
payload: { type: 'object', additionalProperties: true, description: 'The full event object.' },
|
|
createdAt: { type: 'string', format: 'date-time' },
|
|
},
|
|
},
|
|
ShardEconomyPoint: {
|
|
type: 'object',
|
|
nullable: true,
|
|
description: 'One gold-supply sample.',
|
|
properties: {
|
|
accounts: { type: 'integer', nullable: true, example: 240 },
|
|
gold: { type: 'integer', nullable: true, example: 1028983421 },
|
|
t: { type: 'integer', description: 'Sample time, epoch ms.', example: 1783720000000 },
|
|
},
|
|
},
|
|
ShardHouse: {
|
|
type: 'object',
|
|
description: 'A house at its current decay stage.',
|
|
properties: {
|
|
serial: { type: 'string', example: '0x4004705F' },
|
|
stage: { type: 'string', example: 'IDOC' },
|
|
map: { type: 'string', nullable: true, example: 'Trammel' },
|
|
x: { type: 'integer', nullable: true },
|
|
y: { type: 'integer', nullable: true },
|
|
z: { type: 'integer', nullable: true },
|
|
region: { type: 'string', nullable: true },
|
|
name: { type: 'string', nullable: true, example: 'An Unnamed House' },
|
|
ownerSerial: { type: 'string', nullable: true },
|
|
ownerAcct: { type: 'string', nullable: true },
|
|
builtOn: { type: 'string', format: 'date-time', nullable: true },
|
|
lastRefreshed: { type: 'string', format: 'date-time', nullable: true },
|
|
isIdoc: { type: 'boolean', example: true },
|
|
updatedAt: { type: 'string', format: 'date-time' },
|
|
},
|
|
},
|
|
ShardLinkRequest: {
|
|
type: 'object',
|
|
required: ['code'],
|
|
properties: {
|
|
code: { type: 'string', description: 'The one-time code shown by [link in game.', example: 'AB12CD' },
|
|
},
|
|
},
|
|
ShardLinkResult: {
|
|
type: 'object',
|
|
properties: {
|
|
linked: { type: 'boolean', example: true },
|
|
account: { type: 'string', example: 'whitlocktech' },
|
|
},
|
|
},
|
|
ShardLink: {
|
|
type: 'object',
|
|
description: 'A linked in-game account (GET /player/shard/accounts).',
|
|
properties: {
|
|
account: { type: 'string', example: 'whitlocktech' },
|
|
userId: { type: 'integer', example: 42 },
|
|
charName: { type: 'string', nullable: true, example: 'Darrow' },
|
|
linkedAt: { type: 'string', format: 'date-time' },
|
|
},
|
|
},
|
|
TownCrierRequest: {
|
|
type: 'object',
|
|
required: ['id', 'lines'],
|
|
properties: {
|
|
id: { type: 'string', maxLength: 64, description: 'Re-posting the same id replaces the prior entry.', example: 'news-42' },
|
|
lines: { type: 'array', items: { type: 'string', maxLength: 200 }, example: ['Hear ye!', 'Market tax is now 5%.'] },
|
|
durationSec: { type: 'integer', minimum: 1, maximum: 86400, example: 3600 },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
swaggerAutogen(outputFile, routes, doc).then(() => {
|
|
// eslint-disable-next-line no-console
|
|
console.log('swagger-output.json generated.')
|
|
})
|