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

@@ -4,7 +4,7 @@
*/
import { Client, Databases, Query, ID } from 'node-appwrite'
import { config } from '../config/index.mjs'
import { config, isAdmin } from '../config/index.mjs'
import { NotFoundError } from '../middleware/errorHandler.mjs'
// Initialize Appwrite client
@@ -236,22 +236,26 @@ export const emailStats = {
},
async resetDaily() {
// Reset daily counters - would be called by a cron job
const allStats = await db.list(Collections.EMAIL_STATS, [])
let n = 0
for (const stat of allStats) {
await db.update(Collections.EMAIL_STATS, stat.$id, { todaySorted: 0 })
n++
}
return n
},
async resetWeekly() {
// Reset weekly counters - would be called by a cron job
const allStats = await db.list(Collections.EMAIL_STATS, [])
let n = 0
for (const stat of allStats) {
await db.update(Collections.EMAIL_STATS, stat.$id, {
await db.update(Collections.EMAIL_STATS, stat.$id, {
weekSorted: 0,
categoriesJson: '{}',
})
n++
}
return n
},
}
@@ -299,42 +303,60 @@ export const emailUsage = {
* Subscriptions operations
*/
export const subscriptions = {
async getByUser(userId) {
/**
* @param {string} userId
* @param {string|null} [viewerEmail] - if set and isAdmin(email), effective plan is business (highest tier)
*/
async getByUser(userId, viewerEmail = null) {
const subscription = await db.findOne(Collections.SUBSCRIPTIONS, [Query.equal('userId', userId)])
let result
// If no subscription, user is on free tier
if (!subscription) {
const usage = await emailUsage.getUsage(userId)
return {
result = {
plan: 'free',
status: 'active',
isFreeTier: true,
emailsUsedThisMonth: usage.emailsProcessed,
emailsLimit: 500, // From config
}
} else {
// Check if subscription is active
const isActive = subscription.status === 'active'
const isFreeTier = !isActive || subscription.plan === 'free'
// Get usage for free tier users
let emailsUsedThisMonth = 0
let emailsLimit = -1 // Unlimited for paid
if (isFreeTier) {
const usage = await emailUsage.getUsage(userId)
emailsUsedThisMonth = usage.emailsProcessed
emailsLimit = 500 // From config
}
result = {
...subscription,
plan: subscription.plan || 'free',
isFreeTier,
emailsUsedThisMonth,
emailsLimit,
}
}
// Check if subscription is active
const isActive = subscription.status === 'active'
const isFreeTier = !isActive || subscription.plan === 'free'
// Get usage for free tier users
let emailsUsedThisMonth = 0
let emailsLimit = -1 // Unlimited for paid
if (isFreeTier) {
const usage = await emailUsage.getUsage(userId)
emailsUsedThisMonth = usage.emailsProcessed
emailsLimit = 500 // From config
if (viewerEmail && isAdmin(viewerEmail)) {
return {
...result,
plan: config.topSubscriptionPlan,
status: 'active',
isFreeTier: false,
emailsLimit: -1,
}
}
return {
...subscription,
plan: subscription.plan || 'free',
isFreeTier,
emailsUsedThisMonth,
emailsLimit,
}
return result
},
async getByStripeId(stripeSubscriptionId) {
@@ -352,8 +374,8 @@ export const subscriptions = {
},
async upsertByUser(userId, data) {
const existing = await this.getByUser(userId)
if (existing) {
const existing = await db.findOne(Collections.SUBSCRIPTIONS, [Query.equal('userId', userId)])
if (existing?.$id) {
return this.update(existing.$id, data)
}
return this.create({ userId, ...data })
@@ -377,6 +399,12 @@ export const userPreferences = {
autoDetectCompanies: true,
version: 1,
categoryAdvanced: {},
profile: {
displayName: '',
timezone: '',
notificationPrefs: {},
},
cleanupMeta: {},
cleanup: {
enabled: false,
readItems: {
@@ -413,6 +441,9 @@ export const userPreferences = {
companyLabels: preferences.companyLabels || defaults.companyLabels,
nameLabels: preferences.nameLabels || defaults.nameLabels,
autoDetectCompanies: preferences.autoDetectCompanies !== undefined ? preferences.autoDetectCompanies : defaults.autoDetectCompanies,
profile: preferences.profile != null ? { ...defaults.profile, ...preferences.profile } : defaults.profile,
cleanupMeta:
preferences.cleanupMeta !== undefined ? preferences.cleanupMeta : defaults.cleanupMeta,
}
},