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

@@ -6,14 +6,26 @@
import express from 'express'
import { OAuth2Client } from 'google-auth-library'
import { ConfidentialClientApplication } from '@azure/msal-node'
import { asyncHandler, ValidationError, AppError } from '../middleware/errorHandler.mjs'
import { asyncHandler, ValidationError, AppError, AuthorizationError } from '../middleware/errorHandler.mjs'
import { respond } from '../utils/response.mjs'
import { emailAccounts } from '../services/database.mjs'
import { config, features } from '../config/index.mjs'
import { log } from '../middleware/logger.mjs'
import { requireAuth } from '../middleware/auth.mjs'
import { buildOAuthState, parseOAuthState } from '../utils/oauth-state.mjs'
const router = express.Router()
function requireAuthUnlessOAuthPublic(req, res, next) {
const p = req.path || ''
if (['/gmail/callback', '/outlook/callback', '/status'].includes(p)) {
return next()
}
return requireAuth(req, res, next)
}
router.use(requireAuthUnlessOAuthPublic)
// Google OAuth client (lazy initialization)
let googleClient = null
@@ -71,12 +83,6 @@ const OUTLOOK_SCOPES = [
* Initiate Gmail OAuth flow
*/
router.get('/gmail/connect', asyncHandler(async (req, res) => {
const { userId } = req.query
if (!userId) {
throw new ValidationError('userId ist erforderlich')
}
if (!features.gmail()) {
throw new AppError('Gmail OAuth ist nicht konfiguriert', 503, 'FEATURE_DISABLED')
}
@@ -86,7 +92,7 @@ router.get('/gmail/connect', asyncHandler(async (req, res) => {
access_type: 'offline',
scope: GMAIL_SCOPES,
prompt: 'consent',
state: JSON.stringify({ userId }),
state: buildOAuthState(req.appwriteUser.id),
include_granted_scopes: true,
})
@@ -118,10 +124,10 @@ router.get('/gmail/callback', asyncHandler(async (req, res) => {
let userId
try {
const stateData = JSON.parse(state)
const stateData = parseOAuthState(state)
userId = stateData.userId
} catch (e) {
log.error('Gmail OAuth: State konnte nicht geparst werden', { state })
log.error('Gmail OAuth: State konnte nicht geparst werden', { state, error: e.message })
return res.redirect(`${config.frontendUrl}/settings?error=invalid_state`)
}
@@ -214,6 +220,10 @@ router.post('/gmail/refresh', asyncHandler(async (req, res) => {
const account = await emailAccounts.get(accountId)
if (account.userId !== req.appwriteUser.id) {
throw new AuthorizationError('No permission for this account')
}
if (account.provider !== 'gmail') {
throw new ValidationError('Kein Gmail-Konto')
}
@@ -249,12 +259,6 @@ router.post('/gmail/refresh', asyncHandler(async (req, res) => {
* Initiate Outlook OAuth flow
*/
router.get('/outlook/connect', asyncHandler(async (req, res) => {
const { userId } = req.query
if (!userId) {
throw new ValidationError('userId ist erforderlich')
}
if (!features.outlook()) {
throw new AppError('Outlook OAuth ist nicht konfiguriert', 503, 'FEATURE_DISABLED')
}
@@ -263,7 +267,7 @@ router.get('/outlook/connect', asyncHandler(async (req, res) => {
const authUrl = await client.getAuthCodeUrl({
scopes: OUTLOOK_SCOPES,
redirectUri: config.microsoft.redirectUri,
state: JSON.stringify({ userId }),
state: buildOAuthState(req.appwriteUser.id),
prompt: 'select_account',
})
@@ -286,7 +290,14 @@ router.get('/outlook/callback', asyncHandler(async (req, res) => {
throw new ValidationError('Code und State sind erforderlich')
}
const { userId } = JSON.parse(state)
let userId
try {
userId = parseOAuthState(state).userId
} catch (e) {
log.error('Outlook OAuth: invalid state', { error: e.message })
return respond.redirect(res, `${config.frontendUrl}/settings?error=invalid_state`)
}
const client = getMsalClient()
// Exchange code for tokens
@@ -334,6 +345,10 @@ router.post('/outlook/refresh', asyncHandler(async (req, res) => {
const account = await emailAccounts.get(accountId)
if (account.userId !== req.appwriteUser.id) {
throw new AuthorizationError('No permission for this account')
}
if (account.provider !== 'outlook') {
throw new ValidationError('Kein Outlook-Konto')
}