Files
Webklar-Kundenbereich/server/index.js
T
ClaudeandClaude Opus 5.5 35d98da977 Chat: Bilder, Kamerafoto, Dateien und Sprachnachrichten
- Kundenbereich und Admin-Chat: Bueroklammer-Menue (Bild, Kamera, Datei),
  Mikrofon fuer Sprachnachrichten, Vorschau vor dem Senden mit
  Upload-Fortschritt, Einfuegen (Strg+V) und Hineinziehen von Dateien
- Kamera: am Handy/Tablet die Kamera-App, am Rechner die Kamera im Browser
  mit Vorschau, Kamerawechsel und "Nochmal"
- Sprachnachricht: Aufnahme mit Zeit und Pegel, vorher anhoerbar oder
  direkt senden, hoechstens 5 Minuten
- Verlauf: Bilder (einzeln bzw. Raster, Grossansicht), Player fuer
  Sprachnachrichten, Datei-Kacheln mit Download
- Server: jede Datei einzeln roh hochgeladen (max. 25 MB, 10 je Nachricht,
  max. 3 gleichzeitig), Typ aus dem Inhalt, nur erlaubte Endungen, keine
  Programme; Ablage unter upload-data/chat/<Kunde>/ ausserhalb von public/,
  Auslieferung nur mit Login; Bilder, Audio und PDF inline, alles andere
  als Download (nosniff, CSP sandbox)
- Nicht gesendete Anhaenge werden nach 24 Stunden geloescht
- Benachrichtigungs-Mail nennt nur Art und Namen der Anhaenge
- Appwrite: neues Attribut portalMessages.attachments
  (scripts/setup-chat-collection.mjs, vor dem Deploy ausfuehren)
- Datenschutzerklaerung Abschnitt 6 ergaenzt (Stand 26.09.2026), PDF neu

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 14:01:17 +00:00

141 lines
5.8 KiB
JavaScript

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 { syncPreviewTls } 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 takeoverCreditRoutes from './routes/takeoverCredits.js'
import { purgeExpiredUploads } from './services/takeoverCredits.js'
import { purgePendingChatUploads } from './services/chatAttachments.js'
import giteaWebhookRoutes from './routes/webhook/gitea.js'
import adminGiteaRoutes from './routes/admin/gitea.js'
import adminWebsiteProjectsRoutes from './routes/admin/websiteProjects.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'
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', customerDataRoutes)
app.use('/api/chat', chatRoutes)
app.use('/api/abo/takeover-credits', takeoverCreditRoutes)
app.use('/api/abo', aboRoutes)
app.use('/api/portal-admin', portalAdminRoutes)
app.use('/api/admin/gitea', adminGiteaRoutes)
app.use('/api/admin/website-projects', adminWebsiteProjectsRoutes)
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('/webhook', giteaWebhookRoutes)
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,
})
})
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}`)
syncPreviewTls()
.then((r) => console.log('[startup] Preview-TLS-Sync:', JSON.stringify(r)))
.catch((err) => console.error('[startup] Preview-TLS-Sync fehlgeschlagen:', err.message))
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()
})
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 <PID> /F. Oder in .env einen anderen PORT setzen (z. B. PORT=3002).`
)
process.exit(1)
}
throw err
})