Block uploads for demo user, restore PDF preview modal (v2.2.3)

- Demo user gets 403 on all upload endpoints (files, photos, cover, avatar)
- Admin uploads still work normally
- PDF export back in modal popup using srcdoc iframe
- Zero behavior change when DEMO_MODE is not set
This commit is contained in:
Maurice
2026-03-19 15:09:20 +01:00
parent 1a5c8cd385
commit c582a7b6c8
7 changed files with 50 additions and 16 deletions

View File

@@ -356,10 +356,37 @@ ${daysHtml}
</body></html>`
// Open in new window
const win = window.open('', '_blank')
if (win) {
win.document.write(html)
win.document.close()
}
// Open in modal with srcdoc iframe (no URL loading = no X-Frame-Options issue)
const overlay = document.createElement('div')
overlay.id = 'pdf-preview-overlay'
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:9999;display:flex;align-items:center;justify-content:center;padding:8px;'
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove() }
const card = document.createElement('div')
card.style.cssText = 'width:100%;max-width:1000px;height:95vh;background:var(--bg-card);border-radius:12px;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,0.3);'
const header = document.createElement('div')
header.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--border-primary);flex-shrink:0;'
header.innerHTML = `
<span style="font-size:13px;font-weight:600;color:var(--text-primary)">${escHtml(trip?.name || tr('pdf.travelPlan'))}</span>
<div style="display:flex;align-items:center;gap:8px">
<button id="pdf-print-btn" style="display:flex;align-items:center;gap:5px;font-size:12px;font-weight:500;color:var(--text-muted);background:none;border:none;cursor:pointer;padding:4px 8px;border-radius:6px;font-family:inherit">${tr('pdf.saveAsPdf')}</button>
<button id="pdf-close-btn" style="background:none;border:none;cursor:pointer;color:var(--text-faint);display:flex;padding:4px;border-radius:6px">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
`
const iframe = document.createElement('iframe')
iframe.style.cssText = 'flex:1;width:100%;border:none;'
iframe.sandbox = 'allow-same-origin allow-modals'
iframe.srcdoc = html
card.appendChild(header)
card.appendChild(iframe)
overlay.appendChild(card)
document.body.appendChild(overlay)
header.querySelector('#pdf-close-btn').onclick = () => overlay.remove()
header.querySelector('#pdf-print-btn').onclick = () => { iframe.contentWindow?.print() }
}

View File

@@ -1,6 +1,6 @@
{
"name": "nomad-server",
"version": "2.2.2",
"version": "2.2.3",
"main": "src/index.js",
"scripts": {
"start": "node --experimental-sqlite src/index.js",

View File

@@ -53,4 +53,11 @@ const adminOnly = (req, res, next) => {
next();
};
module.exports = { authenticate, optionalAuth, adminOnly };
const demoUploadBlock = (req, res, next) => {
if (process.env.DEMO_MODE === 'true' && req.user?.email === 'demo@nomad.app') {
return res.status(403).json({ error: 'Uploads are disabled in demo mode. Self-host NOMAD for full functionality.' });
}
next();
};
module.exports = { authenticate, optionalAuth, adminOnly, demoUploadBlock };

View File

@@ -7,7 +7,7 @@ const fs = require('fs');
const { v4: uuid } = require('uuid');
const fetch = require('node-fetch');
const { db } = require('../db/database');
const { authenticate } = require('../middleware/auth');
const { authenticate, demoUploadBlock } = require('../middleware/auth');
const router = express.Router();
const { JWT_SECRET } = require('../config');
@@ -243,7 +243,7 @@ router.get('/me/settings', authenticate, (req, res) => {
});
// POST /api/auth/avatar — upload avatar
router.post('/avatar', authenticate, avatarUpload.single('avatar'), (req, res) => {
router.post('/avatar', authenticate, demoUploadBlock, avatarUpload.single('avatar'), (req, res) => {
if (!req.file) return res.status(400).json({ error: 'No image uploaded' });
const current = db.prepare('SELECT avatar FROM users WHERE id = ?').get(req.user.id);

View File

@@ -4,7 +4,7 @@ const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const { db, canAccessTrip } = require('../db/database');
const { authenticate } = require('../middleware/auth');
const { authenticate, demoUploadBlock } = require('../middleware/auth');
const { broadcast } = require('../websocket');
const router = express.Router({ mergeParams: true });
@@ -72,7 +72,7 @@ router.get('/', authenticate, (req, res) => {
});
// POST /api/trips/:tripId/files
router.post('/', authenticate, upload.single('file'), (req, res) => {
router.post('/', authenticate, demoUploadBlock, upload.single('file'), (req, res) => {
const { tripId } = req.params;
const { place_id, description, reservation_id } = req.body;

View File

@@ -4,7 +4,7 @@ const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const { db, canAccessTrip } = require('../db/database');
const { authenticate } = require('../middleware/auth');
const { authenticate, demoUploadBlock } = require('../middleware/auth');
const router = express.Router({ mergeParams: true });
@@ -68,7 +68,7 @@ router.get('/', authenticate, (req, res) => {
});
// POST /api/trips/:tripId/photos
router.post('/', authenticate, upload.array('photos', 20), (req, res) => {
router.post('/', authenticate, demoUploadBlock, upload.array('photos', 20), (req, res) => {
const { tripId } = req.params;
const { day_id, place_id, caption } = req.body;

View File

@@ -4,7 +4,7 @@ const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const { db, canAccessTrip, isOwner } = require('../db/database');
const { authenticate } = require('../middleware/auth');
const { authenticate, demoUploadBlock } = require('../middleware/auth');
const { broadcast } = require('../websocket');
const router = express.Router();
@@ -139,7 +139,7 @@ router.put('/:id', authenticate, (req, res) => {
});
// POST /api/trips/:id/cover
router.post('/:id/cover', authenticate, uploadCover.single('cover'), (req, res) => {
router.post('/:id/cover', authenticate, demoUploadBlock, uploadCover.single('cover'), (req, res) => {
if (!isOwner(req.params.id, req.user.id))
return res.status(403).json({ error: 'Nur der Eigentümer kann das Titelbild ändern' });