Compare commits

..

4 Commits

Author SHA1 Message Date
Gitea Actions Bot 4264026bbe chore: bump backend version to 1.0.119 2025-09-09 18:06:15 +00:00
paul 24b4a314a9 fix(native/http): disable CSP upgrade-insecure-requests and HSTS unless ENABLE_HSTS=true; prevents HTTPS upgrades on HTTP installs
Mirror to GitHub / mirror (push) Successful in 39s
Test and Lint / backend-test (push) Successful in 1m31s
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Successful in 1m3s
Version and Release / trigger-drone (push) Successful in 3s
2025-09-09 20:00:48 +02:00
Gitea Actions Bot ba825823a0 chore: bump backend version to 1.0.118 2025-09-09 17:58:50 +00:00
paul fb16b7bbb8 feat(native): auto-serve SPA when dist exists (unless SERVE_FRONTEND=false); add clear logging; serve index.html for /admin
Mirror to GitHub / mirror (push) Successful in 42s
Test and Lint / backend-test (push) Successful in 1m34s
Test and Lint / frontend-test (push) Successful in 2m10s
Version and Release / version-bump (push) Successful in 59s
Version and Release / trigger-drone (push) Successful in 3s
2025-09-09 19:52:46 +02:00
3 changed files with 35 additions and 19 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "picpeak-backend", "name": "picpeak-backend",
"version": "1.0.117", "version": "1.0.119",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "picpeak-backend", "name": "picpeak-backend",
"version": "1.0.117", "version": "1.0.119",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.850.0", "@aws-sdk/client-s3": "^3.850.0",
"@aws-sdk/lib-storage": "^3.850.0", "@aws-sdk/lib-storage": "^3.850.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "picpeak-backend", "name": "picpeak-backend",
"version": "1.0.117", "version": "1.0.119",
"description": "Backend for PicPeak event photo sharing platform", "description": "Backend for PicPeak event photo sharing platform",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
+32 -16
View File
@@ -43,25 +43,36 @@ const PORT = process.env.PORT || 3000;
app.set('trust proxy', 'loopback, linklocal, uniquelocal'); app.set('trust proxy', 'loopback, linklocal, uniquelocal');
// Security middleware with custom CSP // Security middleware with custom CSP
// In native HTTP installs, do NOT force HTTPS for subresources.
const enableHsts = process.env.ENABLE_HSTS === 'true';
const cspDirectives = {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Required for React
styleSrc: ["'self'", "'unsafe-inline'", "https:"], // Required for styled components
imgSrc: ["'self'", "data:", "https:", "blob:"], // Allow data URLs and external images
connectSrc: ["'self'"], // API connections
fontSrc: ["'self'", "https:", "data:"], // Web fonts
objectSrc: ["'none'"], // Disable plugins
mediaSrc: ["'self'"], // Audio/video
frameSrc: ["'none'"], // Disable iframes
};
// Only upgrade insecure requests when HSTS explicitly enabled (HTTPS deployment)
if (enableHsts) {
// In helmet, an empty array enables the directive
cspDirectives.upgradeInsecureRequests = [];
}
app.use(helmet({ app.use(helmet({
contentSecurityPolicy: { contentSecurityPolicy: {
directives: { // Avoid helmet adding defaults like upgrade-insecure-requests when not desired
defaultSrc: ["'self'"], useDefaults: false,
scriptSrc: ["'self'", "'unsafe-inline'"], // Required for React directives: cspDirectives,
styleSrc: ["'self'", "'unsafe-inline'", "https:"], // Required for styled components
imgSrc: ["'self'", "data:", "https:", "blob:"], // Allow data URLs and external images
connectSrc: ["'self'"], // API connections
fontSrc: ["'self'", "https:", "data:"], // Web fonts
objectSrc: ["'none'"], // Disable plugins
mediaSrc: ["'self'"], // Audio/video
frameSrc: ["'none'"], // Disable iframes
},
}, },
hsts: { hsts: enableHsts ? {
maxAge: 31536000, // 1 year maxAge: 31536000, // 1 year
includeSubDomains: true, includeSubDomains: true,
preload: true preload: true
}, } : false,
permittedCrossDomainPolicies: false, permittedCrossDomainPolicies: false,
referrerPolicy: { policy: "strict-origin-when-cross-origin" } referrerPolicy: { policy: "strict-origin-when-cross-origin" }
})); }));
@@ -222,15 +233,20 @@ app.use('/api/secure-images', secureImagesRoutes);
// Optional: Serve built frontend (native installs) // Optional: Serve built frontend (native installs)
try { try {
const serveFrontend = process.env.SERVE_FRONTEND === 'true'; const serveFrontendEnv = process.env.SERVE_FRONTEND; // 'true' | 'false' | undefined
const frontendDir = process.env.FRONTEND_DIR || path.join(__dirname, '../frontend/dist'); const frontendDir = process.env.FRONTEND_DIR || path.join(__dirname, '../frontend/dist');
if (serveFrontend && fs.existsSync(frontendDir)) { const indexPath = path.join(frontendDir, 'index.html');
// Auto-serve when dist exists unless explicitly disabled
const shouldServe = (serveFrontendEnv === 'true') || ((serveFrontendEnv === undefined || serveFrontendEnv === 'auto') && fs.existsSync(indexPath));
if (shouldServe) {
logger.info(`Serving frontend from ${frontendDir}`); logger.info(`Serving frontend from ${frontendDir}`);
app.use(express.static(frontendDir)); app.use(express.static(frontendDir));
// SPA fallback for non-API routes // SPA fallback for non-API routes
app.get([ '/', '/admin', '/admin/*', '/gallery/*' ], (req, res) => { app.get([ '/', '/admin', '/admin/*', '/gallery/*' ], (req, res) => {
res.sendFile(path.join(frontendDir, 'index.html')); res.sendFile(indexPath);
}); });
} else {
logger.info('Frontend static serving disabled or dist not found', { serveFrontendEnv, frontendDir });
} }
} catch (e) { } catch (e) {
logger.warn('Failed to enable frontend static serving', { error: e.message }); logger.warn('Failed to enable frontend static serving', { error: e.message });