fix(dev): Vite-API-Proxy, Auth, Stripe-Mails und Backend-Erweiterungen

- Client: API-Basis-URL (joinApiUrl, /v1-Falle), Vite strictPort + Proxy 127.0.0.1, Nicht-JSON-Fehler

- Server: /api-404 ohne Wildcard-Bug, SPA-Fallback, Auth-Middleware, Cron, Mailer, Crypto

- Routen: OAuth-State, Email/Stripe/Analytics; client/.env.example

Made-with: Cursor
This commit is contained in:
2026-04-03 00:23:01 +02:00
parent 61008b63bb
commit ecae89a79d
33 changed files with 1663 additions and 550 deletions

View File

@@ -0,0 +1,58 @@
/**
* Appwrite JWT verification for user-scoped API routes.
*/
import { Client, Account } from 'node-appwrite'
import { config } from '../config/index.mjs'
import { AuthenticationError } from './errorHandler.mjs'
/**
* Verify Authorization: Bearer <jwt> and attach Appwrite user to req.appwriteUser
*/
export function requireAuth(req, res, next) {
;(async () => {
try {
const header = req.headers.authorization || ''
const m = /^Bearer\s+(.+)$/i.exec(header)
if (!m?.[1]) {
throw new AuthenticationError('Authorization Bearer token required')
}
const jwt = m[1].trim()
const client = new Client()
.setEndpoint(config.appwrite.endpoint)
.setProject(config.appwrite.projectId)
.setJWT(jwt)
const account = new Account(client)
const user = await account.get()
if (!user || !user.$id) {
throw new AuthenticationError('Ungültige Appwrite-Sitzung')
}
req.appwriteUser = {
id: user.$id,
email: user.email || '',
name: user.name || '',
}
next()
} catch (err) {
if (err instanceof AuthenticationError) {
next(err)
return
}
next(new AuthenticationError(err.message || 'Invalid or expired session'))
}
})()
}
/**
* Skip auth for email provider inbound webhooks only.
*/
export function requireAuthUnlessEmailWebhook(req, res, next) {
const p = req.path || ''
if (p === '/webhook/gmail' || p === '/webhook/outlook') {
return next()
}
return requireAuth(req, res, next)
}

View File

@@ -3,6 +3,8 @@
* Catches all errors and returns consistent JSON responses
*/
import { AppwriteException } from 'node-appwrite'
export class AppError extends Error {
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
super(message)
@@ -56,11 +58,28 @@ export function errorHandler(err, req, res, next) {
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
})
// Default error values
let statusCode = err.statusCode || 500
let code = err.code || 'INTERNAL_ERROR'
// Default error values (AppwriteException uses numeric err.code — do not reuse as JSON "code" string)
let statusCode =
typeof err.statusCode === 'number' ? err.statusCode : undefined
let code = typeof err.code === 'string' && err.code ? err.code : 'INTERNAL_ERROR'
let message = err.message || 'Ein Fehler ist aufgetreten'
if (
err instanceof AppwriteException &&
typeof err.code === 'number' &&
err.code >= 400 &&
err.code < 600
) {
statusCode = err.code
code = err.type || 'APPWRITE_ERROR'
message = err.message || message
err.isOperational = true
}
if (statusCode === undefined) {
statusCode = 500
}
// Handle specific error types
if (err.name === 'ValidationError') {
statusCode = 400

View File

@@ -4,6 +4,7 @@
*/
import { RateLimitError } from './errorHandler.mjs'
import { isAdmin } from '../config/index.mjs'
// In-memory store for rate limiting (use Redis in production)
const requestCounts = new Map()
@@ -25,6 +26,7 @@ setInterval(() => {
* @param {number} options.max - Max requests per window
* @param {string} options.message - Error message
* @param {Function} options.keyGenerator - Function to generate unique key
* @param {Function} options.skip - If (req) => true, do not count this request
*/
export function rateLimit(options = {}) {
const {
@@ -32,9 +34,14 @@ export function rateLimit(options = {}) {
max = 100,
message = 'Zu viele Anfragen. Bitte versuche es später erneut.',
keyGenerator = (req) => req.ip,
skip = () => false,
} = options
return (req, res, next) => {
if (skip(req)) {
return next()
}
const key = keyGenerator(req)
const now = Date.now()
@@ -80,11 +87,12 @@ export const limiters = {
message: 'Zu viele Anmeldeversuche. Bitte warte 15 Minuten.',
}),
// Limit for email sorting (expensive operation)
// Limit for email sorting (expensive operation); ADMIN_EMAILS (isAdmin) bypass
emailSort: rateLimit({
windowMs: 60000,
max: 30, // Erhöht für Entwicklung
message: 'E-Mail-Sortierung ist limitiert. Bitte warte eine Minute.',
skip: (req) => isAdmin(req.appwriteUser?.email),
}),
// Limit for AI operations