Files
Emailsorter/server/routes/api.mjs
ANDJ 6da8ce1cbd huhuih
hzgjuigik
2026-01-27 21:06:48 +01:00

404 lines
11 KiB
JavaScript

/**
* 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