const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const dns = require('dns');
const multer = require('multer');
// Minimal, dependency-free .env loader: load KEY=VALUE lines from a local
// (git-ignored) .env file into process.env without overwriting existing vars.
(function loadEnv() {
try {
const envPath = path.join(__dirname, '.env');
if (!fs.existsSync(envPath)) return;
for (const raw of fs.readFileSync(envPath, 'utf8').split('\n')) {
const line = raw.trim();
if (!line || line.startsWith('#')) continue;
const eq = line.indexOf('=');
if (eq === -1) continue;
const key = line.slice(0, eq).trim();
let val = line.slice(eq + 1).trim();
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1);
}
if (key && !(key in process.env)) process.env[key] = val;
}
} catch (e) {
console.warn('Konnte .env nicht laden:', e.message);
}
})();
const {
generateApplicationDocuments, generateEmailReply, generateFeinschliff, renderDesignVorschau,
DOKUMENT_TYPEN, normalizeDokumente, standardDokumente,
} = require('./lib/documents');
const chat = require('./lib/chat');
const websuche = require('./lib/websuche');
const promptStore = require('./lib/prompts');
const designStore = require('./lib/design');
const mailer = require('./lib/mailer');
const { createExternalApi } = require('./lib/api');
const { buildOpenApiSpec } = require('./lib/openapi');
const blacklist = require('./lib/blacklist');
const caldav = require('./lib/caldav');
const { LABEL_OPTIONS, parseLabels, serializeLabels } = require('./lib/labels');
const config = require('./lib/config');
const { userContext, currentUser, currentUserId } = require('./lib/context');
const password = require('./lib/password');
const migrate = require('./lib/migrate-multiuser');
const { runMigration, importEnvIntoAdmin } = migrate;
const suchprofil = require('./lib/suchprofil');
const suche = require('./lib/suche');
const app = express();
const PORT = process.env.PORT || 3000;
// Shared option lists (used in multiple views)
const ART_OPTIONS = [
'E-Mail', 'Online-Portal', 'Indeed', 'StepStone',
'Firmenwebsite', 'Post', 'Initiativbewerbung',
'Arbeitsagentur', 'Sonstiges'
];
const STATUS_OPTIONS = [
'Entwurf', 'Gesendet', 'Eingangsbestätigung',
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
'Vorstellungsgespräch', 'Vertragsverhandlung',
'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung'
];
// Base document types the user can provide as a foundation for AI tailoring
const BASIS_TYP_OPTIONS = ['Anschreiben', 'Lebenslauf', 'Profil/Kurzprofil', 'Sonstiges'];
// Pick the application "source" (art) for a browser-captured job. Honour an
// explicit value from the extension, otherwise infer it from the URL host so a
// capture from any website is labelled sensibly.
function deriveArt(url, provided) {
if (provided && ART_OPTIONS.includes(provided)) return provided;
const host = (String(url || '').match(/^https?:\/\/([^/]+)/i) || [, ''])[1].toLowerCase();
if (!host) return 'Sonstiges';
if (host.includes('indeed')) return 'Indeed';
if (host.includes('stepstone')) return 'StepStone';
if (host.includes('arbeitsagentur')) return 'Arbeitsagentur';
if (/(linkedin|xing|monster|stellenanzeigen|kimeta|glassdoor|jobware|meinestadt|jobs\.|karriere\.)/.test(host)) return 'Online-Portal';
return 'Firmenwebsite';
}
// Trust the Caddy reverse proxy (TLS terminator) so req.secure / req.ip reflect
// the real client connection — needed for the Secure cookie flag and rate limit.
app.set('trust proxy', 1);
// Don't advertise the framework version.
app.disable('x-powered-by');
// Express ships with helpful dev-mode error pages; in production it never leaks
// stack traces. We run behind Docker, so default to production unless overridden.
app.set('env', process.env.NODE_ENV || 'production');
// --- Security headers ----------------------------------------------------
// No helmet dependency: a small middleware sets the headers that matter. The
// CSP is intentionally permissive about *inline* scripts/styles (the views use a
// lot of inline