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

270 lines
8.0 KiB
JavaScript

/**
* EmailSorter Backend Server
* Main entry point
*/
import 'dotenv/config'
import express from 'express'
import cors from 'cors'
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'
// Config & Middleware
import { config, validateConfig } from './config/index.mjs'
import { errorHandler, asyncHandler, NotFoundError, ValidationError } from './middleware/errorHandler.mjs'
import { respond } from './utils/response.mjs'
import { logger, log } from './middleware/logger.mjs'
import { limiters } from './middleware/rateLimit.mjs'
// Routes
import oauthRoutes from './routes/oauth.mjs'
import emailRoutes from './routes/email.mjs'
import stripeRoutes from './routes/stripe.mjs'
import apiRoutes from './routes/api.mjs'
import analyticsRoutes from './routes/analytics.mjs'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
// Validate configuration
validateConfig()
// Create Express app
const app = express()
// Trust proxy (for rate limiting behind reverse proxy)
app.set('trust proxy', 1)
// Request ID middleware
app.use((req, res, next) => {
req.id = Math.random().toString(36).substring(2, 15)
res.setHeader('X-Request-ID', req.id)
next()
})
// CORS
app.use(cors(config.cors))
// Request logging
app.use(logger({
skip: (req) => req.path === '/api/health' || req.path.startsWith('/assets'),
}))
// Rate limiting
app.use('/api', limiters.api)
// Static files
app.use(express.static(join(__dirname, '..', 'public')))
// Body parsing (BEFORE routes, AFTER static)
// Note: Stripe webhook needs raw body, handled in stripe routes
app.use('/api', express.json({ limit: '1mb' }))
app.use('/api', express.urlencoded({ extended: true }))
// Health check (no rate limit)
app.get('/api/health', (req, res) => {
res.json({
success: true,
data: {
status: 'healthy',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || '1.0.0',
environment: config.nodeEnv,
uptime: Math.floor(process.uptime()),
},
})
})
// API Routes
app.use('/api/oauth', oauthRoutes)
app.use('/api/email', emailRoutes)
app.use('/api/subscription', stripeRoutes)
app.use('/api/analytics', analyticsRoutes)
app.use('/api', apiRoutes)
// Preferences endpoints (inline for simplicity)
import { userPreferences } from './services/database.mjs'
app.get('/api/preferences', asyncHandler(async (req, res) => {
const { userId } = req.query
if (!userId) throw new ValidationError('userId ist erforderlich')
const prefs = await userPreferences.getByUser(userId)
respond.success(res, prefs?.preferences || {
vipSenders: [],
blockedSenders: [],
customRules: [],
priorityTopics: [],
})
}))
app.post('/api/preferences', asyncHandler(async (req, res) => {
const { userId, ...preferences } = req.body
if (!userId) throw new ValidationError('userId ist erforderlich')
await userPreferences.upsert(userId, preferences)
respond.success(res, null, 'Einstellungen gespeichert')
}))
/**
* GET /api/preferences/ai-control
* Get AI Control settings
*/
app.get('/api/preferences/ai-control', asyncHandler(async (req, res) => {
const { userId } = req.query
if (!userId) throw new ValidationError('userId is required')
const prefs = await userPreferences.getByUser(userId)
const preferences = prefs?.preferences || userPreferences.getDefaults()
respond.success(res, {
enabledCategories: preferences.enabledCategories || [],
categoryActions: preferences.categoryActions || {},
autoDetectCompanies: preferences.autoDetectCompanies !== undefined ? preferences.autoDetectCompanies : true,
cleanup: preferences.cleanup || userPreferences.getDefaults().cleanup,
})
}))
/**
* POST /api/preferences/ai-control
* Save AI Control settings
*/
app.post('/api/preferences/ai-control', asyncHandler(async (req, res) => {
const { userId, enabledCategories, categoryActions, autoDetectCompanies, cleanup } = req.body
if (!userId) throw new ValidationError('userId is required')
const updates = {}
if (enabledCategories !== undefined) updates.enabledCategories = enabledCategories
if (categoryActions !== undefined) updates.categoryActions = categoryActions
if (autoDetectCompanies !== undefined) updates.autoDetectCompanies = autoDetectCompanies
if (cleanup !== undefined) updates.cleanup = cleanup
await userPreferences.upsert(userId, updates)
respond.success(res, null, 'AI Control settings saved')
}))
/**
* GET /api/preferences/company-labels
* Get company labels
*/
app.get('/api/preferences/company-labels', asyncHandler(async (req, res) => {
const { userId } = req.query
if (!userId) throw new ValidationError('userId is required')
const prefs = await userPreferences.getByUser(userId)
const preferences = prefs?.preferences || userPreferences.getDefaults()
respond.success(res, preferences.companyLabels || [])
}))
/**
* POST /api/preferences/company-labels
* Save/Update company label
*/
app.post('/api/preferences/company-labels', asyncHandler(async (req, res) => {
const { userId, companyLabel } = req.body
if (!userId) throw new ValidationError('userId is required')
if (!companyLabel) throw new ValidationError('companyLabel is required')
const prefs = await userPreferences.getByUser(userId)
const preferences = prefs?.preferences || userPreferences.getDefaults()
const companyLabels = preferences.companyLabels || []
// Generate ID if not provided
if (!companyLabel.id) {
companyLabel.id = `label_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
// Update or add label
const existingIndex = companyLabels.findIndex(l => l.id === companyLabel.id)
if (existingIndex >= 0) {
companyLabels[existingIndex] = companyLabel
} else {
companyLabels.push(companyLabel)
}
await userPreferences.upsert(userId, { companyLabels })
respond.success(res, companyLabel, 'Company label saved')
}))
/**
* DELETE /api/preferences/company-labels/:id
* Delete company label
*/
app.delete('/api/preferences/company-labels/:id', asyncHandler(async (req, res) => {
const { userId } = req.query
const { id } = req.params
if (!userId) throw new ValidationError('userId is required')
if (!id) throw new ValidationError('label id is required')
const prefs = await userPreferences.getByUser(userId)
const preferences = prefs?.preferences || userPreferences.getDefaults()
const companyLabels = (preferences.companyLabels || []).filter(l => l.id !== id)
await userPreferences.upsert(userId, { companyLabels })
respond.success(res, null, 'Company label deleted')
}))
// Legacy Stripe webhook endpoint
app.use('/stripe', stripeRoutes)
// 404 handler for API routes
app.use('/api/*', (req, res, next) => {
next(new NotFoundError('Endpoint'))
})
// SPA fallback for non-API routes
app.get('*', (req, res) => {
res.sendFile(join(__dirname, '..', 'public', 'index.html'))
})
// Global error handler (must be last)
app.use(errorHandler)
// Graceful shutdown
let server
function gracefulShutdown(signal) {
log.info(`${signal} empfangen, Server wird heruntergefahren...`)
server.close(() => {
log.info('HTTP Server geschlossen')
process.exit(0)
})
// Force close after 10 seconds
setTimeout(() => {
log.error('Erzwungenes Herunterfahren')
process.exit(1)
}, 10000)
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
process.on('SIGINT', () => gracefulShutdown('SIGINT'))
// Handle uncaught errors
process.on('uncaughtException', (err) => {
log.error('Uncaught Exception:', { error: err.message, stack: err.stack })
process.exit(1)
})
process.on('unhandledRejection', (reason, promise) => {
log.error('Unhandled Rejection:', { reason, promise })
})
// Start server
server = app.listen(config.port, () => {
console.log('')
log.success(`Server gestartet auf Port ${config.port}`)
log.info(`Frontend URL: ${config.frontendUrl}`)
log.info(`Environment: ${config.nodeEnv}`)
console.log('')
console.log(` 🌐 API: http://localhost:${config.port}/api`)
console.log(` 💚 Health: http://localhost:${config.port}/api/health`)
console.log('')
})
export default app