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 helmet = require('helmet');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
const crypto = require('crypto');
|
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 sabnzbdRoutes = require('./routes/sabnzbd');
|
||||||
const sonarrRoutes = require('./routes/sonarr');
|
const sonarrRoutes = require('./routes/sonarr');
|
||||||
@@ -26,6 +30,24 @@ const verifyCsrf = require('./middleware/verifyCsrf');
|
|||||||
function createApp({ skipRateLimits = false } = {}) {
|
function createApp({ skipRateLimits = false } = {}) {
|
||||||
const app = express();
|
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) {
|
if (process.env.TRUST_PROXY) {
|
||||||
const trustValue = /^\d+$/.test(process.env.TRUST_PROXY)
|
const trustValue = /^\d+$/.test(process.env.TRUST_PROXY)
|
||||||
? parseInt(process.env.TRUST_PROXY, 10)
|
? 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
|
// API routes
|
||||||
app.use('/api', apiLimiter);
|
app.use('/api', apiLimiter);
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ const crypto = require('crypto');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
|
const swaggerUi = require('swagger-ui-express');
|
||||||
|
const swaggerJsdoc = require('swagger-jsdoc');
|
||||||
|
const YAML = require('yamljs');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
require('./utils/loadSecrets')();
|
require('./utils/loadSecrets')();
|
||||||
const { version } = require('../package.json');
|
const { version } = require('../package.json');
|
||||||
@@ -113,6 +116,23 @@ if (process.env.EMBY_URL) {
|
|||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
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
|
// Resolve TLS_ENABLED early — used in Helmet CSP and server startup
|
||||||
const TLS_ENABLED = (process.env.TLS_ENABLED || 'true').toLowerCase() !== 'false';
|
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
|
// Static files — served before API routes
|
||||||
// index.html is served manually so we can inject the CSP nonce
|
// index.html is served manually so we can inject the CSP nonce
|
||||||
|
|||||||
Reference in New Issue
Block a user