mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 22:04:10 +00:00
- 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>
175 lines
5 KiB
TypeScript
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 })
|
|
}
|
|
}
|