Files
Emailsorter/server/routes/api.mjs
ANDJ 89bc86b615 Try
dfssdfsfdsf
2026-04-09 21:00:04 +02:00

522 lines
14 KiB
JavaScript

/**
* Main API Routes
* General API endpoints
*/
import express from 'express'
import { Client, Users, Query as AppwriteQuery } from 'node-appwrite'
import { asyncHandler, NotFoundError, ValidationError, AuthorizationError } 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,
deleteAllDocumentsForUser,
} from '../services/database.mjs'
import Stripe from 'stripe'
import { config, isAdmin } from '../config/index.mjs'
import { log } from '../middleware/logger.mjs'
import { parseImapAccountAccess } from './email.mjs'
import { ImapService } from '../services/imap.mjs'
import { isAppwriteCollectionMissing } from '../utils/appwriteErrors.mjs'
import { requireAuth } from '../middleware/auth.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',
requireAuth,
asyncHandler(async (req, res) => {
const userId = req.appwriteUser.id
const state = await onboardingState.getByUser(userId)
respond.success(res, state)
})
)
/**
* POST /api/onboarding/step
* Update onboarding step progress
*/
router.post('/onboarding/step',
requireAuth,
validate({
body: {
step: [rules.required('step')],
completedSteps: [rules.isArray('completedSteps')],
},
}),
asyncHandler(async (req, res) => {
const userId = req.appwriteUser.id
const { 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',
requireAuth,
asyncHandler(async (req, res) => {
const userId = req.appwriteUser.id
await onboardingState.skip(userId)
respond.success(res, { skipped: true })
})
)
/**
* POST /api/onboarding/resume
* Resume onboarding
*/
router.post('/onboarding/resume',
requireAuth,
asyncHandler(async (req, res) => {
const userId = req.appwriteUser.id
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',
requireAuth,
asyncHandler(async (req, res) => {
const userId = req.appwriteUser.id
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, req.appwriteUser?.email)
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
* (Also registered on app in index.mjs before router mount.)
*/
export async function handleGetReferralCode(req, res) {
const userId = req.appwriteUser.id
try {
const result = await referrals.getOrCreateCode(userId)
if (!result) {
return respond.success(res, { referralCode: null, referralCount: 0 })
}
return respond.success(res, {
referralCode: result.referralCode,
referralCount: result.referralCount || 0,
})
} catch (err) {
if (isAppwriteCollectionMissing(err)) {
return respond.success(res, {
referralCode: null,
referralCount: 0,
})
}
throw err
}
}
router.get('/referrals/code', requireAuth, asyncHandler(handleGetReferralCode))
/**
* POST /api/referrals/track
* Track a referral (when new user signs up with referral code)
*/
router.post('/referrals/track',
requireAuth,
validate({
body: {
referralCode: [rules.required('referralCode')],
},
}),
asyncHandler(async (req, res) => {
const userId = req.appwriteUser.id
const { 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 })
})
)
async function resolveUserIdByEmail(email) {
const normalized = String(email).trim().toLowerCase()
const c = new Client()
.setEndpoint(config.appwrite.endpoint)
.setProject(config.appwrite.projectId)
.setKey(config.appwrite.apiKey)
const users = new Users(c)
try {
const res = await users.list([AppwriteQuery.equal('email', normalized)])
const uid = res.users?.[0]?.$id
if (uid) return uid
} catch (e) {
log.warn('resolveUserIdByEmail: Users.list failed', { email: normalized, error: e.message })
}
const acc = await db.findOne(Collections.EMAIL_ACCOUNTS, [Query.equal('email', normalized)])
return acc?.userId || null
}
/**
* POST /api/admin/reset-user-sort-data
* Admin only: clear sort-related data for a user (by email).
*/
router.post(
'/admin/reset-user-sort-data',
requireAuth,
validate({
body: {
email: [rules.required('email'), rules.email()],
},
}),
asyncHandler(async (req, res) => {
if (!isAdmin(req.appwriteUser?.email)) {
throw new AuthorizationError('Admin access required')
}
const targetEmail = String(req.body.email).trim().toLowerCase()
const targetUserId = await resolveUserIdByEmail(targetEmail)
if (!targetUserId) {
throw new NotFoundError('User for this email')
}
const statsDoc = await emailStats.getByUser(targetUserId)
let statsDeleted = 0
if (statsDoc?.$id) {
await db.delete(Collections.EMAIL_STATS, statsDoc.$id)
statsDeleted = 1
}
const digestsDeleted = await deleteAllDocumentsForUser(Collections.EMAIL_DIGESTS, targetUserId)
const usageDeleted = await deleteAllDocumentsForUser(Collections.EMAIL_USAGE, targetUserId)
await onboardingState.resetToInitial(targetUserId)
let imapCleared = 0
const imapAccounts = await db.list(Collections.EMAIL_ACCOUNTS, [
Query.equal('userId', targetUserId),
Query.equal('provider', 'imap'),
])
for (const acc of imapAccounts) {
if (!acc.accessToken) continue
try {
const cfg = parseImapAccountAccess(acc)
if (!cfg.password) continue
const imap = new ImapService({
host: cfg.host,
port: cfg.port,
secure: cfg.secure,
user: acc.email,
password: cfg.password,
})
const n = await imap.removeAllSortedFlags()
imapCleared += n
} catch (e) {
log.warn('reset-user-sort-data: IMAP flags failed for account', {
accountId: acc.$id,
error: e.message,
})
}
}
log.info('Admin reset-user-sort-data', {
admin: req.appwriteUser.email,
targetEmail,
targetUserId,
statsDeleted,
digestsDeleted,
usageDeleted,
imapCleared,
})
respond.success(res, {
reset: true,
deleted: {
stats: statsDeleted,
digests: digestsDeleted,
usage: usageDeleted,
},
imapCleared,
})
})
)
export default router