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:
@@ -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);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user