mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 13:12:10 +01:00
refactor: fix biome linting issues and update project documentation
- Fix 36+ biome linting issues reducing errors/warnings from 227 to 191 - Replace explicit 'any' types with proper TypeScript interfaces - Fix React hooks dependencies and useCallback patterns - Resolve unused variables and parameter assignment issues - Improve accessibility with proper label associations - Add comprehensive API documentation for admin and security features - Update README.md with accurate PostgreSQL setup and current tech stack - Create complete documentation for audit logging, CSP monitoring, and batch processing - Fix outdated project information and missing developer workflows
This commit is contained in:
220
app/api/admin/audit-logs/retention/route.ts
Normal file
220
app/api/admin/audit-logs/retention/route.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import {
|
||||
AuditLogRetentionManager,
|
||||
DEFAULT_RETENTION_POLICIES,
|
||||
executeScheduledRetention,
|
||||
} from "../../../../../lib/auditLogRetention";
|
||||
import { auditLogScheduler } from "../../../../../lib/auditLogScheduler";
|
||||
import { authOptions } from "../../../../../lib/auth";
|
||||
import { extractClientIP } from "../../../../../lib/rateLimiter";
|
||||
import {
|
||||
AuditOutcome,
|
||||
createAuditMetadata,
|
||||
securityAuditLogger,
|
||||
} from "../../../../../lib/securityAuditLogger";
|
||||
|
||||
// GET /api/admin/audit-logs/retention - Get retention statistics and policy status
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const ip = extractClientIP(request);
|
||||
const userAgent = request.headers.get("user-agent") || undefined;
|
||||
|
||||
if (!session?.user) {
|
||||
await securityAuditLogger.logAuthorization(
|
||||
"audit_retention_unauthorized_access",
|
||||
AuditOutcome.BLOCKED,
|
||||
{
|
||||
ipAddress: ip,
|
||||
userAgent,
|
||||
metadata: createAuditMetadata({
|
||||
error: "no_session",
|
||||
}),
|
||||
},
|
||||
"Unauthorized attempt to access audit retention management"
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Unauthorized" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Only allow ADMIN users to manage audit log retention
|
||||
if (session.user.role !== "ADMIN") {
|
||||
await securityAuditLogger.logAuthorization(
|
||||
"audit_retention_insufficient_permissions",
|
||||
AuditOutcome.BLOCKED,
|
||||
{
|
||||
userId: session.user.id,
|
||||
companyId: session.user.companyId,
|
||||
ipAddress: ip,
|
||||
userAgent,
|
||||
metadata: createAuditMetadata({
|
||||
userRole: session.user.role,
|
||||
requiredRole: "ADMIN",
|
||||
}),
|
||||
},
|
||||
"Insufficient permissions to access audit retention management"
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Insufficient permissions" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const manager = new AuditLogRetentionManager();
|
||||
|
||||
// Get retention statistics and policy information
|
||||
const [statistics, policyValidation, schedulerStatus] = await Promise.all([
|
||||
manager.getRetentionStatistics(),
|
||||
manager.validateRetentionPolicies(),
|
||||
Promise.resolve(auditLogScheduler.getStatus()),
|
||||
]);
|
||||
|
||||
// Log successful retention info access
|
||||
await securityAuditLogger.logDataPrivacy(
|
||||
"audit_retention_info_accessed",
|
||||
AuditOutcome.SUCCESS,
|
||||
{
|
||||
userId: session.user.id,
|
||||
companyId: session.user.companyId,
|
||||
ipAddress: ip,
|
||||
userAgent,
|
||||
metadata: createAuditMetadata({
|
||||
totalLogs: statistics.totalLogs,
|
||||
schedulerRunning: schedulerStatus.isRunning,
|
||||
}),
|
||||
},
|
||||
"Audit retention information accessed by admin"
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
statistics,
|
||||
policies: DEFAULT_RETENTION_POLICIES,
|
||||
policyValidation,
|
||||
scheduler: schedulerStatus,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching audit retention info:", error);
|
||||
|
||||
await securityAuditLogger.logDataPrivacy(
|
||||
"audit_retention_info_error",
|
||||
AuditOutcome.FAILURE,
|
||||
{
|
||||
userId: session?.user?.id,
|
||||
companyId: session?.user?.companyId,
|
||||
ipAddress: extractClientIP(request),
|
||||
userAgent: request.headers.get("user-agent") || undefined,
|
||||
metadata: createAuditMetadata({
|
||||
error: "server_error",
|
||||
}),
|
||||
},
|
||||
`Server error while fetching audit retention info: ${error}`
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/admin/audit-logs/retention - Execute retention policies manually
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const ip = extractClientIP(request);
|
||||
const userAgent = request.headers.get("user-agent") || undefined;
|
||||
|
||||
if (!session?.user || session.user.role !== "ADMIN") {
|
||||
await securityAuditLogger.logAuthorization(
|
||||
"audit_retention_execute_unauthorized",
|
||||
AuditOutcome.BLOCKED,
|
||||
{
|
||||
userId: session?.user?.id,
|
||||
companyId: session?.user?.companyId,
|
||||
ipAddress: ip,
|
||||
userAgent,
|
||||
metadata: createAuditMetadata({
|
||||
error: "insufficient_permissions",
|
||||
}),
|
||||
},
|
||||
"Unauthorized attempt to execute audit retention"
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Unauthorized" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { action, isDryRun = true } = body;
|
||||
|
||||
if (action !== "execute") {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Invalid action. Use 'execute'" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Log retention execution attempt
|
||||
await securityAuditLogger.logDataPrivacy(
|
||||
"audit_retention_manual_execution",
|
||||
AuditOutcome.SUCCESS,
|
||||
{
|
||||
userId: session.user.id,
|
||||
companyId: session.user.companyId,
|
||||
ipAddress: ip,
|
||||
userAgent,
|
||||
metadata: createAuditMetadata({
|
||||
isDryRun,
|
||||
triggerType: "manual_admin",
|
||||
}),
|
||||
},
|
||||
`Admin manually triggered audit retention (dry run: ${isDryRun})`
|
||||
);
|
||||
|
||||
// Execute retention policies
|
||||
const results = await executeScheduledRetention(isDryRun);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
message: isDryRun
|
||||
? "Dry run completed successfully"
|
||||
: "Retention policies executed successfully",
|
||||
isDryRun,
|
||||
results,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error executing audit retention:", error);
|
||||
|
||||
await securityAuditLogger.logDataPrivacy(
|
||||
"audit_retention_execution_error",
|
||||
AuditOutcome.FAILURE,
|
||||
{
|
||||
userId: session?.user?.id,
|
||||
companyId: session?.user?.companyId,
|
||||
ipAddress: extractClientIP(request),
|
||||
userAgent: request.headers.get("user-agent") || undefined,
|
||||
metadata: createAuditMetadata({
|
||||
error: "server_error",
|
||||
}),
|
||||
},
|
||||
`Server error while executing audit retention: ${error}`
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
208
app/api/admin/audit-logs/route.ts
Normal file
208
app/api/admin/audit-logs/route.ts
Normal file
@ -0,0 +1,208 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { extractClientIP } from "../../../../lib/rateLimiter";
|
||||
import {
|
||||
AuditOutcome,
|
||||
createAuditMetadata,
|
||||
securityAuditLogger,
|
||||
} from "../../../../lib/securityAuditLogger";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const ip = extractClientIP(request);
|
||||
const userAgent = request.headers.get("user-agent") || undefined;
|
||||
|
||||
if (!session?.user) {
|
||||
await securityAuditLogger.logAuthorization(
|
||||
"audit_logs_unauthorized_access",
|
||||
AuditOutcome.BLOCKED,
|
||||
{
|
||||
ipAddress: ip,
|
||||
userAgent,
|
||||
metadata: createAuditMetadata({
|
||||
error: "no_session",
|
||||
}),
|
||||
},
|
||||
"Unauthorized attempt to access audit logs"
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Unauthorized" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Only allow ADMIN users to view audit logs
|
||||
if (session.user.role !== "ADMIN") {
|
||||
await securityAuditLogger.logAuthorization(
|
||||
"audit_logs_insufficient_permissions",
|
||||
AuditOutcome.BLOCKED,
|
||||
{
|
||||
userId: session.user.id,
|
||||
companyId: session.user.companyId,
|
||||
ipAddress: ip,
|
||||
userAgent,
|
||||
metadata: createAuditMetadata({
|
||||
userRole: session.user.role,
|
||||
requiredRole: "ADMIN",
|
||||
}),
|
||||
},
|
||||
"Insufficient permissions to access audit logs"
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Insufficient permissions" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const page = Number.parseInt(url.searchParams.get("page") || "1");
|
||||
const limit = Math.min(
|
||||
Number.parseInt(url.searchParams.get("limit") || "50"),
|
||||
100
|
||||
);
|
||||
const eventType = url.searchParams.get("eventType");
|
||||
const outcome = url.searchParams.get("outcome");
|
||||
const severity = url.searchParams.get("severity");
|
||||
const userId = url.searchParams.get("userId");
|
||||
const startDate = url.searchParams.get("startDate");
|
||||
const endDate = url.searchParams.get("endDate");
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build filter conditions
|
||||
const where: {
|
||||
companyId: string;
|
||||
eventType?: string;
|
||||
outcome?: string;
|
||||
timestamp?: {
|
||||
gte?: Date;
|
||||
lte?: Date;
|
||||
};
|
||||
} = {
|
||||
companyId: session.user.companyId, // Only show logs for user's company
|
||||
};
|
||||
|
||||
if (eventType) {
|
||||
where.eventType = eventType;
|
||||
}
|
||||
|
||||
if (outcome) {
|
||||
where.outcome = outcome;
|
||||
}
|
||||
|
||||
if (severity) {
|
||||
where.severity = severity;
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
where.userId = userId;
|
||||
}
|
||||
|
||||
if (startDate || endDate) {
|
||||
where.timestamp = {};
|
||||
if (startDate) {
|
||||
where.timestamp.gte = new Date(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
where.timestamp.lte = new Date(endDate);
|
||||
}
|
||||
}
|
||||
|
||||
// Get audit logs with pagination
|
||||
const [auditLogs, totalCount] = await Promise.all([
|
||||
prisma.securityAuditLog.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { timestamp: "desc" },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
platformUser: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.securityAuditLog.count({ where }),
|
||||
]);
|
||||
|
||||
// Log successful audit log access
|
||||
await securityAuditLogger.logDataPrivacy(
|
||||
"audit_logs_accessed",
|
||||
AuditOutcome.SUCCESS,
|
||||
{
|
||||
userId: session.user.id,
|
||||
companyId: session.user.companyId,
|
||||
ipAddress: ip,
|
||||
userAgent,
|
||||
metadata: createAuditMetadata({
|
||||
page,
|
||||
limit,
|
||||
filters: {
|
||||
eventType,
|
||||
outcome,
|
||||
severity,
|
||||
userId,
|
||||
startDate,
|
||||
endDate,
|
||||
},
|
||||
recordsReturned: auditLogs.length,
|
||||
}),
|
||||
},
|
||||
"Audit logs accessed by admin user"
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
auditLogs,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
totalCount,
|
||||
totalPages: Math.ceil(totalCount / limit),
|
||||
hasNext: skip + limit < totalCount,
|
||||
hasPrev: page > 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching audit logs:", error);
|
||||
|
||||
await securityAuditLogger.logDataPrivacy(
|
||||
"audit_logs_server_error",
|
||||
AuditOutcome.FAILURE,
|
||||
{
|
||||
userId: session?.user?.id,
|
||||
companyId: session?.user?.companyId,
|
||||
ipAddress: extractClientIP(request),
|
||||
userAgent: request.headers.get("user-agent") || undefined,
|
||||
metadata: createAuditMetadata({
|
||||
error: "server_error",
|
||||
}),
|
||||
},
|
||||
`Server error while fetching audit logs: ${error}`
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
159
app/api/admin/batch-monitoring/route.ts
Normal file
159
app/api/admin/batch-monitoring/route.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import {
|
||||
type BatchOperation,
|
||||
batchLogger,
|
||||
logBatchMetrics,
|
||||
} from "@/lib/batchLogger";
|
||||
import { getCircuitBreakerStatus } from "@/lib/batchProcessor";
|
||||
import { getBatchSchedulerStatus } from "@/lib/batchProcessorIntegration";
|
||||
|
||||
/**
|
||||
* GET /api/admin/batch-monitoring
|
||||
* Get comprehensive batch processing monitoring data
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user || session.user.role !== "ADMIN") {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const companyId = url.searchParams.get("companyId");
|
||||
const operation = url.searchParams.get("operation") as BatchOperation;
|
||||
const format = url.searchParams.get("format") || "json";
|
||||
|
||||
// Get batch processing metrics
|
||||
const metrics = batchLogger.getMetrics(companyId || undefined);
|
||||
|
||||
// Get scheduler status
|
||||
const schedulerStatus = getBatchSchedulerStatus();
|
||||
|
||||
// Get circuit breaker status
|
||||
const circuitBreakerStatus = getCircuitBreakerStatus();
|
||||
|
||||
// Generate performance metrics for specific operation if requested
|
||||
if (operation) {
|
||||
await logBatchMetrics(operation);
|
||||
}
|
||||
|
||||
const monitoringData = {
|
||||
timestamp: new Date().toISOString(),
|
||||
metrics,
|
||||
schedulerStatus,
|
||||
circuitBreakerStatus,
|
||||
systemHealth: {
|
||||
schedulerRunning: schedulerStatus.isRunning,
|
||||
circuitBreakersOpen: Object.values(circuitBreakerStatus).some(
|
||||
(cb) => cb.isOpen
|
||||
),
|
||||
pausedDueToErrors: schedulerStatus.isPaused,
|
||||
consecutiveErrors: schedulerStatus.consecutiveErrors,
|
||||
},
|
||||
};
|
||||
|
||||
if (
|
||||
format === "csv" &&
|
||||
typeof metrics === "object" &&
|
||||
!Array.isArray(metrics)
|
||||
) {
|
||||
// Convert metrics to CSV format
|
||||
const headers = [
|
||||
"company_id",
|
||||
"operation_start_time",
|
||||
"request_count",
|
||||
"success_count",
|
||||
"failure_count",
|
||||
"retry_count",
|
||||
"total_cost",
|
||||
"average_latency",
|
||||
"circuit_breaker_trips",
|
||||
].join(",");
|
||||
|
||||
const rows = Object.entries(metrics).map(([companyId, metric]) =>
|
||||
[
|
||||
companyId,
|
||||
new Date(metric.operationStartTime).toISOString(),
|
||||
metric.requestCount,
|
||||
metric.successCount,
|
||||
metric.failureCount,
|
||||
metric.retryCount,
|
||||
metric.totalCost.toFixed(4),
|
||||
metric.averageLatency.toFixed(2),
|
||||
metric.circuitBreakerTrips,
|
||||
].join(",")
|
||||
);
|
||||
|
||||
return new NextResponse([headers, ...rows].join("\n"), {
|
||||
headers: {
|
||||
"Content-Type": "text/csv",
|
||||
"Content-Disposition": `attachment; filename="batch-monitoring-${Date.now()}.csv"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(monitoringData);
|
||||
} catch (error) {
|
||||
console.error("Batch monitoring API error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch batch monitoring data" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/admin/batch-monitoring/export
|
||||
* Export batch processing logs
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user || session.user.role !== "ADMIN") {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { startDate, endDate, format = "json" } = body;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return NextResponse.json(
|
||||
{ error: "Start date and end date are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const timeRange = {
|
||||
start: new Date(startDate),
|
||||
end: new Date(endDate),
|
||||
};
|
||||
|
||||
const exportData = batchLogger.exportLogs(timeRange);
|
||||
|
||||
if (format === "csv") {
|
||||
return new NextResponse(exportData, {
|
||||
headers: {
|
||||
"Content-Type": "text/csv",
|
||||
"Content-Disposition": `attachment; filename="batch-logs-${startDate}-${endDate}.csv"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new NextResponse(exportData, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Disposition": `attachment; filename="batch-logs-${startDate}-${endDate}.json"`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Batch log export error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to export batch logs" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
142
app/api/admin/security-monitoring/alerts/route.ts
Normal file
142
app/api/admin/security-monitoring/alerts/route.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import {
|
||||
AuditOutcome,
|
||||
createAuditContext,
|
||||
securityAuditLogger,
|
||||
} from "@/lib/securityAuditLogger";
|
||||
import {
|
||||
type AlertSeverity,
|
||||
securityMonitoring,
|
||||
} from "@/lib/securityMonitoring";
|
||||
|
||||
const alertQuerySchema = z.object({
|
||||
severity: z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]).optional(),
|
||||
acknowledged: z.enum(["true", "false"]).optional(),
|
||||
limit: z
|
||||
.string()
|
||||
.transform((val) => Number.parseInt(val, 10))
|
||||
.optional(),
|
||||
offset: z
|
||||
.string()
|
||||
.transform((val) => Number.parseInt(val, 10))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const acknowledgeAlertSchema = z.object({
|
||||
alertId: z.string().uuid(),
|
||||
action: z.literal("acknowledge"),
|
||||
});
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user || !session.user.isPlatformUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const params = Object.fromEntries(url.searchParams.entries());
|
||||
const query = alertQuerySchema.parse(params);
|
||||
|
||||
const context = await createAuditContext(request, session);
|
||||
|
||||
// Get alerts based on filters
|
||||
const alerts = securityMonitoring.getActiveAlerts(
|
||||
query.severity as AlertSeverity
|
||||
);
|
||||
|
||||
// Apply pagination
|
||||
const limit = query.limit || 50;
|
||||
const offset = query.offset || 0;
|
||||
const paginatedAlerts = alerts.slice(offset, offset + limit);
|
||||
|
||||
// Log alert access
|
||||
await securityAuditLogger.logPlatformAdmin(
|
||||
"security_alerts_access",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
undefined,
|
||||
{
|
||||
alertCount: alerts.length,
|
||||
filters: query,
|
||||
}
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
alerts: paginatedAlerts,
|
||||
total: alerts.length,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Security alerts API error:", error);
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid query parameters", details: error.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user || !session.user.isPlatformUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { alertId, action } = acknowledgeAlertSchema.parse(body);
|
||||
const context = await createAuditContext(request, session);
|
||||
|
||||
if (action === "acknowledge") {
|
||||
const success = await securityMonitoring.acknowledgeAlert(
|
||||
alertId,
|
||||
session.user.id
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
return NextResponse.json({ error: "Alert not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Log alert acknowledgment
|
||||
await securityAuditLogger.logPlatformAdmin(
|
||||
"security_alert_acknowledged",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
undefined,
|
||||
{ alertId }
|
||||
);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error("Security alert action error:", error);
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid request", details: error.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
90
app/api/admin/security-monitoring/export/route.ts
Normal file
90
app/api/admin/security-monitoring/export/route.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import {
|
||||
AuditOutcome,
|
||||
createAuditContext,
|
||||
securityAuditLogger,
|
||||
} from "@/lib/securityAuditLogger";
|
||||
import { securityMonitoring } from "@/lib/securityMonitoring";
|
||||
|
||||
const exportQuerySchema = z.object({
|
||||
format: z.enum(["json", "csv"]).default("json"),
|
||||
startDate: z.string().datetime(),
|
||||
endDate: z.string().datetime(),
|
||||
type: z.enum(["alerts", "metrics"]).default("alerts"),
|
||||
});
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user || !session.user.isPlatformUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const params = Object.fromEntries(url.searchParams.entries());
|
||||
const query = exportQuerySchema.parse(params);
|
||||
|
||||
const context = await createAuditContext(request, session);
|
||||
|
||||
const timeRange = {
|
||||
start: new Date(query.startDate),
|
||||
end: new Date(query.endDate),
|
||||
};
|
||||
|
||||
let data: string;
|
||||
let filename: string;
|
||||
let contentType: string;
|
||||
|
||||
if (query.type === "alerts") {
|
||||
data = securityMonitoring.exportSecurityData(query.format, timeRange);
|
||||
filename = `security-alerts-${query.startDate.split("T")[0]}-to-${query.endDate.split("T")[0]}.${query.format}`;
|
||||
contentType = query.format === "csv" ? "text/csv" : "application/json";
|
||||
} else {
|
||||
// Export metrics
|
||||
const metrics = await securityMonitoring.getSecurityMetrics(timeRange);
|
||||
data = JSON.stringify(metrics, null, 2);
|
||||
filename = `security-metrics-${query.startDate.split("T")[0]}-to-${query.endDate.split("T")[0]}.json`;
|
||||
contentType = "application/json";
|
||||
}
|
||||
|
||||
// Log data export
|
||||
await securityAuditLogger.logPlatformAdmin(
|
||||
"security_data_export",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
undefined,
|
||||
{
|
||||
exportType: query.type,
|
||||
format: query.format,
|
||||
timeRange,
|
||||
dataSize: data.length,
|
||||
}
|
||||
);
|
||||
|
||||
const headers = new Headers({
|
||||
"Content-Type": contentType,
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
"Content-Length": data.length.toString(),
|
||||
});
|
||||
|
||||
return new NextResponse(data, { headers });
|
||||
} catch (error) {
|
||||
console.error("Security data export error:", error);
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid query parameters", details: error.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
169
app/api/admin/security-monitoring/route.ts
Normal file
169
app/api/admin/security-monitoring/route.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import {
|
||||
AuditOutcome,
|
||||
createAuditContext,
|
||||
securityAuditLogger,
|
||||
} from "@/lib/securityAuditLogger";
|
||||
import {
|
||||
type AlertSeverity,
|
||||
securityMonitoring,
|
||||
} from "@/lib/securityMonitoring";
|
||||
|
||||
const metricsQuerySchema = z.object({
|
||||
startDate: z.string().datetime().optional(),
|
||||
endDate: z.string().datetime().optional(),
|
||||
companyId: z.string().uuid().optional(),
|
||||
severity: z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]).optional(),
|
||||
});
|
||||
|
||||
const configUpdateSchema = z.object({
|
||||
thresholds: z
|
||||
.object({
|
||||
failedLoginsPerMinute: z.number().min(1).max(100).optional(),
|
||||
failedLoginsPerHour: z.number().min(1).max(1000).optional(),
|
||||
rateLimitViolationsPerMinute: z.number().min(1).max(100).optional(),
|
||||
cspViolationsPerMinute: z.number().min(1).max(100).optional(),
|
||||
adminActionsPerHour: z.number().min(1).max(100).optional(),
|
||||
massDataAccessThreshold: z.number().min(10).max(10000).optional(),
|
||||
suspiciousIPThreshold: z.number().min(1).max(100).optional(),
|
||||
})
|
||||
.optional(),
|
||||
alerting: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
channels: z
|
||||
.array(z.enum(["EMAIL", "WEBHOOK", "SLACK", "DISCORD", "PAGERDUTY"]))
|
||||
.optional(),
|
||||
suppressDuplicateMinutes: z.number().min(1).max(1440).optional(),
|
||||
escalationTimeoutMinutes: z.number().min(5).max(1440).optional(),
|
||||
})
|
||||
.optional(),
|
||||
retention: z
|
||||
.object({
|
||||
alertRetentionDays: z.number().min(1).max(3650).optional(),
|
||||
metricsRetentionDays: z.number().min(1).max(3650).optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Only platform admins can access security monitoring
|
||||
if (!session.user.isPlatformUser) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const params = Object.fromEntries(url.searchParams.entries());
|
||||
const query = metricsQuerySchema.parse(params);
|
||||
|
||||
const context = await createAuditContext(request, session);
|
||||
|
||||
const timeRange = {
|
||||
start: query.startDate
|
||||
? new Date(query.startDate)
|
||||
: new Date(Date.now() - 24 * 60 * 60 * 1000),
|
||||
end: query.endDate ? new Date(query.endDate) : new Date(),
|
||||
};
|
||||
|
||||
// Get security metrics
|
||||
const metrics = await securityMonitoring.getSecurityMetrics(
|
||||
timeRange,
|
||||
query.companyId
|
||||
);
|
||||
|
||||
// Get active alerts
|
||||
const alerts = securityMonitoring.getActiveAlerts(
|
||||
query.severity as AlertSeverity
|
||||
);
|
||||
|
||||
// Get monitoring configuration
|
||||
const config = securityMonitoring.getConfig();
|
||||
|
||||
// Log access to security monitoring
|
||||
await securityAuditLogger.logPlatformAdmin(
|
||||
"security_monitoring_access",
|
||||
AuditOutcome.SUCCESS,
|
||||
context
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
metrics,
|
||||
alerts,
|
||||
config,
|
||||
timeRange,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Security monitoring API error:", error);
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid query parameters", details: error.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!session.user.isPlatformUser) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const config = configUpdateSchema.parse(body);
|
||||
const context = await createAuditContext(request, session);
|
||||
|
||||
// Update monitoring configuration
|
||||
securityMonitoring.updateConfig(config);
|
||||
|
||||
// Log configuration change
|
||||
await securityAuditLogger.logPlatformAdmin(
|
||||
"security_monitoring_config_update",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
undefined,
|
||||
{ configChanges: config }
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
config: securityMonitoring.getConfig(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Security monitoring config update error:", error);
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid configuration", details: error.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
191
app/api/admin/security-monitoring/threat-analysis/route.ts
Normal file
191
app/api/admin/security-monitoring/threat-analysis/route.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import {
|
||||
AuditOutcome,
|
||||
createAuditContext,
|
||||
securityAuditLogger,
|
||||
} from "@/lib/securityAuditLogger";
|
||||
import { securityMonitoring, type SecurityMetrics, type AlertType } from "@/lib/securityMonitoring";
|
||||
|
||||
const threatAnalysisSchema = z.object({
|
||||
ipAddress: z.string().ip().optional(),
|
||||
userId: z.string().uuid().optional(),
|
||||
timeRange: z
|
||||
.object({
|
||||
start: z.string().datetime(),
|
||||
end: z.string().datetime(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user || !session.user.isPlatformUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const analysis = threatAnalysisSchema.parse(body);
|
||||
const context = await createAuditContext(request, session);
|
||||
|
||||
interface ThreatAnalysisResults {
|
||||
ipThreatAnalysis?: {
|
||||
ipAddress: string;
|
||||
threatLevel: number;
|
||||
isBlacklisted: boolean;
|
||||
riskFactors: string[];
|
||||
};
|
||||
timeRangeAnalysis?: {
|
||||
timeRange: { start: Date; end: Date };
|
||||
securityScore: number;
|
||||
threatLevel: string;
|
||||
topThreats: Array<{ type: AlertType; count: number }>;
|
||||
geoDistribution: Record<string, number>;
|
||||
riskUsers: Array<{ userId: string; email: string; riskScore: number }>;
|
||||
};
|
||||
overallThreatLandscape?: {
|
||||
currentThreatLevel: string;
|
||||
securityScore: number;
|
||||
activeAlerts: number;
|
||||
criticalEvents: number;
|
||||
recommendations: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const results: ThreatAnalysisResults = {};
|
||||
|
||||
// IP threat analysis
|
||||
if (analysis.ipAddress) {
|
||||
const ipThreat = await securityMonitoring.calculateIPThreatLevel(
|
||||
analysis.ipAddress
|
||||
);
|
||||
results.ipThreatAnalysis = {
|
||||
ipAddress: analysis.ipAddress,
|
||||
...ipThreat,
|
||||
};
|
||||
}
|
||||
|
||||
// Time-based analysis
|
||||
if (analysis.timeRange) {
|
||||
const timeRange = {
|
||||
start: new Date(analysis.timeRange.start),
|
||||
end: new Date(analysis.timeRange.end),
|
||||
};
|
||||
|
||||
const metrics = await securityMonitoring.getSecurityMetrics(timeRange);
|
||||
results.timeRangeAnalysis = {
|
||||
timeRange,
|
||||
securityScore: metrics.securityScore,
|
||||
threatLevel: metrics.threatLevel,
|
||||
topThreats: metrics.topThreats,
|
||||
geoDistribution: metrics.geoDistribution,
|
||||
riskUsers: metrics.userRiskScores.slice(0, 5),
|
||||
};
|
||||
}
|
||||
|
||||
// General threat landscape
|
||||
const defaultTimeRange = {
|
||||
start: new Date(Date.now() - 24 * 60 * 60 * 1000), // Last 24 hours
|
||||
end: new Date(),
|
||||
};
|
||||
|
||||
const overallMetrics =
|
||||
await securityMonitoring.getSecurityMetrics(defaultTimeRange);
|
||||
results.overallThreatLandscape = {
|
||||
currentThreatLevel: overallMetrics.threatLevel,
|
||||
securityScore: overallMetrics.securityScore,
|
||||
activeAlerts: overallMetrics.activeAlerts,
|
||||
criticalEvents: overallMetrics.criticalEvents,
|
||||
recommendations: generateThreatRecommendations(overallMetrics),
|
||||
};
|
||||
|
||||
// Log threat analysis request
|
||||
await securityAuditLogger.logPlatformAdmin(
|
||||
"threat_analysis_performed",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
undefined,
|
||||
{
|
||||
analysisType: Object.keys(analysis),
|
||||
threatLevel: results.overallThreatLandscape?.currentThreatLevel,
|
||||
}
|
||||
);
|
||||
|
||||
return NextResponse.json(results);
|
||||
} catch (error) {
|
||||
console.error("Threat analysis error:", error);
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid request", details: error.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function generateThreatRecommendations(metrics: SecurityMetrics): string[] {
|
||||
const recommendations: string[] = [];
|
||||
|
||||
if (metrics.securityScore < 70) {
|
||||
recommendations.push(
|
||||
"Security score is below acceptable threshold - immediate action required"
|
||||
);
|
||||
}
|
||||
|
||||
if (metrics.activeAlerts > 5) {
|
||||
recommendations.push(
|
||||
"High number of active alerts - prioritize alert resolution"
|
||||
);
|
||||
}
|
||||
|
||||
if (metrics.criticalEvents > 0) {
|
||||
recommendations.push(
|
||||
"Critical security events detected - investigate immediately"
|
||||
);
|
||||
}
|
||||
|
||||
const highRiskUsers = metrics.userRiskScores.filter(
|
||||
(user) => user.riskScore > 50
|
||||
);
|
||||
if (highRiskUsers.length > 0) {
|
||||
recommendations.push(
|
||||
`${highRiskUsers.length} users have elevated risk scores - review accounts`
|
||||
);
|
||||
}
|
||||
|
||||
// Check for geographic anomalies
|
||||
const countries = Object.keys(metrics.geoDistribution);
|
||||
if (countries.length > 10) {
|
||||
recommendations.push(
|
||||
"High geographic diversity detected - review for suspicious activity"
|
||||
);
|
||||
}
|
||||
|
||||
// Check for common attack patterns
|
||||
const bruteForceAlerts = metrics.topThreats.filter(
|
||||
(threat) => threat.type === "BRUTE_FORCE_ATTACK"
|
||||
);
|
||||
if (bruteForceAlerts.length > 0) {
|
||||
recommendations.push(
|
||||
"Brute force attacks detected - strengthen authentication controls"
|
||||
);
|
||||
}
|
||||
|
||||
if (recommendations.length === 0) {
|
||||
recommendations.push(
|
||||
"Security posture appears stable - continue monitoring"
|
||||
);
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
@ -87,7 +87,6 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
// Start processing (this will run asynchronously)
|
||||
const _startTime = Date.now();
|
||||
|
||||
// Note: We're calling the function but not awaiting it to avoid timeout
|
||||
// The processing will continue in the background
|
||||
|
||||
Reference in New Issue
Block a user