/** * Main API Routes * General API endpoints */ import express from 'express' import { asyncHandler, NotFoundError, ValidationError } from '../middleware/errorHandler.mjs' import { validate, schemas, rules } from '../middleware/validate.mjs' import { respond } from '../utils/response.mjs' import { products, questions, submissions, orders, onboardingState, emailAccounts, emailStats, emailDigests, userPreferences, subscriptions, emailUsage, referrals, db, Collections, Query } from '../services/database.mjs' import Stripe from 'stripe' import { config } from '../config/index.mjs' import { log } from '../middleware/logger.mjs' const router = express.Router() const stripe = new Stripe(config.stripe.secretKey) /** * GET /api/products * Get all active products */ router.get('/products', asyncHandler(async (req, res) => { const productList = await products.getActive() respond.success(res, productList) })) /** * GET /api/questions * Get questions for a product */ router.get('/questions', asyncHandler(async (req, res) => { const { productSlug } = req.query if (!productSlug) { throw new ValidationError('productSlug ist erforderlich', { productSlug: ['Pflichtfeld'] }) } const product = await products.getBySlug(productSlug) if (!product) { throw new NotFoundError('Produkt') } const questionList = await questions.getByProduct(product.$id) respond.success(res, questionList) })) /** * POST /api/submissions * Create a new submission */ router.post('/submissions', validate({ body: { productSlug: [rules.required('productSlug')], answers: [rules.required('answers'), rules.isObject('answers')], }, }), asyncHandler(async (req, res) => { const { productSlug, answers } = req.body const product = await products.getBySlug(productSlug) if (!product) { throw new NotFoundError('Produkt') } // Create submission const submission = await submissions.create({ productId: product.$id, status: 'draft', customerEmail: answers.email || answers.customer_email || null, customerName: answers.name || answers.customer_name || null, finalSummaryJson: JSON.stringify(answers), priceCents: product.priceCents, currency: product.currency, }) // Store answers separately await orders.create(submission.$id, { answers }) respond.created(res, { submissionId: submission.$id }) }) ) /** * POST /api/checkout * Create Stripe checkout session for one-time payment */ router.post('/checkout', validate({ body: { submissionId: [rules.required('submissionId')], }, }), asyncHandler(async (req, res) => { const { submissionId } = req.body // Get submission const submission = await submissions.create let submissionDoc try { const { db, Collections } = await import('../services/database.mjs') submissionDoc = await db.get(Collections.SUBMISSIONS, submissionId) } catch (error) { throw new NotFoundError('Submission') } // Create Stripe checkout session const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: [ { price_data: { currency: submissionDoc.currency || 'eur', product_data: { name: 'Email Sortierer Service', description: 'Personalisiertes E-Mail-Sortier-Setup', }, unit_amount: submissionDoc.priceCents || 4900, }, quantity: 1, }, ], mode: 'payment', success_url: `${config.frontendUrl}/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${config.frontendUrl}/cancel`, metadata: { submissionId, }, customer_email: submissionDoc.customerEmail || undefined, }) respond.success(res, { url: session.url, sessionId: session.id }) }) ) /** * GET /api/submission/:id * Get submission details */ router.get('/submission/:id', asyncHandler(async (req, res) => { const { id } = req.params const { db, Collections } = await import('../services/database.mjs') const submission = await db.get(Collections.SUBMISSIONS, id) // Don't expose sensitive data respond.success(res, { id: submission.$id, status: submission.status, createdAt: submission.$createdAt, }) })) /** * GET /api/config * Get public configuration */ router.get('/config', (req, res) => { respond.success(res, { features: { gmail: Boolean(config.google.clientId), outlook: Boolean(config.microsoft.clientId), ai: Boolean(config.mistral.apiKey), }, pricing: { basic: { price: 9, currency: 'EUR', accounts: 1 }, pro: { price: 19, currency: 'EUR', accounts: 3 }, business: { price: 49, currency: 'EUR', accounts: 10 }, }, }) }) /** * GET /api/onboarding/status * Get current onboarding state */ router.get('/onboarding/status', validate({ query: { userId: [rules.required('userId')], }, }), asyncHandler(async (req, res) => { const { userId } = req.query const state = await onboardingState.getByUser(userId) respond.success(res, state) }) ) /** * POST /api/onboarding/step * Update onboarding step progress */ router.post('/onboarding/step', validate({ body: { userId: [rules.required('userId')], step: [rules.required('step')], completedSteps: [rules.isArray('completedSteps')], }, }), asyncHandler(async (req, res) => { const { userId, step, completedSteps = [] } = req.body await onboardingState.updateStep(userId, step, completedSteps) respond.success(res, { step, completedSteps }) }) ) /** * POST /api/onboarding/skip * Skip onboarding */ router.post('/onboarding/skip', validate({ body: { userId: [rules.required('userId')], }, }), asyncHandler(async (req, res) => { const { userId } = req.body await onboardingState.skip(userId) respond.success(res, { skipped: true }) }) ) /** * POST /api/onboarding/resume * Resume onboarding */ router.post('/onboarding/resume', validate({ body: { userId: [rules.required('userId')], }, }), asyncHandler(async (req, res) => { const { userId } = req.body await onboardingState.resume(userId) const state = await onboardingState.getByUser(userId) respond.success(res, state) }) ) /** * DELETE /api/account/delete * Delete all user data and account */ router.delete('/account/delete', validate({ body: { userId: [rules.required('userId')], }, }), asyncHandler(async (req, res) => { const { userId } = req.body log.info(`Account deletion requested for user ${userId}`) // Delete all user data try { // Delete email accounts const accounts = await emailAccounts.getByUser(userId) for (const account of accounts) { try { await db.delete(Collections.EMAIL_ACCOUNTS, account.$id) } catch (err) { log.warn(`Failed to delete account ${account.$id}`, { error: err.message }) } } // Delete stats const stats = await emailStats.getByUser(userId) if (stats) { try { await db.delete(Collections.EMAIL_STATS, stats.$id) } catch (err) { log.warn(`Failed to delete stats`, { error: err.message }) } } // Delete digests const digests = await emailDigests.getByUser(userId) for (const digest of digests) { try { await db.delete(Collections.EMAIL_DIGESTS, digest.$id) } catch (err) { log.warn(`Failed to delete digest ${digest.$id}`, { error: err.message }) } } // Delete preferences const prefs = await userPreferences.getByUser(userId) if (prefs) { try { await db.delete(Collections.USER_PREFERENCES, prefs.$id) } catch (err) { log.warn(`Failed to delete preferences`, { error: err.message }) } } // Delete subscription const subscription = await subscriptions.getByUser(userId) if (subscription && subscription.$id) { try { await db.delete(Collections.SUBSCRIPTIONS, subscription.$id) } catch (err) { log.warn(`Failed to delete subscription`, { error: err.message }) } } // Delete email usage const usageRecords = await db.list(Collections.EMAIL_USAGE, [Query.equal('userId', userId)]) for (const usage of usageRecords) { try { await db.delete(Collections.EMAIL_USAGE, usage.$id) } catch (err) { log.warn(`Failed to delete usage record`, { error: err.message }) } } // Delete onboarding state const onboarding = await onboardingState.getByUser(userId) if (onboarding && onboarding.$id) { try { await db.delete(Collections.ONBOARDING_STATE, onboarding.$id) } catch (err) { log.warn(`Failed to delete onboarding state`, { error: err.message }) } } log.success(`Account deletion completed for user ${userId}`) respond.success(res, { success: true, message: 'All data deleted successfully' }) } catch (err) { log.error('Account deletion failed', { error: err.message, userId }) throw new ValidationError('Failed to delete account data') } }) ) /** * GET /api/referrals/code * Get or create referral code for user */ router.get('/referrals/code', validate({ query: { userId: [rules.required('userId')], }, }), asyncHandler(async (req, res) => { const { userId } = req.query const referral = await referrals.getOrCreateCode(userId) respond.success(res, { referralCode: referral.referralCode, referralCount: referral.referralCount || 0, }) }) ) /** * POST /api/referrals/track * Track a referral (when new user signs up with referral code) */ router.post('/referrals/track', validate({ body: { userId: [rules.required('userId')], referralCode: [rules.required('referralCode')], }, }), asyncHandler(async (req, res) => { const { userId, referralCode } = req.body // Find referrer by code const referrer = await referrals.getByCode(referralCode) if (!referrer) { throw new NotFoundError('Referral code') } // Don't allow self-referral if (referrer.userId === userId) { throw new ValidationError('Cannot refer yourself') } // Update referrer's count await referrals.incrementCount(referrer.userId) // Store referral relationship await referrals.getOrCreateCode(userId) const userReferral = await referrals.getOrCreateCode(userId) await db.update(Collections.REFERRALS, userReferral.$id, { referredBy: referrer.userId, }) log.info(`Referral tracked: ${userId} referred by ${referrer.userId} (code: ${referralCode})`) respond.success(res, { success: true }) }) ) export default router