MAJOR FEATURES: - AI Control Tab in Settings hinzugefügt mit vollständiger KI-Steuerung - Category Control: Benutzer können Kategorien aktivieren/deaktivieren und Aktionen pro Kategorie festlegen (Keep in Inbox, Archive & Mark Read, Star) - Company Labels: Automatische Erkennung bekannter Firmen (Amazon, Google, Microsoft, etc.) und optionale benutzerdefinierte Company Labels - Auto-Detect Companies Toggle: Automatische Label-Erstellung für bekannte Firmen UI/UX VERBESSERUNGEN: - Sorting Rules Tab entfernt (war zu verwirrend) - Save Buttons nach oben rechts verschoben (Category Control und Company Labels) - Company Labels Section: Custom Labels sind jetzt in einem ausklappbaren Details-Element (Optional) - Verbesserte Beschreibungen und Klarheit in der UI BACKEND ÄNDERUNGEN: - Neue API Endpoints: /api/preferences/ai-control (GET/POST) und /api/preferences/company-labels (GET/POST/DELETE) - AI Sorter Service erweitert: detectCompany(), matchesCompanyLabel(), getCategoryAction(), getEnabledCategories() - Database Service: Default-Werte und Merge-Logik für erweiterte User Preferences - Email Routes: Integration der neuen AI Control Einstellungen in Gmail und Outlook Sortierung - Label-Erstellung: Nur für enabledCategories, Custom Company Labels mit orange Farbe (#ff9800) FRONTEND ÄNDERUNGEN: - Neue TypeScript Types: client/src/types/settings.ts (AIControlSettings, CompanyLabel, CategoryInfo, KnownCompany) - Settings.tsx: Komplett überarbeitet mit AI Control Tab, Category Toggles, Company Labels Management - API Client erweitert: getAIControlSettings(), saveAIControlSettings(), getCompanyLabels(), saveCompanyLabel(), deleteCompanyLabel() - Debug-Logs hinzugefügt für Troubleshooting (main.tsx, App.tsx, Settings.tsx) BUGFIXES: - JSX Syntax-Fehler behoben: Fehlende schließende </div> Tags in Company Labels Section - TypeScript Typ-Fehler behoben: saved.data null-check für Company Labels - Struktur-Fehler behoben: Conditional Blocks korrekt verschachtelt TECHNISCHE DETAILS: - 9 Kategorien verfügbar: VIP, Clients, Invoices, Newsletter, Promotions, Social, Security, Calendar, Review - Company Labels unterstützen Bedingungen wie 'from:amazon.com OR from:amazon.de' - Priorisierung: 1) Custom Company Labels, 2) Auto-Detected Companies, 3) AI Categorization - Deaktivierte Kategorien werden automatisch als 'review' kategorisiert
268 lines
7.9 KiB
JavaScript
268 lines
7.9 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,
|
|
})
|
|
}))
|
|
|
|
/**
|
|
* POST /api/preferences/ai-control
|
|
* Save AI Control settings
|
|
*/
|
|
app.post('/api/preferences/ai-control', asyncHandler(async (req, res) => {
|
|
const { userId, enabledCategories, categoryActions, autoDetectCompanies } = 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
|
|
|
|
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
|