159 lines
6.2 KiB
JavaScript
159 lines
6.2 KiB
JavaScript
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'",
|
||
// 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",
|
||
"worker-src 'self'",
|
||
"object-src 'none'",
|
||
"base-uri 'self'",
|
||
"form-action 'self'",
|
||
].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'));
|
||
|
||
// 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/search', require('./routes/search'));
|
||
|
||
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`);
|