import { runRepoAssignJob } from './services/repoAssignJob.js' import express from 'express' import path from 'node:path' import { fileURLToPath } from 'node:url' import { config, assertServerConfig, WOMS_DATABASE_ID } from './config.js' import { verifyDatabaseAccess } from './services/appwriteAdmin.js' import { syncPreviewTlsOnStartup } from './services/previewDeploy.js' import { requireSession, sessionMiddleware } from './middleware/session.js' import authRoutes from './routes/auth.js' import projectsRoutes from './routes/projects.js' import featuresRoutes from './routes/features.js' import customerDataRoutes from './routes/customerData.js' import portalAdminRoutes from './routes/portalAdmin.js' import chatRoutes from './routes/chat.js' import aboRoutes from './routes/abo.js' import funktionenRoutes from './routes/funktionen.js' import takeoverCreditRoutes from './routes/takeoverCredits.js' import { purgeExpiredUploads } from './services/takeoverCredits.js' import { purgePendingChatUploads } from './services/chatAttachments.js' import { purgeUnclaimedNewsImages } from './services/newsImages.js' import { runActivation } from './services/siteFeatures.js' import giteaWebhookRoutes from './routes/webhook/gitea.js' import pushTimeRoutes from './routes/pushTime.js' import adminGiteaRoutes from './routes/admin/gitea.js' import adminWebsiteProjectsRoutes from './routes/admin/websiteProjects.js' import adminServicesRoutes from './routes/admin/services.js' import adminCustomersRoutes from './routes/admin/customers.js' import adminEmployeesRoutes from './routes/admin/employees.js' import adminInvoiceClientsRoutes from './routes/admin/invoiceClients.js' import adminPreviewOpenRoutes from './routes/admin/previewOpen.js' import newsRoutes from './routes/news.js' import publicNewsRoutes from './routes/publicNews.js' import adminSwitchLinkRoutes from './routes/admin/switchLink.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const publicDir = path.join(__dirname, '..', 'public') assertServerConfig() const app = express() app.set('trust proxy', 1) app.use(sessionMiddleware()) // Uebernahmegutschrift: Nachweise kommen als Base64 im JSON (max. 10 MB Dateien) -> eigenes Limit, // muss vor dem globalen Parser stehen (der ueberspringt bereits geparste Bodies). Login-Pruefung // zuerst, damit niemand ohne Session grosse Bodies parsen laesst. app.use('/api/abo/takeover-credits', requireSession, express.json({ limit: '15mb' })) // rawBody wird fuer die HMAC-Verifikation des Gitea-Webhooks benoetigt app.use(express.json({ limit: '2mb', verify: (req, _res, buf) => { req.rawBody = buf } })) app.use((req, res, next) => { const origin = req.get('Origin') if (origin && config.cors.allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin) res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type') res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS') if (req.method === 'OPTIONS') { return res.sendStatus(204) } } return next() }) app.use('/api/auth', authRoutes) app.use('/api/projects', projectsRoutes) app.use('/api/features', featuresRoutes) app.use('/api/public/news', publicNewsRoutes) app.use('/api/news', newsRoutes) app.use('/api', customerDataRoutes) app.use('/api/chat', chatRoutes) app.use('/api/abo/takeover-credits', takeoverCreditRoutes) app.use('/api/abo', aboRoutes) app.use('/api/funktionen', funktionenRoutes) app.use('/api/portal-admin', portalAdminRoutes) app.use('/api/admin/gitea', adminGiteaRoutes) app.use('/api/admin/website-projects', adminWebsiteProjectsRoutes) app.use('/api/admin/services', adminServicesRoutes) app.use('/api/admin/customers', adminCustomersRoutes) app.use('/api/admin/employees', adminEmployeesRoutes) app.use('/api/admin/invoice-clients', adminInvoiceClientsRoutes) app.use('/api/admin/preview-open', adminPreviewOpenRoutes) app.use('/api/admin/switch-link', adminSwitchLinkRoutes) app.use('/webhook', giteaWebhookRoutes) // Push-Zeitmail: Zeit eines Git-Pushes per Link aus der Mail eintragen (ohne Login, signierter Token) app.use('/zeit', express.urlencoded({ extended: false, limit: '4kb' }), pushTimeRoutes) app.get('/api/health', async (_req, res) => { const dbAccess = await verifyDatabaseAccess() res.json({ ok: dbAccess.ok, service: 'webklar-kundenbereich', databaseId: WOMS_DATABASE_ID, endpoint: config.appwrite.endpoint, appwriteDbAccess: dbAccess, }) }) // Einbau-Skript fuer Kundenwebsites: darf von fremden Seiten geladen werden, 5 Minuten Cache app.use('/embed/', (_req, res, next) => { res.set('Cache-Control', 'public, max-age=300') res.set('Access-Control-Allow-Origin', '*') next() }) app.use(express.static(publicDir)) app.get('/dashboard.html', (req, res, next) => { const raw = req.signedCookies?.[config.cookieName] if (!raw) { return res.redirect('/login.html') } next() }) app.get('/', (_req, res) => { res.redirect('/login.html') }) app.use((err, _req, res, _next) => { console.error('[server] Unbehandelter Fehler:', err) if (!res.headersSent) { res.status(500).json({ error: err.message || 'Interner Serverfehler' }) } }) const server = app.listen(config.port, '0.0.0.0', () => { console.log(`Webklar Kundenbereich läuft auf Port ${config.port}`) syncPreviewTlsOnStartup() verifyDatabaseAccess().then((result) => { if (result.ok) return console.error( '[startup] APPWRITE_API_KEY: Kein Lesezugriff auf woms-database. In Appwrite Console neuen API-Key mit Scopes databases.read + databases.write anlegen und in .env eintragen.' ) if (result.reason) console.error(`[startup] Appwrite: ${result.reason}`) }) // Nachweise abgelehnter Uebernahmegutschriften nach Ablauf der Aufbewahrungsfrist loeschen (taeglich) const purge = () => purgeExpiredUploads() .then((r) => r.purged && console.log(`[takeover] Nachweise von ${r.purged} abgelehnten Anträgen gelöscht`)) .catch((err) => console.warn('[takeover] Aufräumen fehlgeschlagen:', err.message)) setTimeout(purge, 60 * 1000).unref() setInterval(purge, 24 * 60 * 60 * 1000).unref() // Chat-Anhaenge, die nie abgeschickt wurden, nach config.chat.pendingTtlHours loeschen (stuendlich) const purgeChat = () => purgePendingChatUploads() .then((r) => r.purged && console.log(`[chat] ${r.purged} nicht gesendete Anhänge gelöscht`)) .catch((err) => console.warn('[chat] Aufräumen fehlgeschlagen:', err.message)) setTimeout(purgeChat, 90 * 1000).unref() setInterval(purgeChat, 60 * 60 * 1000).unref() // News-Fotos, die nie an einen Beitrag gebunden wurden, nach 24 Stunden loeschen (stuendlich) const purgeNewsImages = () => purgeUnclaimedNewsImages() .then((n) => n && console.log(`[news] ${n} nicht verwendete Fotos gelöscht`)) .catch((err) => console.warn('[news] Aufräumen fehlgeschlagen:', err.message)) setTimeout(purgeNewsImages, 120 * 1000).unref() setInterval(purgeNewsImages, 60 * 60 * 1000).unref() // Funktionen (News-Pinnwand): geschlossene Einrichtungs-Tickets freischalten und den Kunden benachrichtigen (alle 5 Minuten) const activateFeatures = () => runActivation() .then((done) => done.length && console.log(`[funktionen] ${done.length} Funktion(en) freigeschaltet`)) .catch((err) => console.warn('[funktionen] Freischalten fehlgeschlagen:', err.message)) setInterval(activateFeatures, 5 * 60 * 1000).unref() // Neue Gitea-Repos erkennen, Zuordnungs-Mail senden, Erinnerungen/Wiederholungen (alle 10 Minuten) const repoAssign = () => runRepoAssignJob({ now: new Date() }).catch((err) => console.warn('[repoAssign] Lauf fehlgeschlagen:', err.message) ) setTimeout(repoAssign, 60 * 1000).unref() setInterval(repoAssign, 10 * 60 * 1000).unref() }) server.on('error', (err) => { if (err.code === 'EADDRINUSE') { console.error( `[server] Port ${config.port} ist bereits belegt. Windows: netstat -ano | findstr :${config.port} → taskkill /PID /F. Oder in .env einen anderen PORT setzen (z. B. PORT=3002).` ) process.exit(1) } throw err })