404 lines
11 KiB
JavaScript
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
|