Email Sorter Beta

Ich habe soweit automatisiert the Emails sortieren aber ich muss noch schauen was es fur bugs es gibt wenn die app online  ist deswegen wurde ich mit diesen Commit die website veroffentlichen obwohjl es sein konnte  das es noch nicht fertig ist und verkaufs bereit
This commit is contained in:
2026-01-22 19:32:12 +01:00
parent 95349af50b
commit abf761db07
596 changed files with 56405 additions and 51231 deletions

View File

@@ -0,0 +1,106 @@
/**
* Global Error Handler Middleware
* Catches all errors and returns consistent JSON responses
*/
export class AppError extends Error {
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
super(message)
this.statusCode = statusCode
this.code = code
this.isOperational = true
Error.captureStackTrace(this, this.constructor)
}
}
export class ValidationError extends AppError {
constructor(message, fields = {}) {
super(message, 400, 'VALIDATION_ERROR')
this.fields = fields
}
}
export class AuthenticationError extends AppError {
constructor(message = 'Nicht authentifiziert') {
super(message, 401, 'AUTHENTICATION_ERROR')
}
}
export class AuthorizationError extends AppError {
constructor(message = 'Keine Berechtigung') {
super(message, 403, 'AUTHORIZATION_ERROR')
}
}
export class NotFoundError extends AppError {
constructor(resource = 'Ressource') {
super(`${resource} nicht gefunden`, 404, 'NOT_FOUND')
}
}
export class RateLimitError extends AppError {
constructor(message = 'Zu viele Anfragen') {
super(message, 429, 'RATE_LIMIT_EXCEEDED')
}
}
/**
* Error handler middleware
*/
export function errorHandler(err, req, res, next) {
// Log error
console.error(`[ERROR] ${new Date().toISOString()}`, {
method: req.method,
path: req.path,
error: err.message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
})
// Default error values
let statusCode = err.statusCode || 500
let code = err.code || 'INTERNAL_ERROR'
let message = err.message || 'Ein Fehler ist aufgetreten'
// Handle specific error types
if (err.name === 'ValidationError') {
statusCode = 400
code = 'VALIDATION_ERROR'
}
if (err.name === 'JsonWebTokenError') {
statusCode = 401
code = 'INVALID_TOKEN'
message = 'Ungültiger Token'
}
if (err.name === 'TokenExpiredError') {
statusCode = 401
code = 'TOKEN_EXPIRED'
message = 'Token abgelaufen'
}
// Don't expose internal errors in production
if (!err.isOperational && process.env.NODE_ENV === 'production') {
message = 'Ein interner Fehler ist aufgetreten'
}
// Send response
res.status(statusCode).json({
success: false,
error: {
code,
message,
...(err.fields && { fields: err.fields }),
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
},
})
}
/**
* Async handler wrapper to catch errors in async routes
*/
export function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next)
}
}

View File

@@ -0,0 +1,134 @@
/**
* Request Logger Middleware
* Logs all incoming requests with timing information
*/
// ANSI color codes for terminal output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
}
/**
* Get color based on status code
*/
function getStatusColor(status) {
if (status >= 500) return colors.red
if (status >= 400) return colors.yellow
if (status >= 300) return colors.cyan
if (status >= 200) return colors.green
return colors.reset
}
/**
* Get color based on HTTP method
*/
function getMethodColor(method) {
const methodColors = {
GET: colors.green,
POST: colors.blue,
PUT: colors.yellow,
PATCH: colors.yellow,
DELETE: colors.red,
}
return methodColors[method] || colors.reset
}
/**
* Format duration for display
*/
function formatDuration(ms) {
if (ms < 1) return `${(ms * 1000).toFixed(0)}µs`
if (ms < 1000) return `${ms.toFixed(0)}ms`
return `${(ms / 1000).toFixed(2)}s`
}
/**
* Logger middleware
*/
export function logger(options = {}) {
const {
skip = () => false,
format = 'dev',
} = options
return (req, res, next) => {
if (skip(req, res)) {
return next()
}
const startTime = process.hrtime.bigint()
const timestamp = new Date().toISOString()
// Capture response
const originalSend = res.send
res.send = function (body) {
const endTime = process.hrtime.bigint()
const duration = Number(endTime - startTime) / 1e6 // Convert to ms
const statusColor = getStatusColor(res.statusCode)
const methodColor = getMethodColor(req.method)
// Log format
const logLine = [
`${colors.dim}[${timestamp}]${colors.reset}`,
`${methodColor}${req.method.padEnd(7)}${colors.reset}`,
`${req.originalUrl}`,
`${statusColor}${res.statusCode}${colors.reset}`,
`${colors.dim}${formatDuration(duration)}${colors.reset}`,
].join(' ')
console.log(logLine)
// Log errors in detail
if (res.statusCode >= 400 && body) {
try {
const parsed = typeof body === 'string' ? JSON.parse(body) : body
if (parsed.error) {
console.log(` ${colors.red}${parsed.error.message}${colors.reset}`)
}
} catch (e) {
// Body is not JSON
}
}
return originalSend.call(this, body)
}
next()
}
}
/**
* Log levels
*/
export const log = {
info: (message, data = {}) => {
console.log(`${colors.blue}[INFO]${colors.reset} ${message}`, Object.keys(data).length ? data : '')
},
warn: (message, data = {}) => {
console.log(`${colors.yellow}[WARN]${colors.reset} ${message}`, Object.keys(data).length ? data : '')
},
error: (message, data = {}) => {
console.error(`${colors.red}[ERROR]${colors.reset} ${message}`, Object.keys(data).length ? data : '')
},
debug: (message, data = {}) => {
if (process.env.NODE_ENV === 'development') {
console.log(`${colors.magenta}[DEBUG]${colors.reset} ${message}`, Object.keys(data).length ? data : '')
}
},
success: (message, data = {}) => {
console.log(`${colors.green}[OK]${colors.reset} ${message}`, Object.keys(data).length ? data : '')
},
}

View File

@@ -0,0 +1,96 @@
/**
* Rate Limiting Middleware
* Prevents abuse by limiting requests per IP/user
*/
import { RateLimitError } from './errorHandler.mjs'
// In-memory store for rate limiting (use Redis in production)
const requestCounts = new Map()
// Clean up old entries every minute
setInterval(() => {
const now = Date.now()
for (const [key, data] of requestCounts.entries()) {
if (now - data.windowStart > data.windowMs) {
requestCounts.delete(key)
}
}
}, 60000)
/**
* Create rate limiter middleware
* @param {Object} options - Rate limit options
* @param {number} options.windowMs - Time window in milliseconds
* @param {number} options.max - Max requests per window
* @param {string} options.message - Error message
* @param {Function} options.keyGenerator - Function to generate unique key
*/
export function rateLimit(options = {}) {
const {
windowMs = 60000, // 1 minute
max = 100,
message = 'Zu viele Anfragen. Bitte versuche es später erneut.',
keyGenerator = (req) => req.ip,
} = options
return (req, res, next) => {
const key = keyGenerator(req)
const now = Date.now()
let data = requestCounts.get(key)
if (!data || now - data.windowStart > windowMs) {
data = { count: 0, windowStart: now, windowMs }
requestCounts.set(key, data)
}
data.count++
// Set rate limit headers
res.set({
'X-RateLimit-Limit': max,
'X-RateLimit-Remaining': Math.max(0, max - data.count),
'X-RateLimit-Reset': new Date(data.windowStart + windowMs).toISOString(),
})
if (data.count > max) {
return next(new RateLimitError(message))
}
next()
}
}
/**
* Pre-configured rate limiters
*/
export const limiters = {
// General API rate limit
api: rateLimit({
windowMs: 60000,
max: 100,
message: 'API Rate Limit überschritten',
}),
// Stricter limit for auth endpoints
auth: rateLimit({
windowMs: 900000, // 15 minutes
max: 10,
message: 'Zu viele Anmeldeversuche. Bitte warte 15 Minuten.',
}),
// Limit for email sorting (expensive operation)
emailSort: rateLimit({
windowMs: 60000,
max: 30, // Erhöht für Entwicklung
message: 'E-Mail-Sortierung ist limitiert. Bitte warte eine Minute.',
}),
// Limit for AI operations
ai: rateLimit({
windowMs: 60000,
max: 20,
message: 'KI-Anfragen sind limitiert.',
}),
}

View File

@@ -0,0 +1,131 @@
/**
* Request Validation Middleware
* Validates request body, query params, and route params
*/
import { ValidationError } from './errorHandler.mjs'
/**
* Validation rules
*/
export const rules = {
required: (field) => ({
validate: (value) => value !== undefined && value !== null && value !== '',
message: `${field} ist erforderlich`,
}),
email: () => ({
validate: (value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
message: 'Ungültige E-Mail-Adresse',
}),
minLength: (field, min) => ({
validate: (value) => !value || value.length >= min,
message: `${field} muss mindestens ${min} Zeichen lang sein`,
}),
maxLength: (field, max) => ({
validate: (value) => !value || value.length <= max,
message: `${field} darf maximal ${max} Zeichen lang sein`,
}),
isIn: (field, values) => ({
validate: (value) => !value || values.includes(value),
message: `${field} muss einer der folgenden Werte sein: ${values.join(', ')}`,
}),
isNumber: (field) => ({
validate: (value) => !value || !isNaN(Number(value)),
message: `${field} muss eine Zahl sein`,
}),
isPositive: (field) => ({
validate: (value) => !value || Number(value) > 0,
message: `${field} muss positiv sein`,
}),
isArray: (field) => ({
validate: (value) => !value || Array.isArray(value),
message: `${field} muss ein Array sein`,
}),
isObject: (field) => ({
validate: (value) => !value || (typeof value === 'object' && !Array.isArray(value)),
message: `${field} muss ein Objekt sein`,
}),
}
/**
* Validate request against schema
* @param {Object} schema - Validation schema { body: {}, query: {}, params: {} }
*/
export function validate(schema) {
return (req, res, next) => {
const errors = {}
// Validate each part of the request
for (const [location, fields] of Object.entries(schema)) {
const data = req[location] || {}
for (const [field, fieldRules] of Object.entries(fields)) {
const value = data[field]
const fieldErrors = []
for (const rule of fieldRules) {
if (!rule.validate(value)) {
fieldErrors.push(rule.message)
}
}
if (fieldErrors.length > 0) {
errors[field] = fieldErrors
}
}
}
if (Object.keys(errors).length > 0) {
return next(new ValidationError('Validierungsfehler', errors))
}
next()
}
}
/**
* Common validation schemas
*/
export const schemas = {
// User registration
register: {
body: {
email: [rules.required('E-Mail'), rules.email()],
password: [rules.required('Passwort'), rules.minLength('Passwort', 8)],
},
},
// Email connection
connectEmail: {
body: {
userId: [rules.required('User ID')],
provider: [rules.required('Provider'), rules.isIn('Provider', ['gmail', 'outlook'])],
email: [rules.required('E-Mail'), rules.email()],
},
},
// Checkout
checkout: {
body: {
userId: [rules.required('User ID')],
plan: [rules.required('Plan'), rules.isIn('Plan', ['basic', 'pro', 'business'])],
},
},
// Email sorting
sortEmails: {
body: {
userId: [rules.required('User ID')],
accountId: [rules.required('Account ID')],
maxEmails: [rules.isNumber('maxEmails'), rules.isPositive('maxEmails')],
},
},
}