require('dotenv').config(); const express = require('express'); const http = require('http'); const bodyParser = require('body-parser'); const cors = require('cors'); const path = require('path'); const session = require('express-session'); const SQLiteStore = require('connect-sqlite3')(session); const multer = require('multer'); const app = express(); const PORT = process.env.PORT || 3000; app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); app.use(express.static(path.join(__dirname, 'public'))); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use(cors()); const { localeMiddleware } = require('./locales'); const { crmContextMiddleware, getClientHubLinks } = require('./lib/crm'); const { getNavSections } = require('./lib/nav'); const { initAiSchema, syncAiConfigFile } = require('./lib/ai'); app.use(localeMiddleware); try { initAiSchema(); syncAiConfigFile(); } catch (e) { console.warn('AI init:', e.message); } app.use(session({ store: new SQLiteStore({ db: 'sessions.db', dir: path.join(__dirname, 'data') }), secret: process.env.SESSION_SECRET || 'mek-tech-consulting-secret', resave: false, saveUninitialized: false, cookie: { maxAge: 7 * 24 * 60 * 60 * 1000, sameSite: 'lax' } })); app.use((req, res, next) => { crmContextMiddleware(req, res, () => { res.locals.navSections = getNavSections(req.path); res.locals.showCrmBar = !req.path.startsWith('/auth') && req.path !== '/login'; if (res.locals.activeClient) { res.locals.clientHubLinks = getClientHubLinks(res.locals.activeClient.id); } next(); }); }); app.use((req, res, next) => { const publicPaths = ['/login', '/auth/login', '/css/dynamic.css', '/js/theme.js', '/health', '/api/spa/auth/login']; const publicPrefixes = ['/wachtwoord-vergeten', '/wachtwoord-reset/', '/css/', '/js/', '/health']; if (!publicPaths.includes(req.path) && !publicPrefixes.some(p => req.path.startsWith(p)) && !req.session.userId) { // API-aanroepen krijgen 401 JSON i.p.v. een HTML-redirect (o.a. voor de React SPA) if (req.path.startsWith('/api/')) { return res.status(401).json({ error: 'Niet ingelogd' }); } return res.redirect('/login'); } res.locals.user = req.session.userId ? { username: req.session.username } : null; next(); }); app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); const QS_HOST = process.env.QS_HOST || 'python-quality'; const QS_PORT = parseInt(process.env.QS_PORT || '3002', 10); app.use('/python-quality', (req, res) => { const proxyPath = '/python-quality' + req.url; const proxyReq = http.request({ hostname: QS_HOST, port: QS_PORT, path: proxyPath, method: req.method, headers: { ...req.headers, host: `${QS_HOST}:${QS_PORT}` } }, (proxyRes) => { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }); proxyReq.on('error', () => { res.status(502).type('html').send(`
De Python Quality engine is momenteel niet bereikbaar.
`); }); if (req.method === 'GET' || req.method === 'HEAD') proxyReq.end(); else req.pipe(proxyReq); }); app.use((req, res, next) => { try { const { getSettings } = require('./db'); const settings = getSettings(); res.locals.auto_refresh = settings.auto_refresh || '0'; res.locals.settings_data = JSON.stringify(settings); } catch (e) { res.locals.auto_refresh = '0'; res.locals.settings_data = '{}'; } next(); }); app.get('/css/dynamic.css', (req, res) => { try { const { getSettings } = require('./db'); const { THEME_PRESETS, THEME_CSS_KEYS } = require('./themes'); const settings = getSettings(); let css = '/* Mek-Tech Dynamic Settings */\n'; const themeKey = settings.theme || 'mek-neon-blue'; const preset = THEME_PRESETS[themeKey] || THEME_PRESETS['mek-neon-blue']; const customColors = settings.theme_colors || {}; const resolved = {}; css += ':root{\n'; THEME_CSS_KEYS.forEach(k => { const val = customColors[k] || preset[k]; if (val) { resolved[k] = val; css += `--${k.replace(/_/g, '-')}:${val};\n`; } }); css += '}\n'; const bg = resolved.bg || preset.bg; const bgNav = resolved.bg_nav || preset.bg_nav; const accent = resolved.accent || preset.accent; css += ` /* User theme overrides */ body{background-color:${bg}!important} .navbar{background:${bgNav}!important;border-bottom-color:var(--border)!important} .sidebar{background:var(--bg-nav)!important} .sb-item.active{background:rgba(0,212,255,.12)!important;color:${accent}!important} .kpi-card:hover,.client-card:hover,.card:hover{box-shadow:0 0 28px rgba(0,212,255,.15)!important} `; // Icon colors if (settings.icon_colors && typeof settings.icon_colors === 'object') { for (const [icon, color] of Object.entries(settings.icon_colors)) { css += `[data-lucide="${icon}"]{color:${color}!important}\n`; } } res.type('css').send(css); } catch (e) { console.error('dynamic.css error:', e.message); res.type('css').send('/* no settings */'); } }); app.use('/', require('./routes/auth')); app.use('/', require('./routes/dashboard')); app.use('/', require('./routes/password')); app.use('/clients', require('./routes/clients')); app.use('/engagements', require('./routes/engagements')); app.use('/tasks', require('./routes/tasks')); app.use('/assess', require('./routes/assess')); app.use('/racks', require('./routes/racks')); app.use('/networking', require('./routes/networking')); app.use('/architecture', require('./routes/architecture')); app.use('/sizing', require('./routes/sizing')); app.use('/infrastructure', require('./routes/infrastructure')); app.use('/finance', require('./routes/finance')); app.use('/settings', require('./routes/settings').router); app.use('/backup', require('./routes/backup')); app.use('/users', require('./routes/users')); app.use('/time', require('./routes/time')); app.use('/invoices', require('./routes/invoices')); app.use('/notifications', require('./routes/notifications').router); app.use('/calendar', require('./routes/calendar')); app.use('/search', require('./routes/search')); app.use('/audit', require('./routes/audit')); app.use('/email', require('./routes/email')); app.use('/quality', require('./routes/quality')); app.use('/market', require('./routes/market')); app.use('/consulting', require('./routes/consulting')); app.use('/projects', require('./routes/projects')); app.use('/reports', require('./routes/reports')); app.use('/integrations', require('./routes/integrations')); app.use('/ai', require('./routes/ai')); app.use('/api/v1', require('./routes/api')); app.use('/api/spa', require('./routes/spa')); // ---- React SPA (production build in public/spa) ---- const spaDir = path.join(__dirname, 'public', 'spa'); if (require('fs').existsSync(spaDir)) { app.use('/app', express.static(spaDir)); // History-API fallback: alle niet-bestand paden onder /app -> index.html app.get('/app/*', (req, res) => { res.sendFile(path.join(spaDir, 'index.html')); }); app.get('/app', (req, res) => { res.sendFile(path.join(spaDir, 'index.html')); }); } app.get('/health', async (req, res) => { try { const { getServicesStatus } = require('./lib/services'); const services = await getServicesStatus(); res.json({ status: 'ok', services, ts: new Date().toISOString() }); } catch (e) { res.status(500).json({ status: 'error', error: e.message }); } }); app.use((req, res) => { res.status(404).render('404', { path: req.path }); }); app.listen(PORT, '0.0.0.0', () => { console.log(`Mek-Tech Consulting Framework running on http://0.0.0.0:${PORT}`); });