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:
140
server/index.mjs
140
server/index.mjs
@@ -11,10 +11,11 @@ import { dirname, join } from 'path'
|
||||
|
||||
// Config & Middleware
|
||||
import { config, validateConfig } from './config/index.mjs'
|
||||
import { errorHandler, asyncHandler, NotFoundError, ValidationError, AuthorizationError } from './middleware/errorHandler.mjs'
|
||||
import { errorHandler, asyncHandler, AppError, NotFoundError, ValidationError, AuthorizationError } from './middleware/errorHandler.mjs'
|
||||
import { respond } from './utils/response.mjs'
|
||||
import { logger, log } from './middleware/logger.mjs'
|
||||
import { limiters } from './middleware/rateLimit.mjs'
|
||||
import { requireAuth } from './middleware/auth.mjs'
|
||||
|
||||
// Routes
|
||||
import oauthRoutes from './routes/oauth.mjs'
|
||||
@@ -23,6 +24,7 @@ import stripeRoutes from './routes/stripe.mjs'
|
||||
import apiRoutes from './routes/api.mjs'
|
||||
import analyticsRoutes from './routes/analytics.mjs'
|
||||
import webhookRoutes from './routes/webhook.mjs'
|
||||
import { startCounterJobs } from './jobs/reset-counters.mjs'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
@@ -93,21 +95,16 @@ import { userPreferences } from './services/database.mjs'
|
||||
import { isAdmin } from './config/index.mjs'
|
||||
|
||||
/**
|
||||
* GET /api/me?email=xxx
|
||||
* Returns current user context (e.g. isAdmin) for the given email.
|
||||
* GET /api/me
|
||||
* Returns current user context (JWT). isAdmin from verified email.
|
||||
*/
|
||||
app.get('/api/me', asyncHandler(async (req, res) => {
|
||||
const { email } = req.query
|
||||
if (!email || typeof email !== 'string') {
|
||||
throw new ValidationError('email is required')
|
||||
}
|
||||
respond.success(res, { isAdmin: isAdmin(email) })
|
||||
app.get('/api/me', requireAuth, asyncHandler(async (req, res) => {
|
||||
respond.success(res, { isAdmin: isAdmin(req.appwriteUser.email) })
|
||||
}))
|
||||
|
||||
app.get('/api/preferences', asyncHandler(async (req, res) => {
|
||||
const { userId } = req.query
|
||||
if (!userId) throw new ValidationError('userId ist erforderlich')
|
||||
|
||||
app.get('/api/preferences', requireAuth, asyncHandler(async (req, res) => {
|
||||
const userId = req.appwriteUser.id
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
respond.success(res, prefs?.preferences || {
|
||||
vipSenders: [],
|
||||
@@ -117,22 +114,40 @@ app.get('/api/preferences', asyncHandler(async (req, res) => {
|
||||
})
|
||||
}))
|
||||
|
||||
app.post('/api/preferences', asyncHandler(async (req, res) => {
|
||||
const { userId, ...preferences } = req.body
|
||||
if (!userId) throw new ValidationError('userId ist erforderlich')
|
||||
|
||||
app.post('/api/preferences', requireAuth, asyncHandler(async (req, res) => {
|
||||
const userId = req.appwriteUser.id
|
||||
const { ...preferences } = req.body
|
||||
|
||||
await userPreferences.upsert(userId, preferences)
|
||||
respond.success(res, null, 'Einstellungen gespeichert')
|
||||
}))
|
||||
|
||||
/**
|
||||
* PATCH /api/preferences/profile
|
||||
* { displayName?, timezone?, notificationPrefs? }
|
||||
*/
|
||||
app.patch('/api/preferences/profile', requireAuth, asyncHandler(async (req, res) => {
|
||||
const userId = req.appwriteUser.id
|
||||
const { displayName, timezone, notificationPrefs } = req.body
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
const current = prefs?.preferences?.profile || userPreferences.getDefaults().profile
|
||||
const profile = {
|
||||
...current,
|
||||
...(displayName !== undefined && { displayName }),
|
||||
...(timezone !== undefined && { timezone }),
|
||||
...(notificationPrefs !== undefined && { notificationPrefs }),
|
||||
}
|
||||
await userPreferences.upsert(userId, { profile })
|
||||
respond.success(res, { profile }, 'Profile saved')
|
||||
}))
|
||||
|
||||
/**
|
||||
* 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')
|
||||
|
||||
app.get('/api/preferences/ai-control', requireAuth, asyncHandler(async (req, res) => {
|
||||
const userId = req.appwriteUser.id
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
const preferences = prefs?.preferences || userPreferences.getDefaults()
|
||||
|
||||
@@ -148,10 +163,10 @@ app.get('/api/preferences/ai-control', asyncHandler(async (req, res) => {
|
||||
* POST /api/preferences/ai-control
|
||||
* Save AI Control settings
|
||||
*/
|
||||
app.post('/api/preferences/ai-control', asyncHandler(async (req, res) => {
|
||||
const { userId, enabledCategories, categoryActions, autoDetectCompanies, cleanup } = req.body
|
||||
if (!userId) throw new ValidationError('userId is required')
|
||||
|
||||
app.post('/api/preferences/ai-control', requireAuth, asyncHandler(async (req, res) => {
|
||||
const userId = req.appwriteUser.id
|
||||
const { enabledCategories, categoryActions, autoDetectCompanies, cleanup } = req.body
|
||||
|
||||
const updates = {}
|
||||
if (enabledCategories !== undefined) updates.enabledCategories = enabledCategories
|
||||
if (categoryActions !== undefined) updates.categoryActions = categoryActions
|
||||
@@ -166,10 +181,9 @@ app.post('/api/preferences/ai-control', asyncHandler(async (req, res) => {
|
||||
* 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')
|
||||
|
||||
app.get('/api/preferences/company-labels', requireAuth, asyncHandler(async (req, res) => {
|
||||
const userId = req.appwriteUser.id
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
const preferences = prefs?.preferences || userPreferences.getDefaults()
|
||||
|
||||
@@ -180,9 +194,9 @@ app.get('/api/preferences/company-labels', asyncHandler(async (req, res) => {
|
||||
* 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')
|
||||
app.post('/api/preferences/company-labels', requireAuth, asyncHandler(async (req, res) => {
|
||||
const userId = req.appwriteUser.id
|
||||
const { companyLabel } = req.body
|
||||
if (!companyLabel) throw new ValidationError('companyLabel is required')
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
@@ -211,10 +225,9 @@ app.post('/api/preferences/company-labels', asyncHandler(async (req, res) => {
|
||||
* DELETE /api/preferences/company-labels/:id
|
||||
* Delete company label
|
||||
*/
|
||||
app.delete('/api/preferences/company-labels/:id', asyncHandler(async (req, res) => {
|
||||
const { userId } = req.query
|
||||
app.delete('/api/preferences/company-labels/:id', requireAuth, asyncHandler(async (req, res) => {
|
||||
const userId = req.appwriteUser.id
|
||||
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)
|
||||
@@ -230,12 +243,10 @@ app.delete('/api/preferences/company-labels/:id', asyncHandler(async (req, res)
|
||||
* GET /api/preferences/name-labels
|
||||
* Get name labels (worker labels). Admin only.
|
||||
*/
|
||||
app.get('/api/preferences/name-labels', asyncHandler(async (req, res) => {
|
||||
const { userId, email } = req.query
|
||||
if (!userId) throw new ValidationError('userId is required')
|
||||
if (!email || typeof email !== 'string') throw new ValidationError('email is required')
|
||||
if (!isAdmin(email)) throw new AuthorizationError('Admin access required for name labels')
|
||||
app.get('/api/preferences/name-labels', requireAuth, asyncHandler(async (req, res) => {
|
||||
if (!isAdmin(req.appwriteUser.email)) throw new AuthorizationError('Admin access required for name labels')
|
||||
|
||||
const userId = req.appwriteUser.id
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
const preferences = prefs?.preferences || userPreferences.getDefaults()
|
||||
respond.success(res, preferences.nameLabels || [])
|
||||
@@ -245,11 +256,11 @@ app.get('/api/preferences/name-labels', asyncHandler(async (req, res) => {
|
||||
* POST /api/preferences/name-labels
|
||||
* Save/Update name label (worker). Admin only.
|
||||
*/
|
||||
app.post('/api/preferences/name-labels', asyncHandler(async (req, res) => {
|
||||
const { userId, email, nameLabel } = req.body
|
||||
if (!userId) throw new ValidationError('userId is required')
|
||||
if (!email || typeof email !== 'string') throw new ValidationError('email is required')
|
||||
if (!isAdmin(email)) throw new AuthorizationError('Admin access required for name labels')
|
||||
app.post('/api/preferences/name-labels', requireAuth, asyncHandler(async (req, res) => {
|
||||
if (!isAdmin(req.appwriteUser.email)) throw new AuthorizationError('Admin access required for name labels')
|
||||
|
||||
const userId = req.appwriteUser.id
|
||||
const { nameLabel } = req.body
|
||||
if (!nameLabel) throw new ValidationError('nameLabel is required')
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
@@ -274,12 +285,11 @@ app.post('/api/preferences/name-labels', asyncHandler(async (req, res) => {
|
||||
* DELETE /api/preferences/name-labels/:id
|
||||
* Delete name label. Admin only.
|
||||
*/
|
||||
app.delete('/api/preferences/name-labels/:id', asyncHandler(async (req, res) => {
|
||||
const { userId, email } = req.query
|
||||
app.delete('/api/preferences/name-labels/:id', requireAuth, asyncHandler(async (req, res) => {
|
||||
if (!isAdmin(req.appwriteUser.email)) throw new AuthorizationError('Admin access required for name labels')
|
||||
|
||||
const userId = req.appwriteUser.id
|
||||
const { id } = req.params
|
||||
if (!userId) throw new ValidationError('userId is required')
|
||||
if (!email || typeof email !== 'string') throw new ValidationError('email is required')
|
||||
if (!isAdmin(email)) throw new AuthorizationError('Admin access required for name labels')
|
||||
if (!id) throw new ValidationError('label id is required')
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
@@ -292,14 +302,33 @@ app.delete('/api/preferences/name-labels/:id', asyncHandler(async (req, res) =>
|
||||
// Legacy Stripe webhook endpoint
|
||||
app.use('/stripe', stripeRoutes)
|
||||
|
||||
// 404 handler for API routes
|
||||
app.use('/api/*', (req, res, next) => {
|
||||
// Unmatched /api → JSON 404 (Express 4 treats '/api/*' as a literal path, not a wildcard)
|
||||
app.use((req, res, next) => {
|
||||
const pathOnly = req.originalUrl.split('?')[0]
|
||||
if (!pathOnly.startsWith('/api')) {
|
||||
return next()
|
||||
}
|
||||
next(new NotFoundError('Endpoint'))
|
||||
})
|
||||
|
||||
// SPA fallback for non-API routes
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(join(__dirname, '..', 'public', 'index.html'))
|
||||
// SPA fallback: never send index.html for /api (avoids 404/HTML when public/index.html is missing)
|
||||
app.get('*', (req, res, next) => {
|
||||
const pathOnly = req.originalUrl.split('?')[0]
|
||||
if (pathOnly.startsWith('/api')) {
|
||||
return next(new NotFoundError('Endpoint'))
|
||||
}
|
||||
const indexPath = join(__dirname, '..', 'public', 'index.html')
|
||||
res.sendFile(indexPath, (err) => {
|
||||
if (err) {
|
||||
next(
|
||||
new AppError(
|
||||
'public/index.html fehlt. In Entwicklung: Frontend über Vite (z. B. http://localhost:5173) starten; für Produktion: Client-Build nach public/ legen.',
|
||||
404,
|
||||
'NOT_FOUND'
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Global error handler (must be last)
|
||||
@@ -346,6 +375,7 @@ server = app.listen(config.port, () => {
|
||||
console.log(` 🌐 API: http://localhost:${config.port}/api`)
|
||||
console.log(` 💚 Health: http://localhost:${config.port}/api/health`)
|
||||
console.log('')
|
||||
startCounterJobs()
|
||||
})
|
||||
|
||||
export default app
|
||||
|
||||
Reference in New Issue
Block a user