Files
jobbi-bewerbung/lib/config.js
T
thomasandClaude Opus 4.8 1dd4807ca6 Einstellungen: Vorauswahl für KI-Generierung (Dokumente + Anlagen)
Zwei neue, pro Benutzer gespeicherte Konfigurationswerte:
GEN_DOKUMENTE_DEFAULT (Standard: Anschreiben + Lebenslauf) und
GEN_ANLAGEN_DEFAULT (Standard: keine). Sie belegen die Häkchen auf der
Bewerbungsseite vor; eine bereits getroffene Dokumentenauswahl der
Bewerbung gewinnt weiterhin. Die REST-API nutzt die Vorauswahl, wenn
dokumente/anlagen im Body fehlen.

Die Einstellungsseite kennt dafür jetzt Checkbox-Gruppen, deren Optionen
auch erst zur Renderzeit feststehen dürfen (die eigenen Anlagen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 22:31:19 +02:00

259 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Centralized, per-user configuration store.
//
// Multi-user: every setting (Ollama, E-Mail, CalDAV, API-Token) is owned by a
// user. The values live in the SQLite app_state table (key prefixed "cfg:"),
// now keyed by (user_id, key). The /einstellungen page edits the *current* user's
// values; the libs (mailer, caldav, chat, documents) read through config.get() /
// config.ollama() at call time, picking up the current user from the per-request
// context (lib/context.js). An edit therefore takes effect immediately, scoped
// to the user who made it — no restart, no .env file.
//
// On first start of an install that previously used .env (or that still passes
// the values as container env vars), the still-present env values are imported
// into the *admin* user's rows once — see importEnvIntoAdmin(), called from
// server.js at boot. After that the database is the single source of truth.
//
// process.env is deliberately NOT a read fallback: env config belongs to the
// admin, so falling back to it would hand every freshly created user the admin's
// mailbox, calendar, Ollama key and API token. Unset keys resolve to the
// built-in defaults instead, which leaves a new user's settings empty.
const { currentUser } = require('./context');
const DEFAULTS = {
// Ollama Cloud (KI text generation + chat).
OLLAMA_API_KEY: '',
OLLAMA_MODEL: 'glm-5.2:cloud',
OLLAMA_HOST: 'https://ollama.com',
OLLAMA_TIMEOUT_MS: '300000',
// Which documents are preselected when a generation is started (comma separated,
// see DOKUMENT_TYPEN in lib/documents.js). Empty means both.
GEN_DOKUMENTE_DEFAULT: 'anschreiben,lebenslauf',
// Which static attachments (basis_anhaenge IDs, comma separated) are preselected.
// Empty = none, which is the default.
GEN_ANLAGEN_DEFAULT: '',
// E-Mail (SMTP submission + IMAP receive).
MAIL_HOST: '',
MAIL_SMTP_PORT: '587',
MAIL_IMAP_PORT: '993',
MAIL_USER: '',
MAIL_PASSWORD: '',
MAIL_FROM_NAME: '',
MAIL_FROM: '',
MAIL_IMAP_MAILBOX: 'INBOX',
MAIL_POLL_MS: '180000',
// Bewerbungskalender (CalDAV, z. B. SOGo).
CALDAV_URL: '',
CALDAV_ALARM_MIN: '60',
CALDAV_POLL_MS: '300000',
// REST-API für Drittanbietersoftware (/api/v1, Header X-API-Key).
API_TOKEN: '',
};
// UI metadata for the /einstellungen page: grouped sections with input types.
// `secret` fields render as password inputs with a reveal toggle.
const FIELDS = [
{
titel: 'Ollama Cloud (KI-Generierung + Chat)',
beschreibung: 'Steuerung der KI, die Bewerbungsunterlagen erzeugt und den KI-Chat beantwortet. Ohne API-Schlüssel sind diese Funktionen deaktiviert.',
items: [
{ key: 'OLLAMA_API_KEY', label: 'API-Schlüssel', secret: true, help: 'Schlüssel von https://ollama.com/settings/keys' },
{ key: 'OLLAMA_MODEL', label: 'Modell', help: 'Standard: glm-5.2:cloud' },
{ key: 'OLLAMA_HOST', label: 'Host', help: 'Standard: https://ollama.com — lokaler Ollama: http://localhost:11434' },
{ key: 'OLLAMA_TIMEOUT_MS', label: 'Timeout (ms)', help: 'Standard: 300000 (5 Min.)' },
],
},
{
titel: 'Bewerbungsunterlagen (Vorauswahl)',
beschreibung: 'Was ist auf der Bewerbungsseite vorab angehakt, wenn die KI-Generierung gestartet wird? Pro Bewerbung lässt sich die Auswahl weiterhin ändern; die zuletzt gewählten Dokumente bleiben an der jeweiligen Bewerbung erhalten.',
items: [
{
key: 'GEN_DOKUMENTE_DEFAULT',
label: 'Standardmäßig generieren',
type: 'checkboxes',
options: [
{ value: 'anschreiben', label: 'Anschreiben' },
{ value: 'lebenslauf', label: 'Lebenslauf' },
],
help: 'Standard: beides. Die E-Mail-Begleitnachricht wird immer erzeugt. Ohne Häkchen gilt wieder beides.',
},
{
key: 'GEN_ANLAGEN_DEFAULT',
label: 'Standardmäßig beigelegte Anlagen',
type: 'checkboxes',
// Auswahlmöglichkeiten sind die eigenen Anlagen aus „Vorlagen“ und stehen
// erst zur Renderzeit fest — die Route reicht sie unter diesem Namen hinein.
optionsFrom: 'anlagen',
leerHinweis: 'Noch keine Anlagen hinterlegt unter „Vorlagen“ kannst du Zeugnisse & Co. hochladen.',
help: 'Standard: keine. Angehakte Anlagen werden bei jeder Generierung vorausgewählt und im Anschreiben unter „Anlagen“ aufgeführt.',
},
],
},
{
titel: 'E-Mail (SMTP-Versand + IMAP-Empfang)',
beschreibung: 'Versand läuft über den eigenen Mailserver (DKIM/SPF/DMARC-Alignment). Ohne Host/Benutzer/Passwort ist der E-Mail-Teil deaktiviert. Pro Benutzer eigenes Postfach.',
items: [
{ key: 'MAIL_HOST', label: 'SMTP/IMAP Host' },
{ key: 'MAIL_SMTP_PORT', label: 'SMTP-Port', help: '587 = STARTTLS, 465 = implicit TLS' },
{ key: 'MAIL_IMAP_PORT', label: 'IMAP-Port', help: 'Standard: 993 (implicit TLS)' },
{ key: 'MAIL_USER', label: 'Benutzername (Login)', help: 'Auch CalDAV-Login' },
{ key: 'MAIL_PASSWORD', label: 'Passwort', secret: true, help: 'Auch CalDAV-Passwort' },
{ key: 'MAIL_FROM_NAME', label: 'Absendername' },
{ key: 'MAIL_FROM', label: 'Absenderadresse', help: 'Leer = Benutzername' },
{ key: 'MAIL_IMAP_MAILBOX', label: 'IMAP-Postfach', help: 'Standard: INBOX' },
{ key: 'MAIL_POLL_MS', label: 'Abrufintervall (ms)', help: 'Standard: 180000 (3 Min.) — greift nach Neustart' },
],
},
{
titel: 'Bewerbungskalender (CalDAV)',
beschreibung: 'Voll-URL der Kalender-Sammlung (mit abschließendem /). Authentifizierung läuft über MAIL_USER/MAIL_PASSWORD. Ohne URL sind die Kalender-Funktionen deaktiviert.',
items: [
{ key: 'CALDAV_URL', label: 'Kalender-URL', help: 'z. B. https://mail.example.com/SOGo/dav/name@…/Calendar/XXXX/' },
{ key: 'CALDAV_ALARM_MIN', label: 'Erinnerung (Minuten vor Termin)', help: 'Standard: 60' },
{ key: 'CALDAV_POLL_MS', label: 'Sync-Intervall (ms)', help: 'Standard: 300000 (5 Min.) — greift nach Neustart' },
],
},
{
titel: 'REST-API für Drittanbietersoftware',
beschreibung: 'Ist ein Token gesetzt, ist /api/v1 für diesen Benutzer aktiv und erwartet den Wert im Header „X-API-Key“. Der Token identifiziert den Benutzer: Anfragen sehen und ändern ausschließlich dessen eigene Daten. Ohne Token antwortet die API (bis auf /health) mit 401. Swagger unter /swagger.',
items: [
{
key: 'API_TOKEN',
label: 'API-Token (X-API-Key)',
secret: true,
generate: true,
help: 'Leer = API deaktiviert. Über das Auge einblenden, „Neu generieren“ erzeugt einen zufälligen Token — danach speichern. Ein neuer Token macht den alten sofort ungültig.',
},
],
},
];
const PREFIX = 'cfg:';
// Per-user cache: Map<userId, Object<string,string>>. A user is loaded lazily on
// first access (ensureLoaded) and stays cached for the process lifetime. Edits
// via saveAll() update the cache in place so subsequent reads are consistent.
const cache = new Map(); // userId -> { key: value }
const loaded = new Set(); // userIds whose cfg rows have been read from the DB
let dbAllFn = null;
let dbRunFn = null;
// The built-in default for a key. Deliberately NOT a process.env lookup: env
// values belong to the admin (they are imported into the admin's rows once at
// boot, see importEnvIntoAdmin in lib/migrate-multiuser.js). If get() fell back
// to process.env, every user without their own row would silently inherit the
// admin's credentials — their own mailbox, calendar, Ollama key and API token.
// The defaults below are non-secret standards only (model, host, ports, poll
// intervals); every credential defaults to empty, so a new user starts with
// Ollama/E-Mail/CalDAV/API switched off until they configure their own.
function builtinDefault(key) {
return DEFAULTS[key];
}
// Load one user's cfg rows from the DB into the cache. No-op if already loaded.
async function ensureLoaded(userId) {
if (!userId || loaded.has(userId)) return;
const rows = await dbAllFn('SELECT key, value FROM app_state WHERE user_id = ?', [userId]);
const obj = Object.create(null);
for (const r of rows) obj[r.key.slice(PREFIX.length)] = r.value;
cache.set(userId, obj);
loaded.add(userId);
}
// Drop the cached rows for a user so the next read reloads from the DB. Used
// after a direct DB write outside saveAll() (e.g. the one-time env migration).
function invalidate(userId) {
loaded.delete(userId);
cache.delete(userId);
}
// Synchronous read for the *current user only*. A key the user has not stored
// falls back to the built-in default — never to another user's value and never
// to process.env. A user who has configured nothing therefore reads as "empty"
// (no mail host, no keys), which is exactly what isolates them: mailer/caldav
// isConfigured() turns false and their background jobs stay idle.
//
// Outside a request context (boot) there is no user, so only the defaults apply.
// Callers that need a specific user's values must run inside that user's context
// (see lib/context.js) and must have called ensureLoaded(userId) first.
function get(key) {
const u = currentUser();
const userObj = u ? cache.get(u.id) : null;
if (userObj && userObj[key] !== undefined) return userObj[key];
return builtinDefault(key);
}
// All keys with their effective values for the current user — used by the
// settings UI. Must be called within a request context.
function getAll() {
const out = {};
for (const key of Object.keys(DEFAULTS)) out[key] = get(key);
return out;
}
// The current user's preselected static attachments (GEN_ANLAGEN_DEFAULT) as
// basis_anhaenge IDs. Deleted attachments may linger in the list; callers match
// them against the existing rows, so stale IDs simply never hit.
function anlagenDefaultIds() {
return String(get('GEN_ANLAGEN_DEFAULT') || '')
.split(',')
.map((v) => parseInt(v, 10))
.filter((n) => !Number.isNaN(n));
}
// Ollama bundle (shared by lib/documents.js + lib/chat.js), for the current user.
function ollama() {
return {
host: (get('OLLAMA_HOST') || 'https://ollama.com').replace(/\/+$/, ''),
model: get('OLLAMA_MODEL') || 'glm-5.2:cloud',
timeoutMs: Number(get('OLLAMA_TIMEOUT_MS')) || 300000,
apiKey: get('OLLAMA_API_KEY') || '',
};
}
// Wire up DB helpers. The one-time .env migration now runs per-admin at boot
// from server.js (it needs the admin user id); this init only stores the fns.
async function init({ dbAll, dbRun }) {
dbAllFn = dbAll;
dbRunFn = dbRun;
}
// Persist every key for the current user (writes all rows, including empty
// strings, so a cleared field is stored as empty and no longer falls back to
// env/default). Must be called within a request context.
async function saveAll(values) {
if (!dbRunFn) throw new Error('Konfigurationsspeicher nicht initialisiert.');
const u = currentUser();
if (!u) throw new Error('Kein Benutzerkontext für Konfigurationsspeicherung.');
const obj = cache.get(u.id) || Object.create(null);
for (const key of Object.keys(DEFAULTS)) {
const v = values && values[key] != null ? String(values[key]) : '';
await dbRunFn(
'INSERT INTO app_state (user_id, key, value) VALUES (?, ?, ?) ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value',
[u.id, PREFIX + key, v]
);
obj[key] = v;
}
cache.set(u.id, obj);
loaded.add(u.id);
}
// Write a single key for an explicit user (used by the one-time env migration,
// which runs outside a request context). Updates the cache if loaded.
async function setForUser(userId, key, value) {
if (!dbRunFn) throw new Error('Konfigurationsspeicher nicht initialisiert.');
await dbRunFn(
'INSERT INTO app_state (user_id, key, value) VALUES (?, ?, ?) ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value',
[userId, PREFIX + key, String(value)]
);
if (loaded.has(userId)) {
const obj = cache.get(userId) || Object.create(null);
obj[key] = String(value);
cache.set(userId, obj);
}
}
module.exports = {
DEFAULTS, FIELDS, PREFIX,
get, getAll, ollama, anlagenDefaultIds, init, saveAll, setForUser, ensureLoaded, invalidate,
};