import 'dotenv/config'; import express from 'express'; import cors from 'cors'; import { initDatabase, pool } from './db.js'; import authRoutes from './routes/auth.js'; import userRoutes from './routes/users.js'; import leadRoutes from './routes/leads.js'; import connectionRoutes from './routes/connections.js'; import eventRoutes from './routes/events.js'; import opportunityRoutes from './routes/opportunities.js'; import acquisitionRoutes from './routes/acquisitions.js'; import badgeRoutes from './routes/badges.js'; import scannerRoutes from './routes/scanner.js'; import mailRoutes from './routes/mail.js'; import { requireAuth } from './middleware/auth.js'; import { initScannerDatabase } from './scanner/models/index.js'; import { startScheduler } from './scanner/services/scheduler.js'; import { SCANNER_KEY_CONFIGURED } from './scanner/utils/crypto.js'; import { startMailScheduler } from './mail/scheduler.js'; const app = express(); const PORT = Number(process.env.PORT || 5006); const HOST = process.env.HOST || '127.0.0.1'; // Avatare kommen als Data-URL - deshalb ein groesseres Limit als der Default. app.use(express.json({ limit: '6mb' })); // Im Produktivbetrieb liefert Apache Frontend und API unter derselben Origin // aus, CORS ist dann irrelevant. Fuer den lokalen Vite-Dev-Server auf einem // anderen Port braucht es die Freigabe. app.use( cors({ origin: (process.env.CORS_ORIGINS || 'http://localhost:3100,http://127.0.0.1:3100').split(','), }) ); app.get('/api/health', async (req, res) => { try { await pool.query('SELECT 1'); res.json({ ok: true, db: 'up' }); } catch { res.status(503).json({ ok: false, db: 'down' }); } }); app.use('/api/auth', authRoutes); app.use('/api/users', userRoutes); app.use('/api/leads', leadRoutes); app.use('/api/connections', connectionRoutes); app.use('/api/events', eventRoutes); app.use('/api/opportunities', opportunityRoutes); app.use('/api/acquisitions', acquisitionRoutes); // Zahlen fuer die Seitenleiste - eine Anfrage fuer alle Zaehler. app.use('/api/badges', badgeRoutes); // Projekt Scanner. Komplett hinter der CRM-Anmeldung - der urspruengliche // HTTP-Basic-Auth-Zugang des Scanners entfaellt damit. app.use('/api/scanner', requireAuth, scannerRoutes); // Postfachanbindung crm@perinco.de. Lesen darf jeder Angemeldete, konfigurieren // und anstossen nur Administratoren - das regeln die Routen selbst. app.use('/api/mail', requireAuth, mailRoutes); // Das Diktat laeuft komplett im Browser (Whisper ueber transformers.js) - das // Backend ist daran nicht beteiligt und sieht keine Audiodaten. app.use('/api', (req, res) => res.status(404).json({ error: 'Endpunkt nicht gefunden.' })); // Zentrale Fehlerbehandlung: Details ins Log, generische Meldung nach aussen. app.use((err, req, res, next) => { if (res.headersSent) return next(err); // Kaputtes JSON o.ae. ist ein Client-Fehler - kein 500, und es muss auch // nicht als unbehandelter Serverfehler im Log landen. const status = err?.status || err?.statusCode; if (err?.type === 'entity.parse.failed' || status === 400) { return res.status(400).json({ error: 'Ungültige Anfrage (JSON konnte nicht gelesen werden).' }); } if (err?.type === 'entity.too.large' || status === 413) { return res.status(413).json({ error: 'Die Anfrage ist zu gross (max. 6 MB).' }); } console.error('[perinco CRM] Unbehandelter Fehler:', err); res.status(500).json({ error: 'Interner Serverfehler.' }); }); try { await initDatabase(); /* * Der Scanner ist eine vollstaendig eigenstaendige Instanz: eigene Datenbank, * eigener Verschluesselungsschluessel, eigenes Upload-Verzeichnis, eigener * Scheduler. Er laeuft damit parallel zur alten App, ohne ihre Daten zu * beruehren. Fehlt die Datenbank, startet das CRM trotzdem - dann antworten * nur die Scanner-Endpunkte mit einem Fehler. */ let scannerReady = true; try { await initScannerDatabase(); } catch { scannerReady = false; console.warn('[Scanner] Bereich ist nicht verfuegbar - CRM startet ohne Scanner.'); } if (scannerReady && !SCANNER_KEY_CONFIGURED) { console.warn( '[Scanner] SCANNER_ENCRYPTION_KEY ist nicht gesetzt - Portal-Passwoerter werden mit ' + 'einem bekannten Ersatzschluessel verschluesselt. Fuer den Produktivbetrieb setzen.' ); } /* * Hintergrund-Scheduler (Scraping + OpenAI-Bewertung). Standard: AN, denn der * Parallelbetrieb soll funktional sein. Beide Scanner bewerten dieselben * Projekte unabhaengig voneinander - die OpenAI-Kosten fallen also doppelt an, * solange die alte App laeuft. Zum Abschalten SCANNER_SCHEDULER=false setzen. */ if (scannerReady && process.env.SCANNER_SCHEDULER !== 'false') { await startScheduler(); } else if (scannerReady) { console.log('[Scanner] Scheduler deaktiviert (SCANNER_SCHEDULER=false).'); } /* * Postfachabgleich (Microsoft Graph). Fehlen die Zugangsdaten in der .env, * meldet der Scheduler das und bleibt aus - die Oberflaeche laeuft weiter und * zeigt in den Einstellungen, welche Werte fehlen. */ await startMailScheduler(); app.listen(PORT, HOST, () => { console.log(`[perinco CRM] Backend laeuft auf http://${HOST}:${PORT}`); }); } catch (err) { console.error('[perinco CRM] Start fehlgeschlagen:', err); process.exit(1); }