feat: add repository pattern, service layer architecture, and scheduler management

- Implement repository pattern for data access layer
- Add comprehensive service layer for business logic
- Create scheduler management system with health monitoring
- Add bounded buffer utility for memory management
- Enhance security audit logging with retention policies
This commit is contained in:
2025-07-12 07:00:37 +02:00
parent e1abedb148
commit 041a1cc3ef
54 changed files with 5755 additions and 878 deletions

View File

@ -1,3 +1,4 @@
import type { Prisma } from "@prisma/client";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../../../lib/auth";
@ -5,7 +6,9 @@ import { prisma } from "../../../../lib/prisma";
import { extractClientIP } from "../../../../lib/rateLimiter";
import {
AuditOutcome,
type AuditSeverity,
createAuditMetadata,
type SecurityEventType,
securityAuditLogger,
} from "../../../../lib/securityAuditLogger";
@ -89,26 +92,16 @@ function parseAuditLogFilters(url: URL) {
function buildAuditLogWhereClause(
companyId: string,
filters: ReturnType<typeof parseAuditLogFilters>
) {
): Prisma.SecurityAuditLogWhereInput {
const { eventType, outcome, severity, userId, startDate, endDate } = filters;
const where: {
companyId: string;
eventType?: string;
outcome?: string;
severity?: string;
userId?: string;
timestamp?: {
gte?: Date;
lte?: Date;
};
} = {
const where: Prisma.SecurityAuditLogWhereInput = {
companyId, // Only show logs for user's company
};
if (eventType) where.eventType = eventType;
if (outcome) where.outcome = outcome;
if (severity) where.severity = severity;
if (eventType) where.eventType = eventType as SecurityEventType;
if (outcome) where.outcome = outcome as AuditOutcome;
if (severity) where.severity = severity as AuditSeverity;
if (userId) where.userId = userId;
if (startDate || endDate) {

View File

@ -0,0 +1,61 @@
import { NextResponse } from "next/server";
import { getSchedulerIntegration } from "@/lib/services/schedulers/ServerSchedulerIntegration";
/**
* Health check endpoint for schedulers
* Used by load balancers and orchestrators for health monitoring
*/
export async function GET() {
try {
const integration = getSchedulerIntegration();
const health = integration.getHealthStatus();
// Return appropriate HTTP status based on health
const status = health.healthy ? 200 : 503;
return NextResponse.json(
{
healthy: health.healthy,
status: health.healthy ? "healthy" : "unhealthy",
timestamp: new Date().toISOString(),
schedulers: {
total: health.totalSchedulers,
running: health.runningSchedulers,
errors: health.errorSchedulers,
},
details: health.schedulerStatuses,
},
{ status }
);
} catch (error) {
console.error("[Scheduler Health API] Error:", error);
return NextResponse.json(
{
healthy: false,
status: "error",
timestamp: new Date().toISOString(),
error: "Failed to get scheduler health status",
},
{ status: 500 }
);
}
}
/**
* Readiness check endpoint
* Used by Kubernetes and other orchestrators
*/
export async function HEAD() {
try {
const integration = getSchedulerIntegration();
const health = integration.getHealthStatus();
// Return 200 if healthy, 503 if not
const status = health.healthy ? 200 : 503;
return new NextResponse(null, { status });
} catch (_error) {
return new NextResponse(null, { status: 500 });
}
}

View File

@ -0,0 +1,131 @@
import { type NextRequest, NextResponse } from "next/server";
import { getSchedulerIntegration } from "@/lib/services/schedulers/ServerSchedulerIntegration";
/**
* Get all schedulers with their status and metrics
*/
export async function GET() {
try {
const integration = getSchedulerIntegration();
const schedulers = integration.getSchedulersList();
const health = integration.getHealthStatus();
return NextResponse.json({
success: true,
data: {
health,
schedulers,
timestamp: new Date().toISOString(),
},
});
} catch (error) {
console.error("[Scheduler Management API] GET Error:", error);
return NextResponse.json(
{
success: false,
error: "Failed to get scheduler information",
timestamp: new Date().toISOString(),
},
{ status: 500 }
);
}
}
/**
* Control scheduler operations (start/stop/trigger)
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { action, schedulerId } = body;
if (!action) {
return NextResponse.json(
{
success: false,
error: "Action is required",
},
{ status: 400 }
);
}
const integration = getSchedulerIntegration();
switch (action) {
case "start":
if (!schedulerId) {
return NextResponse.json(
{
success: false,
error: "schedulerId is required for start action",
},
{ status: 400 }
);
}
await integration.startScheduler(schedulerId);
break;
case "stop":
if (!schedulerId) {
return NextResponse.json(
{
success: false,
error: "schedulerId is required for stop action",
},
{ status: 400 }
);
}
await integration.stopScheduler(schedulerId);
break;
case "trigger":
if (!schedulerId) {
return NextResponse.json(
{
success: false,
error: "schedulerId is required for trigger action",
},
{ status: 400 }
);
}
await integration.triggerScheduler(schedulerId);
break;
case "startAll":
await integration.getManager().startAll();
break;
case "stopAll":
await integration.getManager().stopAll();
break;
default:
return NextResponse.json(
{
success: false,
error: `Unknown action: ${action}`,
},
{ status: 400 }
);
}
return NextResponse.json({
success: true,
message: `Action '${action}' completed successfully`,
timestamp: new Date().toISOString(),
});
} catch (error) {
console.error("[Scheduler Management API] POST Error:", error);
return NextResponse.json(
{
success: false,
error:
error instanceof Error ? error.message : "Unknown error occurred",
timestamp: new Date().toISOString(),
},
{ status: 500 }
);
}
}

View File

@ -22,8 +22,8 @@ function convertToMockChatSession(
sentiment: string | null;
messagesSent: number | null;
avgResponseTime: number | null;
escalated: boolean;
forwardedHr: boolean;
escalated: boolean | null;
forwardedHr: boolean | null;
initialMsg: string | null;
fullTranscriptUrl: string | null;
summary: string | null;

View File

@ -17,8 +17,8 @@ function mapPrismaSessionToChatSession(prismaSession: {
sentiment: string | null;
messagesSent: number | null;
avgResponseTime: number | null;
escalated: boolean;
forwardedHr: boolean;
escalated: boolean | null;
forwardedHr: boolean | null;
initialMsg: string | null;
fullTranscriptUrl: string | null;
summary: string | null;
@ -55,8 +55,8 @@ function mapPrismaSessionToChatSession(prismaSession: {
sentiment: prismaSession.sentiment ?? null,
messagesSent: prismaSession.messagesSent ?? undefined, // Maintain consistency with other nullable fields
avgResponseTime: prismaSession.avgResponseTime ?? null,
escalated: prismaSession.escalated,
forwardedHr: prismaSession.forwardedHr,
escalated: prismaSession.escalated ?? false,
forwardedHr: prismaSession.forwardedHr ?? false,
initialMsg: prismaSession.initialMsg ?? undefined,
fullTranscriptUrl: prismaSession.fullTranscriptUrl ?? undefined,
summary: prismaSession.summary ?? undefined, // New field

View File

@ -1,6 +1,6 @@
import type { CompanyStatus } from "@prisma/client";
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { getServerSession, type Session } from "next-auth";
import { platformAuthOptions } from "../../../../lib/platform-auth";
import { prisma } from "../../../../lib/prisma";
import { extractClientIP } from "../../../../lib/rateLimiter";
@ -12,7 +12,7 @@ import {
// GET /api/platform/companies - List all companies
export async function GET(request: NextRequest) {
let session: any = null;
let session: Session | null = null;
try {
session = await getServerSession(platformAuthOptions);
@ -139,7 +139,7 @@ export async function GET(request: NextRequest) {
// POST /api/platform/companies - Create new company
export async function POST(request: NextRequest) {
let session: any = null;
let session: Session | null = null;
try {
session = await getServerSession(platformAuthOptions);
@ -229,7 +229,7 @@ export async function POST(request: NextRequest) {
name: adminName,
role: "ADMIN",
companyId: company.id,
invitedBy: session.user.email || "platform",
invitedBy: session?.user?.email || "platform",
invitedAt: new Date(),
},
});