feat(swagger): mount Swagger UI at /api/swagger

- Import swagger-ui-express, swagger-jsdoc, yamljs in app.js and index.js
- Load server/openapi.yaml as base spec
- Configure swagger-jsdoc to merge JSDoc comments from route files
- Mount Swagger UI at /api/swagger (publicly accessible)
- Add authentication banner explaining cookie + CSRF flow
- Ensure spec loads from both createApp (tests) and index.js (production)
This commit is contained in:
2026-05-21 12:30:53 +01:00
parent 777fa26e5b
commit 964dacc588
3 changed files with 96 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
// Swagger UI authentication banner
// This banner explains the cookie + CSRF authentication flow
(function() {
window.addEventListener('load', function() {
const banner = document.createElement('div');
banner.style.cssText = `
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
padding: 12px 16px;
margin: 16px;
font-family: sans-serif;
font-size: 14px;
line-height: 1.5;
color: #856404;
`;
banner.innerHTML = `
<strong>Authentication Required for Most Endpoints</strong><br>
sofarr uses cookie-based authentication with Emby/Jellyfin. To test authenticated endpoints:<br>
1. Call <code>POST /api/auth/login</code> with your username and password<br>
2. The server sets an <code>emby_user</code> cookie and <code>csrf_token</code> cookie<br>
3. Include these cookies in subsequent requests<br>
4. For state-changing operations (POST/PUT/PATCH/DELETE), also send the <code>X-CSRF-Token</code> header<br>
<br>
<em>Note: The Swagger UI "Authorize" button is not used. Authentication is handled via cookies.</em>
`;
// Insert after the topbar (which we hide with CSS) or at the top of the info section
const info = document.querySelector('.info');
if (info) {
info.insertBefore(banner, info.firstChild);
}
});
})();
+31
View File
@@ -11,6 +11,10 @@ const cookieParser = require('cookie-parser');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const crypto = require('crypto');
const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');
const YAML = require('yamljs');
const path = require('path');
const sabnzbdRoutes = require('./routes/sabnzbd');
const sonarrRoutes = require('./routes/sonarr');
@@ -26,6 +30,24 @@ const verifyCsrf = require('./middleware/verifyCsrf');
function createApp({ skipRateLimits = false } = {}) {
const app = express();
// Load OpenAPI spec from YAML
const openapiSpec = YAML.load(path.join(__dirname, 'openapi.yaml'));
// Configure swagger-jsdoc to merge JSDoc comments from route files
const swaggerOptions = {
definition: {
...openapiSpec,
openapi: '3.1.0'
},
apis: [
path.join(__dirname, 'routes/*.js'),
path.join(__dirname, 'app.js'),
path.join(__dirname, 'index.js')
]
};
const swaggerSpec = swaggerJsdoc(swaggerOptions);
if (process.env.TRUST_PROXY) {
const trustValue = /^\d+$/.test(process.env.TRUST_PROXY)
? parseInt(process.env.TRUST_PROXY, 10)
@@ -93,6 +115,15 @@ function createApp({ skipRateLimits = false } = {}) {
}
});
// Swagger UI - publicly accessible API documentation
app.use('/api/swagger', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
customSiteTitle: 'sofarr API Documentation',
customCss: '.swagger-ui .topbar { display: none }',
customJs: [
'/swagger-auth-banner.js'
]
}));
// API routes
app.use('/api', apiLimiter);
app.use('/api/auth', authRoutes);
+31
View File
@@ -8,6 +8,9 @@ const crypto = require('crypto');
const fs = require('fs');
const http = require('http');
const https = require('https');
const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');
const YAML = require('yamljs');
require('dotenv').config();
require('./utils/loadSecrets')();
const { version } = require('../package.json');
@@ -113,6 +116,23 @@ if (process.env.EMBY_URL) {
const app = express();
const PORT = process.env.PORT || 3001;
// Load OpenAPI spec from YAML
const openapiSpec = YAML.load(path.join(__dirname, 'openapi.yaml'));
// Configure swagger-jsdoc to merge JSDoc comments from route files
const swaggerOptions = {
definition: {
...openapiSpec,
openapi: '3.1.0'
},
apis: [
path.join(__dirname, 'routes/*.js'),
path.join(__dirname, 'index.js')
]
};
const swaggerSpec = swaggerJsdoc(swaggerOptions);
// Resolve TLS_ENABLED early — used in Helmet CSP and server startup
const TLS_ENABLED = (process.env.TLS_ENABLED || 'true').toLowerCase() !== 'false';
@@ -212,6 +232,17 @@ app.get('/ready', (req, res) => {
}
});
// ---------------------------------------------------------------------------
// Swagger UI - publicly accessible API documentation
// ---------------------------------------------------------------------------
app.use('/api/swagger', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
customSiteTitle: 'sofarr API Documentation',
customCss: '.swagger-ui .topbar { display: none }',
customJs: [
'/swagger-auth-banner.js'
]
}));
// ---------------------------------------------------------------------------
// Static files — served before API routes
// index.html is served manually so we can inject the CSP nonce