cms.c2sgmbh/src/app/(payload)/api/email-logs/stats/route.ts
Martin Porwoll 6bbbea52fc feat: implement monitoring & alerting system
- Add AuditLogs collection for tracking critical system actions
  - User changes (create, update, delete)
  - Tenant changes with sensitive data masking
  - Login events tracking

- Add Alert Service with multi-channel support
  - Email, Slack, Discord, Console channels
  - Configurable alert levels (info, warning, error, critical)
  - Environment-based configuration

- Add Email failure alerting
  - Automatic alerts on repeated failed emails
  - Per-tenant failure counting with hourly reset

- Add Email-Logs API endpoints
  - GET /api/email-logs/export (CSV/JSON export)
  - GET /api/email-logs/stats (statistics with filters)

- Add audit hooks for Users and Tenants collections
- Update TODO.md with completed monitoring tasks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-07 20:58:20 +00:00

175 lines
5 KiB
TypeScript

/**
* Email-Logs Statistics Endpoint
*
* GET /api/email-logs/stats
*
* Liefert Statistiken über Email-Logs.
* Nur für Super Admins und Tenant-Admins (für ihre eigenen Tenants).
*
* Query-Parameter:
* - tenantId: number (optional)
* - period: '24h' | '7d' | '30d' (default: '7d')
*/
import { getPayload } from 'payload'
import configPromise from '@payload-config'
import { NextRequest, NextResponse } from 'next/server'
interface UserWithTenants {
id: number
email: string
isSuperAdmin?: boolean
tenants?: Array<{ tenant: { id: number } | number }>
}
function getUserTenantIds(user: UserWithTenants): number[] {
return (user.tenants || []).map((t) => (typeof t.tenant === 'object' ? t.tenant.id : t.tenant))
}
function getPeriodDate(period: string): Date {
const now = new Date()
switch (period) {
case '24h':
return new Date(now.getTime() - 24 * 60 * 60 * 1000)
case '30d':
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
case '7d':
default:
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
}
}
export async function GET(req: NextRequest): Promise<NextResponse> {
try {
const payload = await getPayload({ config: configPromise })
// Auth prüfen
const { user } = await payload.auth({ headers: req.headers })
if (!user) {
return NextResponse.json({ error: 'Nicht authentifiziert' }, { status: 401 })
}
const typedUser = user as unknown as UserWithTenants
// Query-Parameter parsen
const searchParams = req.nextUrl.searchParams
const tenantIdParam = searchParams.get('tenantId')
const period = searchParams.get('period') || '7d'
// Tenant-Zugriff prüfen
const userTenantIds = getUserTenantIds(typedUser)
let tenantFilter: number[] | undefined
if (typedUser.isSuperAdmin) {
if (tenantIdParam) {
tenantFilter = [parseInt(tenantIdParam, 10)]
}
} else {
if (tenantIdParam) {
const requestedTenant = parseInt(tenantIdParam, 10)
if (!userTenantIds.includes(requestedTenant)) {
return NextResponse.json({ error: 'Kein Zugriff auf diesen Tenant' }, { status: 403 })
}
tenantFilter = [requestedTenant]
} else {
tenantFilter = userTenantIds
}
}
const periodDate = getPeriodDate(period)
// Basis-Where für alle Queries
const baseWhere: Record<string, unknown> = {
createdAt: { greater_than_equal: periodDate.toISOString() },
}
if (tenantFilter) {
baseWhere.tenant = { in: tenantFilter }
}
// Statistiken parallel abrufen - Type assertions für email-logs Collection
const countFn = payload.count as Function
const findFn = payload.find as Function
const [totalResult, sentResult, failedResult, pendingResult, recentFailed] = await Promise.all([
// Gesamt
countFn({
collection: 'email-logs',
where: baseWhere,
}),
// Gesendet
countFn({
collection: 'email-logs',
where: { ...baseWhere, status: { equals: 'sent' } },
}),
// Fehlgeschlagen
countFn({
collection: 'email-logs',
where: { ...baseWhere, status: { equals: 'failed' } },
}),
// Ausstehend
countFn({
collection: 'email-logs',
where: { ...baseWhere, status: { equals: 'pending' } },
}),
// Letzte 5 fehlgeschlagene (für Quick-View)
findFn({
collection: 'email-logs',
where: { ...baseWhere, status: { equals: 'failed' } },
limit: 5,
sort: '-createdAt',
depth: 1,
}),
])
// Erfolgsrate berechnen
const total = totalResult.totalDocs
const sent = sentResult.totalDocs
const failed = failedResult.totalDocs
const pending = pendingResult.totalDocs
const successRate = total > 0 ? Math.round((sent / total) * 100 * 10) / 10 : 0
// Statistiken nach Quelle
const sourceStats: Record<string, number> = {}
const sources = ['manual', 'form', 'system', 'newsletter']
await Promise.all(
sources.map(async (source) => {
const result = await countFn({
collection: 'email-logs',
where: { ...baseWhere, source: { equals: source } },
})
sourceStats[source] = result.totalDocs
}),
)
return NextResponse.json({
success: true,
period,
periodStart: periodDate.toISOString(),
stats: {
total,
sent,
failed,
pending,
successRate,
},
bySource: sourceStats,
recentFailures: recentFailed.docs.map((doc: Record<string, unknown>) => ({
id: doc.id,
to: doc.to,
subject: doc.subject,
error: doc.error,
createdAt: doc.createdAt,
tenantId:
typeof doc.tenant === 'object' && doc.tenant
? (doc.tenant as { id?: number }).id
: doc.tenant,
})),
})
} catch (error) {
console.error('[EmailLogs:Stats] Error:', error)
return NextResponse.json({ error: 'Interner Serverfehler' }, { status: 500 })
}
}