Der GExtension-Abgleich liefert die Käufe, der Verbrauch steht im Forum. Die App liest jetzt die drei CC-Boards und sammelt die Anträge. Gelesen wird nur die Board-Übersicht: sie führt bereits den Ersteller, nicht nur den letzten Antwortenden. Damit bleibt ein Lauf bei wenigen Anfragen statt einer je Thread. Unterforen und globale Kästen tragen ebenfalls Thread-Links, deshalb liest der Parser strikt entlang der Listeneinträge - sonst landen Changelogs und Bewerbungen in der Antragsliste. Klassifiziert wird ausschließlich von Hand. Weder Titel noch Board sind verlässlich genug: Threads werden nicht immer verschoben, ein genehmigter Antrag kann weiter im Antrags-Board liegen. Das Board steht deshalb nur als Hinweis an der Zeile. Angenommene Anträge rufen dieselben Aktionen wie die Knöpfe auf der Spielerseite, es gibt keinen zweiten Buchungsweg mit eigenen Regeln. Wandert ein bereits entschiedener Thread später in ein anderes Board, erscheint das als Hinweis zum Abhaken - der CC-Status ändert sich nie von allein. Die Thread-ID bleibt beim Verschieben stabil und traegt deshalb die Wiedererkennung; die Board-Historie wird mitgeschrieben. Zurückgelassene Verweise erkennt der Parser und zählt sie nicht als eigenen Antrag.
63 lines
2.3 KiB
TypeScript
63 lines
2.3 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
const schema = z.object({
|
|
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
|
PORT: z.coerce.number().int().positive().default(3000),
|
|
HOST: z.string().default('0.0.0.0'),
|
|
|
|
DATABASE_URL: z.string().min(1, 'DATABASE_URL fehlt'),
|
|
SESSION_SECRET: z.string().min(32, 'SESSION_SECRET muss mindestens 32 Zeichen lang sein'),
|
|
|
|
// Lebensdauer einer Session in Stunden.
|
|
SESSION_TTL_HOURS: z.coerce.number().int().positive().default(12),
|
|
|
|
// Wird beim ersten Start angelegt, solange noch kein Account existiert.
|
|
BOOTSTRAP_ADMIN_USER: z.string().min(3).default('admin'),
|
|
BOOTSTRAP_ADMIN_PASSWORD: z.string().min(12).optional(),
|
|
|
|
// Hinter einem Reverse Proxy nötig, damit Client-IPs im Audit-Log stimmen.
|
|
TRUST_PROXY: z
|
|
.string()
|
|
.default('true')
|
|
.transform((v) => v === 'true' || v === '1'),
|
|
|
|
// Muss in Produktion true sein (Cookies nur über HTTPS).
|
|
COOKIE_SECURE: z
|
|
.string()
|
|
.optional()
|
|
.transform((v) => (v === undefined ? undefined : v === 'true' || v === '1')),
|
|
|
|
// Basis-Adresse der GExtension-Instanz. Der Parser ist auf deren Aufbau
|
|
// zugeschnitten; eine völlig andere Software ließe sich damit nicht lesen.
|
|
GEXTENSION_BASE_URL: z.string().url().default('https://gextension.senseigaming.de'),
|
|
|
|
// Basis-Adresse des Forums für den Antrags-Abgleich. Der Parser ist auf
|
|
// WoltLab (Burning Board) zugeschnitten.
|
|
FORUM_BASE_URL: z.string().url().default('https://senseigaming.de/forum'),
|
|
|
|
// Aufbewahrung der Audit-Einträge in Tagen. 0 schaltet das Aufräumen ab.
|
|
// Die Guthaben-Historie ist davon nicht betroffen, die bleibt dauerhaft.
|
|
AUDIT_RETENTION_DAYS: z.coerce.number().int().min(0).default(365),
|
|
|
|
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
|
|
});
|
|
|
|
const parsed = schema.safeParse(process.env);
|
|
|
|
if (!parsed.success) {
|
|
const issues = parsed.error.issues.map((i) => ` - ${i.path.join('.')}: ${i.message}`).join('\n');
|
|
console.error(`Konfiguration unvollständig:\n${issues}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const raw = parsed.data;
|
|
|
|
export const env = {
|
|
...raw,
|
|
isProduction: raw.NODE_ENV === 'production',
|
|
// In Produktion ohne expliziten Wert: sichere Cookies erzwingen.
|
|
cookieSecure: raw.COOKIE_SECURE ?? raw.NODE_ENV === 'production',
|
|
};
|
|
|
|
export type Env = typeof env;
|