// Load .env.local (gitignored) before anything else reads process.env. require('dotenv').config({ path: require('path').join(__dirname, '.env.local') }); const express = require('express'); const path = require('path'); const cors = require('cors'); const fs = require('fs'); const helmet = require('helmet'); const session = require('express-session'); const SQLiteStore = require('connect-sqlite3')(session); const rateLimit = require('express-rate-limit'); const multer = require('multer'); const { initDatabase, closeDatabase } = require('./database'); const { requireLogin } = require('./middleware/auth'); const app = express(); const PORT = process.env.PORT || 3000; const isProd = process.env.NODE_ENV === 'production'; // We sit behind nginx/PM2 in production; trust X-Forwarded-* headers so that // req.secure reflects the actual client connection (otherwise secure cookies // would never be set and rate-limiting would treat every client as the same IP). if (isProd) { app.set('trust proxy', 1); } initDatabase(); // Graceful shutdown function gracefulShutdown(signal) { console.log(`\n${signal} received, closing database connection...`); closeDatabase(); process.exit(0); } process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); // Uploads directory const uploadsDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true }); // Resolve a stored relative filepath safely under uploadsDir, or null if it escapes. function safeUploadPath(storedPath) { const resolved = path.resolve(__dirname, storedPath); const uploadsResolved = path.resolve(uploadsDir); if (resolved !== uploadsResolved && !resolved.startsWith(uploadsResolved + path.sep)) { return null; } return resolved; } app.locals.safeUploadPath = safeUploadPath; // --- Security middleware --- app.use(helmet({ contentSecurityPolicy: false, // existing inline styles/scripts in index.html; enable once frontend is cleaned up })); // CORS: only allow configured origins. Default = same-origin only (no CORS headers). const allowedOrigins = (process.env.ALLOWED_ORIGINS || '') .split(',') .map(s => s.trim()) .filter(Boolean); app.use(cors({ origin: (origin, cb) => { if (!origin) return cb(null, true); // same-origin / curl if (allowedOrigins.includes(origin)) return cb(null, true); return cb(new Error('CORS: Origin nicht erlaubt')); }, credentials: true, })); app.use(express.json({ limit: '1mb' })); // API rate limit (login has its own stricter limiter in routes/auth.js) app.use('/api/', rateLimit({ windowMs: 60 * 1000, max: 300, standardHeaders: true, legacyHeaders: false, })); // Sessions, stored in SQLite next to provision.db app.use(session({ store: new SQLiteStore({ db: 'sessions.db', dir: __dirname }), secret: process.env.SESSION_SECRET || 'change-me-in-production', resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: 'lax', // 'auto' = secure cookie when req.secure is true (HTTPS or trust-proxy // detected HTTPS), plaintext cookie when accessed over HTTP. Lets the // same build serve both http://host:3000 and https://host/ without a // login loop. secure: 'auto', maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days }, })); if (!process.env.SESSION_SECRET) { console.warn('WARNING: SESSION_SECRET not set; using insecure default. Set it before deploying.'); } // --- Public routes (login page + auth endpoints) --- const authRouter = require('./routes/auth'); app.use('/api/auth', authRouter); // Serve login page without auth app.get(['/login', '/login.html'], (req, res) => { res.sendFile(path.join(__dirname, 'public', 'login.html')); }); app.get('/login.js', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'login.js')); }); // --- Auth gate for all /api/* except /api/auth/* --- app.use('/api', (req, res, next) => { if (req.path.startsWith('/auth/')) return next(); return requireLogin(req, res, next); }); // --- Protected API routes --- const anbieterRouter = require('./routes/anbieter'); const tarifeRouter = require('./routes/tarife'); const schaltungenRouter = require('./routes/schaltungen'); const tarifoptionenRouter = require('./routes/tarifoptionen'); const dokumenteRouter = require('./routes/dokumente'); const provisionenRouter = require('./routes/provisionen'); const provisionsregelnRouter = require('./routes/provisionsregeln'); const mitarbeiterRouter = require('./routes/mitarbeiter'); app.use('/api/anbieter', anbieterRouter); app.use('/api/tarife', tarifeRouter); app.use('/api/schaltungen', schaltungenRouter); app.use('/api/tarifoptionen', tarifoptionenRouter); app.use('/api/mitarbeiter-prozentsaetze', mitarbeiterRouter); app.use('/api', dokumenteRouter); // /api/schaltungen/:id/dokumente, /api/dokumente/:id/* app.use('/api', provisionenRouter); // /api/provisionen, /api/schaltungen/:id/provision, /api/geloeschte-schaltungen app.use('/api', provisionsregelnRouter); // /api/provisionsregeln, /api/anbieter-provisionsregeln // Singular tariff endpoint (kept for frontend backward compat) app.get('/api/tarif/:id', requireLogin, async (req, res, next) => { const db = require('./db'); try { const row = await db.get('SELECT * FROM tarife WHERE id = ?', [req.params.id]); if (!row) return res.status(404).json({ error: 'Tarif nicht gefunden' }); res.json(row); } catch (err) { next(err); } }); // --- Static files (after auth gate so /api routes don't accidentally serve assets) --- // Frontend gates itself via /api/auth/me; static files (login page, JS, CSS) are public. app.use(express.static(path.join(__dirname, 'public'))); app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // --- Error handler (must be last) --- app.use((err, req, res, next) => { if (err instanceof multer.MulterError) { if (err.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'Datei zu groß. Maximum: 10MB' }); } return res.status(400).json({ error: err.message }); } if (err && err.status === 404) { return res.status(404).json({ error: err.message }); } if (err && /CORS/.test(err.message)) { return res.status(403).json({ error: err.message }); } console.error('Unhandled error:', err); res.status(err && err.status ? err.status : 500).json({ error: err && err.message ? err.message : 'Interner Server-Fehler' }); }); app.listen(PORT, () => { console.log(`Server läuft auf http://localhost:${PORT}`); });