Files
perinco-CRM/server/src/server.js
T
Sascha WolfandClaude Opus 5.5 e35c7518db feat(ki): Einstellungen -> KI mit OpenAI-Schluessel und Modellwahl
Neuer Admin-Reiter "KI": zeigt die letzten vier Zeichen des hinterlegten
OpenAI-Schluessels samt Quelle, nimmt einen neuen an (vorher gegen
/v1/models geprueft) und laesst je Einsatzzweck das Modell per Dropdown
waehlen - Scanner-Bewertung, Abgleich "bereits beworben", erweiterte Suche.
Die Auswahl kommt live von OpenAI, Vorgaben sind die bisher fest
verdrahteten Modelle. Reasoning-Modelle bekommen reasoning_effort statt
temperature, damit ein Wechsel keinen 400er ausloest.

Schluesselfeld aus dem Scanner-Reiter entfernt; Doku in docs/14-ki.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:31:55 +02:00

160 lines
6.1 KiB
JavaScript

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 dashboardRoutes from './routes/dashboard.js';
import scannerRoutes from './routes/scanner.js';
import mailRoutes from './routes/mail.js';
import integrationRoutes from './routes/integrations.js';
import iconCrmRoutes from './routes/iconCrm.js';
import searchRoutes from './routes/search.js';
import aiRoutes from './routes/ai.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' });
}
});
/*
* Leseschnittstelle fuer das ICON CRM. Eigener Token statt JWT - der Aufrufer
* ist das ICON-Backend, kein angemeldeter Mensch. Nur GET.
*/
app.use('/api/integrations', integrationRoutes);
// Gegenrichtung: die Kontakte des ICON CRM lesend anzeigen. Nichts wird gespeichert.
app.use('/api/icon', iconCrmRoutes);
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/leads', leadRoutes);
// Globale Suche (Volltext) und die erweiterte Suche mit dem Modell des Scanners.
app.use('/api/search', searchRoutes);
// OpenAI-Schluessel und Modellwahl (Einstellungen -> KI, nur admin).
app.use('/api/ai', aiRoutes);
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);
// Auswertungen des Dashboards.
app.use('/api/dashboard', dashboardRoutes);
// 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);
}