Add Swagger/OpenAPI API docs (swagger-ui + swagger-autogen)
Generate an OpenAPI 3.0 spec from route annotations and serve it with Swagger UI so the full REST API is browsable and testable. - Add swagger-ui-express (runtime) and swagger-autogen (dev) deps, plus an `npm run swagger` script. - server/swagger/swagger.js: generator config with API metadata, servers, 14 tag groups, cookie + bearer security schemes, and 28 reusable component schemas. Follows the Express mount chain from src/app.js so generated paths are fully-qualified (/api/v1/...). - Annotate every route (auth, mobile, sso, public, admin, health) with #swagger tags/summaries/parameters/request bodies/security and the actual response codes each handler returns (400/401/403/404/409/429/ 302/502, multipart uploads). - Serve Swagger UI at /api/docs and the raw spec at /api/docs.json, guarded so a missing spec disables docs instead of crashing. - Commit the generated swagger-output.json so docs work with no build step; swagger-autogen stays dev-only and is not needed at runtime. - README: new "API documentation (Swagger)" section plus tech-stack and project-structure entries. Covers 51 paths / 64 operations. Existing test suite (83) still passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,16 +7,54 @@ const { ssoStartLimiter } = require('../../../middleware/rateLimit')
|
||||
const ssoRouter = express.Router()
|
||||
|
||||
// Public discovery — the login page reads this to render provider buttons.
|
||||
ssoRouter.get('/providers', ctrl.listProviders)
|
||||
ssoRouter.get(
|
||||
'/providers',
|
||||
// #swagger.tags = ['Auth · SSO']
|
||||
// #swagger.summary = 'List enabled SSO providers'
|
||||
// #swagger.description = 'Public discovery used by the login page to render provider buttons. Never exposes secrets.'
|
||||
/* #swagger.responses[200] = { description: 'Enabled, valid providers', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Provider" } } } } } */
|
||||
ctrl.listProviders,
|
||||
)
|
||||
|
||||
// Begin login (public) — redirects to the IdP.
|
||||
ssoRouter.get('/sso/:provider/start', ssoStartLimiter, ctrl.start)
|
||||
ssoRouter.get(
|
||||
'/sso/:provider/start',
|
||||
// #swagger.tags = ['Auth · SSO']
|
||||
// #swagger.summary = 'Begin SSO login (redirect to the IdP)'
|
||||
// #swagger.description = 'Sets a short-lived signed transaction cookie and 302-redirects to the provider authorize URL. On error redirects back to the login page with an sso_error query param.'
|
||||
// #swagger.parameters['provider'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id (e.g. google, discord).' }
|
||||
// #swagger.parameters['returnTo'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Internal /admin path to return to after login.' }
|
||||
/* #swagger.responses[302] = { description: 'Redirect to the identity provider (or back to the login page on error)' } */
|
||||
ssoStartLimiter,
|
||||
ctrl.start,
|
||||
)
|
||||
|
||||
// Begin account linking (must be signed in — the tx captures the acting user).
|
||||
ssoRouter.get('/sso/:provider/link', requireAuth, ctrl.linkStart)
|
||||
ssoRouter.get(
|
||||
'/sso/:provider/link',
|
||||
// #swagger.tags = ['Auth · SSO']
|
||||
// #swagger.summary = 'Begin linking an SSO identity to the current account'
|
||||
// #swagger.description = 'Requires an authenticated session; the signed transaction captures the acting user so the callback can attach the external identity.'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
// #swagger.parameters['provider'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id (e.g. google, discord).' }
|
||||
/* #swagger.responses[302] = { description: 'Redirect to the identity provider (or back to the account page on error)' } */
|
||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
requireAuth,
|
||||
ctrl.linkStart,
|
||||
)
|
||||
|
||||
// OAuth redirect target — completes login or linking. Not behind requireAuth:
|
||||
// the signed tx cookie authorizes link mode; login mode is link-only anyway.
|
||||
ssoRouter.get('/sso/:provider/callback', ctrl.callback)
|
||||
ssoRouter.get(
|
||||
'/sso/:provider/callback',
|
||||
// #swagger.tags = ['Auth · SSO']
|
||||
// #swagger.summary = 'OAuth redirect target — completes login or linking'
|
||||
// #swagger.description = 'The provider redirects here with code + state. On success sets the session cookie (login) or links the identity (link), then 302-redirects into /admin. Login is link-only: unknown identities are refused (sso_error=not_linked).'
|
||||
// #swagger.parameters['provider'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id (e.g. google, discord).' }
|
||||
// #swagger.parameters['code'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'OAuth authorization code.' }
|
||||
// #swagger.parameters['state'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'OAuth state (matched against the tx cookie).' }
|
||||
/* #swagger.responses[302] = { description: 'Redirect into /admin on success, or back to login/account with an error code' } */
|
||||
ctrl.callback,
|
||||
)
|
||||
|
||||
module.exports = ssoRouter
|
||||
|
||||
Reference in New Issue
Block a user