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:
106
server/middleware/errorHandler.mjs
Normal file
106
server/middleware/errorHandler.mjs
Normal 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)
|
||||
}
|
||||
}
|
||||
134
server/middleware/logger.mjs
Normal file
134
server/middleware/logger.mjs
Normal 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 : '')
|
||||
},
|
||||
}
|
||||
96
server/middleware/rateLimit.mjs
Normal file
96
server/middleware/rateLimit.mjs
Normal 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.',
|
||||
}),
|
||||
}
|
||||
131
server/middleware/validate.mjs
Normal file
131
server/middleware/validate.mjs
Normal 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')],
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user