const express = require('express'); const path = require('path'); const fs = require('fs'); require('./db'); const app = express(); app.use(express.json({ limit: '10mb' })); // ── Aktivitäts-Tracking: nur bei echten Aktionen (POST/PUT/DELETE) ───────────── app.use((req, res, next) => { if (['POST','PUT','DELETE'].includes(req.method)) { const auth = req.headers.authorization; if (auth?.startsWith('Bearer ')) { try { const jwt = require('jsonwebtoken'); const p = jwt.verify(auth.slice(7), process.env.JWT_SECRET || 'dev-secret'); if (p?.id) { db.prepare("UPDATE users SET last_active_at=datetime('now','localtime') WHERE id=?").run(p.id); } } catch {} } } next(); }); // ── Security Headers ────────────────────────────────────────────────────────── app.use((_req, res, next) => { res.setHeader('Content-Security-Policy', [ "default-src 'self'", "script-src 'self'", // unsafe-inline nötig für React inline-styles + @import Google Fonts "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", "font-src 'self' https://fonts.gstatic.com", // iCal-Feeds werden serverseitig geprosxt → nur 'self' nötig "connect-src 'self' https://web.archive.org", // data: für Avatare (base64), google.com + gstatic.com für Favicons im QuickLinks-Widget "img-src 'self' data: https://www.google.com https://*.gstatic.com https://image.tmdb.org", "worker-src 'self'", "object-src 'none'", "base-uri 'self'", "form-action 'self'", "frame-src https://archive.ph https://archive.is https://archive.today", ].join('; ')); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); next(); }); // ── API ─────────────────────────────────────────────────────────────────────── app.use('/api/auth', require('./routes/auth')); app.use('/api/admin', require('./routes/admin')); app.use('/api/calendar', require('./routes/calendar')); app.use('/api/dashboard', require('./routes/dashboard')); app.use('/api/system', require('./routes/system')); // Öffentliche Datei-Share Verwaltung (vor Auto-Loader damit /:id nicht matcht) app.use('/api/tools/dateien/public-shares', require('./tools/dateien/public-share')); // Tool-Routen automatisch laden const toolsDir = path.join(__dirname, 'tools'); fs.readdirSync(toolsDir, { withFileTypes: true }) .filter(d => d.isDirectory()) .forEach(d => { const routeFile = path.join(toolsDir, d.name, 'routes.js'); if (fs.existsSync(routeFile)) { app.use(`/api/tools/${d.name}`, require(routeFile)); console.log(` 🔧 Tool geladen: ${d.name}`); } }); app.use('/api/tools/snippets', require('./tools/snippets/routes')); app.use('/api/tools/qrcodes', require('./tools/qrcodes/routes')); app.use('/api/upload-shares', require('./tools/dateien/upload-share')); app.use('/api/public/file-share', require('./tools/dateien/public-share-public')); app.use('/api/search', require('./routes/search')); // Öffentliche Upload-Seite: React-App ausliefern – App erkennt /u/ Pfad selbst app.get('/u/:token', (_req, res) => { res.setHeader('Cache-Control', 'no-store, no-cache'); res.sendFile(require('path').join(PUBLIC, 'index.html')); }); // Öffentliche Datei-Share-Seite: React-App ausliefern – App erkennt /s/ Pfad selbst app.get('/s/:token', (_req, res) => { res.setHeader('Cache-Control', 'no-store, no-cache'); res.sendFile(require('path').join(PUBLIC, 'index.html')); }); const PUBLIC = path.join(__dirname, '../public'); app.get('/manifest.json', (_req, res) => { res.setHeader('Content-Type', 'application/manifest+json'); res.setHeader('Cache-Control', 'no-cache'); res.sendFile(path.join(PUBLIC, 'manifest.json')); }); app.get('/sw.js', (_req, res) => { res.setHeader('Content-Type', 'application/javascript'); res.setHeader('Service-Worker-Allowed', '/'); res.setHeader('Cache-Control', 'no-cache'); res.sendFile(path.join(PUBLIC, 'sw.js')); }); app.use(express.static(PUBLIC, { setHeaders: (res, filePath) => { // JS/CSS Assets haben Hash im Namen → lang cachen if (filePath.match(/\.(js|css)$/) && !filePath.includes('sw.js')) { res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); } } })); // index.html nie cachen app.get('*', (_req, res) => { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); res.setHeader('Pragma', 'no-cache'); res.sendFile(path.join(PUBLIC, 'index.html')); }); // Build-Zeit Endpoint – Frontend prüft ob es eine neue Version gibt app.get('/api/build-time', (_req, res) => { res.setHeader('Cache-Control', 'no-store'); try { const ver = require('fs').readFileSync(path.join(__dirname, '../version.txt'), 'utf8').trim(); res.json({ buildTime: ver }); } catch { res.json({ buildTime: 'unknown' }); } }); app.listen(4000, () => console.log('🚀 Dicken Dock läuft auf Port 4000')); // ── Push-Zeitplaner Hintergrund-Job ────────────────────────────────────────── const db = require('./db'); async function sendPushoverMsg(userKey, appToken, message, opts = {}) { try { const params = { token: appToken, user: userKey, title: 'DickenDock Erinnerung', message }; if (opts.retry && opts.expire) { params.priority = 2; params.retry = opts.retry; params.expire = opts.expire; } await fetch('https://api.pushover.net/1/messages.json', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(params), }); } catch(e) { console.error('Pushover Fehler:', e.message); } } // Beim Start auf die nächste volle Minute synchronisieren const msToNextMinute = (60 - new Date().getSeconds()) * 1000 - new Date().getMilliseconds(); setTimeout(() => { runScheduler(); setInterval(runScheduler, 60 * 1000); }, msToNextMinute); async function runScheduler() { try { const now = db.prepare("SELECT datetime('now','localtime') as t").get().t; const due = db.prepare(` SELECT s.*, p.user_key, p.app_token, p.retry, p.expire FROM push_schedules s JOIN pushover_settings p ON p.user_id = s.user_id WHERE s.sent = 0 AND s.scheduled_at <= ? `).all(now); for (const row of due) { await sendPushoverMsg(row.user_key, row.app_token, row.message, { retry: row.retry, expire: row.expire }); db.prepare('UPDATE push_schedules SET sent=1 WHERE id=?').run(row.id); console.log(`✓ Push gesendet an user ${row.user_id}: "${row.message}" (fällig: ${row.scheduled_at})`); } } catch(e) { console.error('Push-Job Fehler:', e.message); } } console.log(`⏰ Push-Scheduler aktiv – nächste Prüfung in ${Math.round(msToNextMinute/1000)}s`);