import 'dotenv/config' import path from 'node:path' /** Einzige erlaubte Datenbank (Ticketsystem). Keine separate Portal-DB mehr. */ export const WOMS_DATABASE_ID = 'woms-database' const DEPRECATED_DATABASE_IDS = new Set([ '6943bf0e0003291f8c35', // frühere Portal-/Legacy-DB ]) function resolveDatabaseId() { const fromEnv = (process.env.APPWRITE_DATABASE_ID || '').trim() if (!fromEnv || fromEnv === WOMS_DATABASE_ID) return WOMS_DATABASE_ID if (DEPRECATED_DATABASE_IDS.has(fromEnv)) { console.warn( `[config] Veraltete APPWRITE_DATABASE_ID "${fromEnv}" – es wird nur ${WOMS_DATABASE_ID} verwendet.` ) return WOMS_DATABASE_ID } console.warn( `[config] APPWRITE_DATABASE_ID="${fromEnv}" ignoriert – Portal nutzt ausschließlich ${WOMS_DATABASE_ID}.` ) return WOMS_DATABASE_ID } export const config = { port: Number(process.env.PORT) || 3000, sessionSecret: process.env.SESSION_SECRET || '', cookieName: process.env.SESSION_COOKIE_NAME || 'webklar_portal_session', allowedCustomerStatuses: (process.env.ALLOWED_CUSTOMER_STATUSES || 'active') .split(',') .map((s) => s.trim()) .filter(Boolean), appwrite: { endpoint: process.env.APPWRITE_ENDPOINT || 'https://ticket.webklar.com/v1', projectId: process.env.APPWRITE_PROJECT_ID || '6a1058610003c5a13a05', databaseId: resolveDatabaseId(), apiKey: process.env.APPWRITE_API_KEY || '', }, collections: { customers: process.env.APPWRITE_COLLECTION_CUSTOMERS || 'customers', customerPortalAccess: process.env.APPWRITE_COLLECTION_CUSTOMER_PORTAL_ACCESS || 'customerPortalAccess', websiteProjects: process.env.APPWRITE_COLLECTION_WEBSITE_PROJECTS || 'websiteProjects', portalFeatures: process.env.APPWRITE_COLLECTION_PORTAL_FEATURES || 'portalFeatures', workorders: process.env.APPWRITE_COLLECTION_WORKORDERS || 'workorders', worksheets: process.env.APPWRITE_COLLECTION_WORKSHEETS || 'worksheets', employees: process.env.APPWRITE_COLLECTION_EMPLOYEES || 'employees', portalMessages: process.env.APPWRITE_COLLECTION_PORTAL_MESSAGES || 'portalMessages', subscriptions: process.env.APPWRITE_COLLECTION_SUBSCRIPTIONS || 'subscriptions', hourCredits: process.env.APPWRITE_COLLECTION_HOUR_CREDITS || 'hourCredits', takeoverCredits: process.env.APPWRITE_COLLECTION_TAKEOVER_CREDITS || 'takeoverCredits', aboOffers: process.env.APPWRITE_COLLECTION_ABO_OFFERS || 'aboOffers', }, publicBaseUrl: (process.env.PORTAL_PUBLIC_URL || 'https://project.webklar.com').replace(/\/$/, ''), // Wartungs-Abo (Stripe Billing). Preis/Produkt legt der Server per lookup_key selbst an. stripe: { secretKey: process.env.STRIPE_SECRET_KEY || '', webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '', priceLookupKey: process.env.STRIPE_ABO_PRICE_LOOKUP_KEY || 'webklar_wartung_monatlich_5000', priceAmountCents: 5000, productName: 'WEBklar Wartungs-Abo', productDescription: 'Betrieb und Pflege Ihrer Website bzw. Web-Anwendung auf unserem Server in Deutschland (Sicherheitsupdates, SSL, tägliche Datensicherung, Überwachung der Erreichbarkeit), 1 Domain inklusive Verwaltung, 1 E-Mail-Postfach (5 GB) und 1 Stunde Arbeitszeit pro Monat (12 Monate nutzbar). Mindestlaufzeit 12 Monate, danach monatlich kündbar. Gemäß § 19 UStG ohne Umsatzsteuer.', statementDescriptor: 'WEBKLAR WARTUNG', productImage: 'https://project.webklar.com/img/wartungs-abo-produkt.png', // Kleinunternehmer: Pflichthinweis auf jeder Stripe-Rechnung invoiceFooter: process.env.STRIPE_INVOICE_FOOTER || 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet (Kleinunternehmerregelung). WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler', }, abo: { // Bei jeder Textaenderung an Bedingungen oder Bestellseite hochziehen (wird mit der Zustimmung gespeichert) termsVersion: '2026-09-25-v3', // Mindestlaufzeit ab Leistungsbeginn; Kuendigung zum Ende bis 1 Monat vorher, danach monatlich minTermMonths: 12, minutesPerMonth: 60, creditValidityMonths: 12, usageRoundingMinutes: 15, withdrawalDays: 14, // "Beginn nach Ablauf der Widerrufsfrist": 15 Tage Probezeit ohne Leistung und ohne Abbuchung lateStartTrialDays: 15, // Pilotbetrieb: nur diese Kunden-E-Mails duerfen bestellen (leer = alle) pilotEmails: (process.env.ABO_PILOT_EMAILS || '') .split(',') .map((s) => s.trim().toLowerCase()) .filter(Boolean), // Testkonten zahlen nur den Stripe-Mindestbetrag (0,50 €) ueber einen Rabatt auf den echten Preis testEmails: (process.env.ABO_TEST_EMAILS || '') .split(',') .map((s) => s.trim().toLowerCase()) .filter(Boolean), testCouponId: 'WEBKLAR-TEST-050', }, // Individuelle Abo-Angebote (z. B. erste 2 Monate je 25 €): je Angebot ein einmal einloesbarer Stripe-Coupon offers: { maxMonths: 12, defaultValidityDays: 30, maxValidityDays: 366, }, // Uebernahmegutschrift: beim bisherigen Anbieter bereits bezahlte Zeit wird mit den Abo-Gebuehren verrechnet takeover: { // Bei jeder Textaenderung an den Bedingungen der Gutschrift hochziehen (wird mit dem Antrag gespeichert) conditionsVersion: '2026-09-25', // Nachweise liegen ausserhalb von public/ (gitignored, ueberlebt Deploys wie preview-data/) uploadDir: process.env.TAKEOVER_UPLOAD_DIR || path.join(process.cwd(), 'upload-data', 'takeover-credits'), maxFiles: 3, maxFileBytes: 8 * 1024 * 1024, maxTotalBytes: 10 * 1024 * 1024, // Plausibilitaetsgrenze fuer eine einzelne Gutschrift (Tippfehler abfangen) maxAmountCents: 100000, maxOpenPerCustomer: 3, // Nachweise abgelehnter Antraege werden nach dieser Frist geloescht (Datenschutzerklaerung Abschnitt 7) rejectedRetentionDays: 183, }, // Chat-Anhaenge (Bilder, Dateien, Kamerafotos, Sprachnachrichten): liegen ausserhalb von public/ // (gitignored, ueberleben Deploys wie preview-data/) chat: { uploadDir: process.env.CHAT_UPLOAD_DIR || path.join(process.cwd(), 'upload-data', 'chat'), maxFileBytes: 25 * 1024 * 1024, maxFilesPerMessage: 10, // Missbrauchsschutz: Uploads je Kunde und Stunde, Gesamtspeicher je Kunde, freier Platz auf dem Server maxUploadsPerHour: 200, maxBytesPerCustomer: 1024 * 1024 * 1024, minFreeDiskBytes: 2 * 1024 * 1024 * 1024, // Hochgeladen, aber nie abgeschickt: nach dieser Zeit loeschen (Datenschutzerklaerung Abschnitt 6) pendingTtlHours: 24, }, smtp: { host: process.env.SMTP_HOST || '', port: Number(process.env.SMTP_PORT) || 587, secure: (process.env.SMTP_SECURE || 'false') === 'true', user: process.env.SMTP_USERNAME || '', pass: process.env.SMTP_PASSWORD || '', from: process.env.SMTP_FROM || process.env.SMTP_USERNAME || '', }, chatNotify: { emails: (process.env.CHAT_NOTIFY_EMAILS || 'kenso@webklar.com,andrej@webklar.com') .split(',') .map((s) => s.trim()) .filter(Boolean), throttleMin: Number(process.env.CHAT_NOTIFY_THROTTLE_MIN) || 15, }, gitea: { webhookToken: process.env.GITEA_WEBHOOK_TOKEN || '', baseUrl: process.env.GITEA_BASE_URL || 'https://git.webklar.com', apiToken: process.env.GITEA_API_TOKEN || '', templateOwner: process.env.GITEA_TEMPLATE_OWNER || 'knso', templateRepo: process.env.GITEA_TEMPLATE_REPO || 'webklar-preview-template', defaultOwner: process.env.GITEA_DEFAULT_OWNER || 'knso', }, cors: { allowedOrigins: (process.env.ADMIN_CORS_ORIGINS || 'https://ticket.webklar.com,http://localhost:5173') .split(',') .map((s) => s.trim()) .filter(Boolean), }, preview: { baseHost: process.env.PREVIEW_BASE_HOST || 'project.webklar.com', deployRoot: process.env.PREVIEW_DEPLOY_ROOT || '', traefikDynamicDir: process.env.TRAEFIK_DYNAMIC_DIR || '', sessionSecret: process.env.PREVIEW_SESSION_SECRET || process.env.SESSION_SECRET || '', cookieDomain: process.env.PREVIEW_COOKIE_DOMAIN || '.project.webklar.com', sessionMaxAge: Number(process.env.PREVIEW_SESSION_MAX_AGE || 60 * 60 * 24 * 7), }, invoiceNinja: { url: (process.env.INVOICE_NINJA_URL || 'https://invoice.webklar.com').replace(/\/$/, ''), apiToken: process.env.INVOICE_NINJA_API_TOKEN || '', }, } export function assertServerConfig() { if (!config.sessionSecret || config.sessionSecret.length < 32) { console.warn('[config] SESSION_SECRET fehlt oder ist zu kurz (min. 32 Zeichen).') } if (!config.appwrite.apiKey) { console.warn('[config] APPWRITE_API_KEY fehlt – DB-Zugriff und Webhook schlagen fehl.') } }