import { Router } from 'express' import { config } from '../config.js' import { getCustomerByAppwriteUserId, getCustomerByEmail, getDocument, getPortalAccessByCustomerId, listDocuments, updateDocument, Query, } from '../services/appwriteAdmin.js' import { loginWithAppwrite, getLoginCooldownRemainingSec } from '../services/appwriteClient.js' import { clearPortalSession, setPortalSession, } from '../middleware/session.js' import { PREVIEW_COOKIE_NAME, clearPreviewSession, createPreviewSessionToken, previewSessionCookieOptions, verifyPreviewSessionToken, } from '../services/previewSession.js' import { consumeStaffPreviewToken } from '../services/staffPreviewTokens.js' const router = Router() function sanitizeCustomer(customer) { return { id: customer.$id, code: customer.code || '', name: customer.name || '', companyName: customer.companyName || '', email: customer.email || '', phone: customer.phone || '', location: customer.location || '', customerStatus: customer.customerStatus || '', portalAccessEnabled: Boolean(customer.portalAccessEnabled), } } async function validatePortalAccess(appwriteUserId, email) { let customer = await getCustomerByAppwriteUserId(appwriteUserId) if (!customer && email) { customer = await getCustomerByEmail(email) } if (!customer) { const error = new Error( `Kein Kundenkonto für diesen Login gefunden. Im Ticketsystem customers.appwriteUserId auf "${appwriteUserId}" setzen (E-Mail: ${email}).` ) error.status = 403 throw error } if (!customer.portalAccessEnabled) { const error = new Error('Portalzugang ist nicht freigeschaltet.') error.status = 403 throw error } const portalAccess = await getPortalAccessByCustomerId(customer.$id) if (!portalAccess || !portalAccess.enabled) { const error = new Error('Portalzugang ist deaktiviert.') error.status = 403 throw error } const status = (customer.customerStatus || '').toLowerCase() if (!config.allowedCustomerStatuses.includes(status)) { const error = new Error('Kundenkonto ist nicht aktiv.') error.status = 403 throw error } return { customer, portalAccess } } async function syncCustomerAppwriteUserId(customer, appwriteUserId) { if (!customer?.$id || !appwriteUserId) return customer if (customer.appwriteUserId === appwriteUserId) return customer const now = new Date().toISOString() return updateDocument(config.collections.customers, customer.$id, { appwriteUserId, portalAccessEnabled: true, updatedAt: now, }) } async function resolveSessionCustomer(session) { if (!session?.customerId) return null let customer = await getDocument(config.collections.customers, session.customerId).catch(() => null) if (customer) return customer if (session.appwriteUserId) { customer = await getCustomerByAppwriteUserId(session.appwriteUserId) if (customer) return customer } if (session.email) { customer = await getCustomerByEmail(session.email) } return customer } function isPreviewProjectReady(project) { if (!project) return false if (project.hasPreview === false) return false const status = (project.provisioningStatus || project.status || '').toLowerCase() return ['ready', 'deployed'].includes(status) } function isAdminSession(session) { return session?.role === 'admin' || (session?.labels || []).includes('admin') } function safePreviewRedirectUrl(url) { try { const parsed = new URL(String(url || '')) if ( parsed.hostname.endsWith('.project.webklar.com') || parsed.hostname === 'project.webklar.com' ) { return parsed.toString() } } catch { /* ignore */ } return null } router.get('/login-status', (_req, res) => { const retryAfterSeconds = getLoginCooldownRemainingSec() res.json({ blocked: retryAfterSeconds > 0, retryAfterSeconds, }) }) router.post('/login', async (req, res) => { const { email, password } = req.body || {} if (!email || !password) { return res.status(400).json({ error: 'E-Mail und Passwort erforderlich' }) } try { const user = await loginWithAppwrite(email.trim(), password) let { customer, portalAccess } = await validatePortalAccess(user.$id, email.trim()) customer = await syncCustomerAppwriteUserId(customer, user.$id) setPortalSession(res, { customerId: customer.$id, appwriteUserId: user.$id, name: customer.name || user.name || '', email: customer.email || user.email || email, }) const previewToken = await createPreviewSessionToken({ userId: user.$id, customerId: customer.$id, email: customer.email || user.email || email, name: customer.name || user.name || '', role: 'customer', labels: [], authSource: 'kundenbereich', }) res.cookie(PREVIEW_COOKIE_NAME, previewToken, previewSessionCookieOptions()) try { await updateDocument(config.collections.customerPortalAccess, portalAccess.$id, { lastLoginAt: new Date().toISOString(), }) } catch (err) { console.warn('[auth] lastLoginAt update failed:', err.message) } return res.json({ success: true, customer: sanitizeCustomer(customer) }) } catch (err) { const status = err.status || 500 if (status === 429) { return res.status(429).json({ error: err.message || 'Zu viele Anmeldeversuche. Bitte warte einige Minuten, bevor du es erneut versuchst.', retryAfterSeconds: getLoginCooldownRemainingSec(), }) } if (err?.message?.includes('not authorized')) { return res.status(500).json({ error: 'Server-Konfiguration: APPWRITE_API_KEY benötigt databases.read für woms-database (customers, customerPortalAccess).', }) } return res.status(status).json({ error: err.message || 'Anmeldung fehlgeschlagen' }) } }) router.post('/logout', (_req, res) => { clearPortalSession(res) clearPreviewSession(res) res.json({ success: true }) }) router.get('/preview-access', async (req, res) => { const token = req.cookies?.[PREVIEW_COOKIE_NAME] if (!token) { return res.status(401).json({ allowed: false }) } const session = await verifyPreviewSessionToken(token) const admin = isAdminSession(session) if (!session || (!session.customerId && !admin)) { return res.status(401).json({ allowed: false }) } const subdomain = String(req.query.subdomain || '') .trim() .toLowerCase() if (!subdomain) { return res.status(400).json({ allowed: false, error: 'subdomain required' }) } try { const queries = [Query.equal('subdomain', subdomain), Query.limit(1)] if (!admin) { queries.splice(1, 0, Query.equal('customerId', session.customerId)) } const projects = await listDocuments(config.collections.websiteProjects, queries) const project = projects[0] if (!project) { return res.json({ allowed: false, reason: 'no_project' }) } if (!isPreviewProjectReady(project)) { return res.json({ allowed: false, reason: 'not_ready', projectId: project.$id }) } return res.json({ allowed: true, projectId: project.$id }) } catch (err) { return res.status(500).json({ allowed: false, error: err.message || 'Fehler' }) } }) router.get('/staff-preview', async (req, res) => { const tokenValue = String(req.query.token || '').trim() if (!tokenValue) { return res.status(400).send('Token fehlt') } const tokenData = consumeStaffPreviewToken(tokenValue) if (!tokenData) { return res.status(410).send('Der Preview-Link ist abgelaufen. Bitte erneut aus dem Ticketsystem oeffnen.') } const previewUrl = safePreviewRedirectUrl(tokenData.previewUrl) if (!previewUrl) { return res.status(400).send('Ungueltige Preview-URL') } const previewToken = await createPreviewSessionToken({ userId: tokenData.userId, customerId: tokenData.customerId || '', email: tokenData.email || '', name: tokenData.name || 'Admin', role: 'admin', labels: ['admin'], authSource: 'ticket-admin', }) res.cookie(PREVIEW_COOKIE_NAME, previewToken, previewSessionCookieOptions()) return res.redirect(previewUrl) }) router.get('/me', async (req, res) => { const raw = req.signedCookies?.[config.cookieName] if (!raw) { return res.json({ authenticated: false }) } try { const session = JSON.parse(raw) if (!session.customerId || !session.appwriteUserId) { return res.json({ authenticated: false }) } let customer = await resolveSessionCustomer(session) if (!customer) { clearPortalSession(res) clearPreviewSession(res) return res.json({ authenticated: false }) } try { await validatePortalAccess(session.appwriteUserId, session.email || customer.email) } catch { clearPortalSession(res) clearPreviewSession(res) return res.json({ authenticated: false }) } if (customer.appwriteUserId !== session.appwriteUserId) { customer = await syncCustomerAppwriteUserId(customer, session.appwriteUserId) } return res.json({ authenticated: true, customer: sanitizeCustomer(customer), }) } catch (err) { return res.status(500).json({ error: err.message || 'Fehler beim Laden' }) } }) export default router