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:
58
server/middleware/auth.mjs
Normal file
58
server/middleware/auth.mjs
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user