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>
84 lines
4.8 KiB
JavaScript
84 lines
4.8 KiB
JavaScript
const express = require('express')
|
|
const { body } = require('express-validator')
|
|
|
|
const { login, loginTotp, logout, me, HONEYPOT_FIELD } = require('./auth.controller')
|
|
const { isLoggedIn } = require('../../../utils/auth')
|
|
const { loginLimiter } = require('../../../middleware/rateLimit')
|
|
const { slowLogin, backoffGuard } = require('../../../middleware/loginProtection')
|
|
const validate = require('../../../middleware/validate')
|
|
const mobileRouter = require('./mobile.routes')
|
|
const ssoRouter = require('./sso.routes')
|
|
|
|
const authRouter = express.Router()
|
|
|
|
// Native/Android bearer-token auth. Additive alongside the web cookie flow below.
|
|
authRouter.use('/mobile', mobileRouter)
|
|
|
|
// SSO discovery + OAuth redirect flow (/auth/providers, /auth/sso/:provider/*).
|
|
// Additive; the web cookie + TOTP flow below is unchanged.
|
|
authRouter.use(ssoRouter)
|
|
|
|
// Login protection order (cheapest rejection first):
|
|
// backoffGuard → per-IP exponential lockout on repeated failures
|
|
// slowLogin → progressive per-request delay within the window
|
|
// loginLimiter → hard 10-per-15-min cap
|
|
const loginGuards = [backoffGuard, slowLogin, loginLimiter]
|
|
|
|
authRouter.post(
|
|
'/login',
|
|
// #swagger.tags = ['Auth']
|
|
// #swagger.summary = 'Log in with username and password'
|
|
// #swagger.description = 'On success sets the httpOnly session cookie. If the account has 2FA enabled, returns { totpRequired, challenge } instead and no cookie is set — complete login at POST /login/totp. Rate limited and behind bot/backoff guards.'
|
|
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/LoginRequest" } } } } */
|
|
/* #swagger.responses[200] = { description: 'Session issued, or TOTP challenge required', content: { "application/json": { schema: { $ref: "#/components/schemas/LoginResponse" } } } } */
|
|
/* #swagger.responses[400] = { description: 'Validation error', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
|
|
/* #swagger.responses[401] = { description: 'Incorrect username or password', 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,
|
|
body('username').isString().trim().notEmpty(),
|
|
body('password').isString().notEmpty(),
|
|
// Honeypot must be absent/empty for humans; bots that fill it are caught in
|
|
// the controller. Accept-but-ignore here so a filled value still reaches it.
|
|
body(HONEYPOT_FIELD).optional(),
|
|
validate,
|
|
login,
|
|
)
|
|
|
|
// Second factor: same throttling, since it's a code-guessing surface too.
|
|
authRouter.post(
|
|
'/login/totp',
|
|
// #swagger.tags = ['Auth']
|
|
// #swagger.summary = 'Complete login with a TOTP code'
|
|
// #swagger.description = 'Second step for 2FA accounts. Exchange the challenge from /login plus the current authenticator code for a session cookie.'
|
|
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/TotpLoginRequest" } } } } */
|
|
/* #swagger.responses[200] = { description: 'Session issued', content: { "application/json": { schema: { $ref: "#/components/schemas/LoginResponse" } } } } */
|
|
/* #swagger.responses[400] = { description: 'Validation error', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
|
|
/* #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,
|
|
body('challenge').isString().notEmpty(),
|
|
body('code').isString().trim().isLength({ min: 6, max: 8 }),
|
|
validate,
|
|
loginTotp,
|
|
)
|
|
|
|
authRouter.post(
|
|
'/logout',
|
|
// #swagger.tags = ['Auth']
|
|
// #swagger.summary = 'Log out (clear the session cookie)'
|
|
/* #swagger.responses[200] = { description: 'Logged out', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
|
logout,
|
|
)
|
|
authRouter.get(
|
|
'/me',
|
|
// #swagger.tags = ['Auth']
|
|
// #swagger.summary = 'Current authenticated user'
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
/* #swagger.responses[200] = { description: 'The signed-in user', content: { "application/json": { schema: { type: "object", properties: { user: { $ref: "#/components/schemas/User" } } } } } } */
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
isLoggedIn,
|
|
me,
|
|
)
|
|
|
|
module.exports = authRouter
|