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:
2025-07-11 21:50:53 +02:00
committed by Kaj Kowalski
parent 3e9e75e854
commit 1eea2cc3e4
121 changed files with 28687 additions and 4895 deletions

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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;
}

View File

@ -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

View File

@ -0,0 +1,110 @@
import { type NextRequest, NextResponse } from "next/server";
import { cspMonitoring } from "@/lib/csp-monitoring";
import { rateLimiter } from "@/lib/rateLimiter";
export async function GET(request: NextRequest) {
try {
// Rate limiting for metrics endpoint
const ip =
request.ip || request.headers.get("x-forwarded-for") || "unknown";
const rateLimitResult = await rateLimiter.check(
`csp-metrics:${ip}`,
30, // 30 requests
60 * 1000 // per minute
);
if (!rateLimitResult.success) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
// Parse query parameters
const url = new URL(request.url);
const timeRange = url.searchParams.get("range") || "24h";
const format = url.searchParams.get("format") || "json";
// Calculate time range
const now = new Date();
let start: Date;
switch (timeRange) {
case "1h":
start = new Date(now.getTime() - 60 * 60 * 1000);
break;
case "6h":
start = new Date(now.getTime() - 6 * 60 * 60 * 1000);
break;
case "24h":
start = new Date(now.getTime() - 24 * 60 * 60 * 1000);
break;
case "7d":
start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case "30d":
start = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
break;
default:
start = new Date(now.getTime() - 24 * 60 * 60 * 1000);
}
// Get metrics from monitoring service
const metrics = cspMonitoring.getMetrics({ start, end: now });
// Get policy recommendations
const recommendations = cspMonitoring.generatePolicyRecommendations({
start,
end: now,
});
const response = {
timeRange: {
start: start.toISOString(),
end: now.toISOString(),
range: timeRange,
},
summary: {
totalViolations: metrics.totalViolations,
criticalViolations: metrics.criticalViolations,
bypassAttempts: metrics.bypassAttempts,
violationRate:
metrics.totalViolations /
((now.getTime() - start.getTime()) / (60 * 60 * 1000)), // per hour
},
topViolatedDirectives: metrics.topViolatedDirectives,
topBlockedUris: metrics.topBlockedUris,
violationTrends: metrics.violationTrends,
recommendations: recommendations,
lastUpdated: now.toISOString(),
};
// Export format handling
if (format === "csv") {
const csv = cspMonitoring.exportViolations("csv");
return new NextResponse(csv, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": `attachment; filename="csp-violations-${timeRange}.csv"`,
},
});
}
return NextResponse.json(response);
} catch (error) {
console.error("Error fetching CSP metrics:", error);
return NextResponse.json(
{ error: "Failed to fetch metrics" },
{ status: 500 }
);
}
}
// Handle preflight requests
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}

130
app/api/csp-report/route.ts Normal file
View File

@ -0,0 +1,130 @@
import { type NextRequest, NextResponse } from "next/server";
import {
type CSPViolationReport,
detectCSPBypass,
parseCSPViolation,
} from "@/lib/csp";
import { cspMonitoring } from "@/lib/csp-monitoring";
import { rateLimiter } from "@/lib/rateLimiter";
export async function POST(request: NextRequest) {
try {
// Rate limiting for CSP reports
const ip =
request.ip || request.headers.get("x-forwarded-for") || "unknown";
const rateLimitResult = await rateLimiter.check(
`csp-report:${ip}`,
10, // 10 reports
60 * 1000 // per minute
);
if (!rateLimitResult.success) {
return NextResponse.json(
{ error: "Too many CSP reports" },
{ status: 429 }
);
}
const contentType = request.headers.get("content-type");
if (
!contentType?.includes("application/csp-report") &&
!contentType?.includes("application/json")
) {
return NextResponse.json(
{ error: "Invalid content type" },
{ status: 400 }
);
}
const report: CSPViolationReport = await request.json();
if (!report["csp-report"]) {
return NextResponse.json(
{ error: "Invalid CSP report format" },
{ status: 400 }
);
}
// Process violation through monitoring service
const monitoringResult = await cspMonitoring.processViolation(
report,
ip,
request.headers.get("user-agent") || undefined
);
// Enhanced logging based on monitoring analysis
const logEntry = {
timestamp: new Date().toISOString(),
ip,
userAgent: request.headers.get("user-agent"),
violation: parseCSPViolation(report),
bypassDetection: detectCSPBypass(
report["csp-report"]["blocked-uri"] +
" " +
(report["csp-report"]["script-sample"] || "")
),
originalReport: report,
alertLevel: monitoringResult.alertLevel,
shouldAlert: monitoringResult.shouldAlert,
recommendations: monitoringResult.recommendations,
};
// In development, log to console with recommendations
if (process.env.NODE_ENV === "development") {
console.warn("🚨 CSP Violation Detected:", {
...logEntry,
recommendations: monitoringResult.recommendations,
});
if (monitoringResult.recommendations.length > 0) {
console.info("💡 Recommendations:", monitoringResult.recommendations);
}
}
// Enhanced alerting based on monitoring service analysis
if (monitoringResult.shouldAlert) {
const alertEmoji = {
low: "🟡",
medium: "🟠",
high: "🔴",
critical: "🚨",
}[monitoringResult.alertLevel];
console.error(
`${alertEmoji} CSP ${monitoringResult.alertLevel.toUpperCase()} ALERT:`,
{
directive: logEntry.violation.directive,
blockedUri: logEntry.violation.blockedUri,
isBypassAttempt: logEntry.bypassDetection.isDetected,
riskLevel: logEntry.bypassDetection.riskLevel,
recommendations: monitoringResult.recommendations.slice(0, 3), // Limit to 3 recommendations
}
);
}
// Clean up old violations periodically (every 100 requests)
if (Math.random() < 0.01) {
cspMonitoring.cleanupOldViolations();
}
return new NextResponse(null, { status: 204 });
} catch (error) {
console.error("Error processing CSP report:", error);
return NextResponse.json(
{ error: "Failed to process report" },
{ status: 500 }
);
}
}
// Handle preflight requests
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}

View File

@ -5,7 +5,7 @@
* It generates a new token and sets it as an HTTP-only cookie.
*/
import { NextRequest } from "next/server";
import type { NextRequest } from "next/server";
import { generateCSRFTokenResponse } from "../../../middleware/csrfProtection";
/**
@ -14,6 +14,6 @@ import { generateCSRFTokenResponse } from "../../../middleware/csrfProtection";
* Generates and returns a new CSRF token.
* The token is also set as an HTTP-only cookie for automatic inclusion in requests.
*/
export function GET(request: NextRequest) {
export function GET() {
return generateCSRFTokenResponse();
}
}

View File

@ -3,7 +3,7 @@ import { getServerSession } from "next-auth";
import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
export async function GET(_request: NextRequest) {
export async function GET() {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: "Not logged in" }, { status: 401 });

View File

@ -3,7 +3,7 @@ import { getServerSession } from "next-auth/next";
import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
export async function GET(_request: NextRequest) {
export async function GET() {
const authSession = await getServerSession(authOptions);
if (!authSession || !authSession.user?.companyId) {

View File

@ -11,7 +11,7 @@ interface UserBasicInfo {
role: string;
}
export async function GET(_request: NextRequest) {
export async function GET() {
const session = await getServerSession(authOptions);
if (!session?.user || session.user.role !== "ADMIN") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });

View File

@ -2,6 +2,11 @@ import crypto from "node:crypto";
import { type NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../lib/prisma";
import { extractClientIP, InMemoryRateLimiter } from "../../../lib/rateLimiter";
import {
AuditOutcome,
createAuditMetadata,
securityAuditLogger,
} from "../../../lib/securityAuditLogger";
import { sendEmail } from "../../../lib/sendEmail";
import { forgotPasswordSchema, validateInput } from "../../../lib/validation";
@ -17,9 +22,25 @@ export async function POST(request: NextRequest) {
try {
// Rate limiting check using shared utility
const ip = extractClientIP(request);
const userAgent = request.headers.get("user-agent") || undefined;
const rateLimitResult = passwordResetLimiter.checkRateLimit(ip);
if (!rateLimitResult.allowed) {
await securityAuditLogger.logPasswordReset(
"password_reset_rate_limited",
AuditOutcome.RATE_LIMITED,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
resetTime: rateLimitResult.resetTime,
maxAttempts: 5,
windowMs: 15 * 60 * 1000,
}),
},
"Password reset rate limit exceeded"
);
return NextResponse.json(
{
success: false,
@ -34,6 +55,19 @@ export async function POST(request: NextRequest) {
// Validate input
const validation = validateInput(forgotPasswordSchema, body);
if (!validation.success) {
await securityAuditLogger.logPasswordReset(
"password_reset_invalid_input",
AuditOutcome.FAILURE,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "invalid_email_format",
}),
},
"Invalid email format in password reset request"
);
return NextResponse.json(
{
success: false,
@ -65,11 +99,55 @@ export async function POST(request: NextRequest) {
subject: "Password Reset",
text: `Reset your password: ${resetUrl}`,
});
await securityAuditLogger.logPasswordReset(
"password_reset_email_sent",
AuditOutcome.SUCCESS,
{
userId: user.id,
companyId: user.companyId,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
email: "[REDACTED]",
tokenExpiry: expiry.toISOString(),
}),
},
"Password reset email sent successfully"
);
} else {
// Log attempt for non-existent user
await securityAuditLogger.logPasswordReset(
"password_reset_user_not_found",
AuditOutcome.FAILURE,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
email: "[REDACTED]",
}),
},
"Password reset attempt for non-existent user"
);
}
return NextResponse.json({ success: true }, { status: 200 });
} catch (error) {
console.error("Forgot password error:", error);
await securityAuditLogger.logPasswordReset(
"password_reset_server_error",
AuditOutcome.FAILURE,
{
ipAddress: extractClientIP(request),
userAgent: request.headers.get("user-agent") || undefined,
metadata: createAuditMetadata({
error: "server_error",
}),
},
`Server error in password reset: ${error}`
);
return NextResponse.json(
{
success: false,

View File

@ -73,14 +73,13 @@ export async function POST(
{ error: "User already exists in this company" },
{ status: 400 }
);
} else {
return NextResponse.json(
{
error: `Email already in use by a user in company: ${existingUser.company.name}. Each email address can only be used once across all companies.`
},
{ status: 400 }
);
}
return NextResponse.json(
{
error: `Email already in use by a user in company: ${existingUser.company.name}. Each email address can only be used once across all companies.`,
},
{ status: 400 }
);
}
// Generate a temporary password (in a real app, you'd send an invitation email)

View File

@ -3,13 +3,34 @@ import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { platformAuthOptions } from "../../../../lib/platform-auth";
import { prisma } from "../../../../lib/prisma";
import { extractClientIP } from "../../../../lib/rateLimiter";
import {
AuditOutcome,
createAuditMetadata,
securityAuditLogger,
} from "../../../../lib/securityAuditLogger";
// GET /api/platform/companies - List all companies
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(platformAuthOptions);
const ip = extractClientIP(request);
const userAgent = request.headers.get("user-agent") || undefined;
if (!session?.user?.isPlatformUser) {
await securityAuditLogger.logPlatformAdmin(
"platform_companies_unauthorized_access",
AuditOutcome.BLOCKED,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "no_platform_session",
}),
},
"Unauthorized attempt to access platform companies list"
);
return NextResponse.json(
{ error: "Platform access required" },
{ status: 401 }
@ -63,6 +84,24 @@ export async function GET(request: NextRequest) {
prisma.company.count({ where }),
]);
// Log successful platform companies access
await securityAuditLogger.logPlatformAdmin(
"platform_companies_list_accessed",
AuditOutcome.SUCCESS,
{
platformUserId: session.user.id,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
companiesReturned: companies.length,
totalCompanies: total,
filters: { status, search },
pagination: { page, limit },
}),
},
"Platform companies list accessed"
);
return NextResponse.json({
companies,
pagination: {
@ -74,6 +113,21 @@ export async function GET(request: NextRequest) {
});
} catch (error) {
console.error("Platform companies list error:", error);
await securityAuditLogger.logPlatformAdmin(
"platform_companies_list_error",
AuditOutcome.FAILURE,
{
platformUserId: session?.user?.id,
ipAddress: extractClientIP(request),
userAgent: request.headers.get("user-agent") || undefined,
metadata: createAuditMetadata({
error: "server_error",
}),
},
`Server error in platform companies list: ${error}`
);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
@ -85,11 +139,29 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(platformAuthOptions);
const ip = extractClientIP(request);
const userAgent = request.headers.get("user-agent") || undefined;
if (
!session?.user?.isPlatformUser ||
session.user.platformRole === "SUPPORT"
) {
await securityAuditLogger.logPlatformAdmin(
"platform_company_create_unauthorized",
AuditOutcome.BLOCKED,
{
platformUserId: session?.user?.id,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "insufficient_permissions",
requiredRole: "ADMIN",
currentRole: session?.user?.platformRole,
}),
},
"Unauthorized attempt to create platform company"
);
return NextResponse.json(
{ error: "Admin access required" },
{ status: 403 }
@ -165,6 +237,27 @@ export async function POST(request: NextRequest) {
};
});
// Log successful company creation
await securityAuditLogger.logCompanyManagement(
"platform_company_created",
AuditOutcome.SUCCESS,
{
platformUserId: session.user.id,
companyId: result.company.id,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
companyName: result.company.name,
companyStatus: result.company.status,
adminUserEmail: "[REDACTED]",
adminUserName: result.adminUser.name,
maxUsers: result.company.maxUsers,
hasGeneratedPassword: !!result.generatedPassword,
}),
},
"Platform company created successfully"
);
return NextResponse.json(
{
company: result.company,
@ -179,6 +272,21 @@ export async function POST(request: NextRequest) {
);
} catch (error) {
console.error("Platform company creation error:", error);
await securityAuditLogger.logCompanyManagement(
"platform_company_create_error",
AuditOutcome.FAILURE,
{
platformUserId: session?.user?.id,
ipAddress: extractClientIP(request),
userAgent: request.headers.get("user-agent") || undefined,
metadata: createAuditMetadata({
error: "server_error",
}),
},
`Server error in platform company creation: ${error}`
);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }

View File

@ -2,15 +2,37 @@ import crypto from "node:crypto";
import bcrypt from "bcryptjs";
import { type NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../lib/prisma";
import { extractClientIP } from "../../../lib/rateLimiter";
import {
AuditOutcome,
createAuditMetadata,
securityAuditLogger,
} from "../../../lib/securityAuditLogger";
import { resetPasswordSchema, validateInput } from "../../../lib/validation";
export async function POST(request: NextRequest) {
try {
const ip = extractClientIP(request);
const userAgent = request.headers.get("user-agent") || undefined;
const body = await request.json();
// Validate input with strong password requirements
const validation = validateInput(resetPasswordSchema, body);
if (!validation.success) {
await securityAuditLogger.logPasswordReset(
"password_reset_validation_failed",
AuditOutcome.FAILURE,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "validation_failed",
validationErrors: validation.errors,
}),
},
"Password reset validation failed"
);
return NextResponse.json(
{
success: false,
@ -34,6 +56,19 @@ export async function POST(request: NextRequest) {
});
if (!user) {
await securityAuditLogger.logPasswordReset(
"password_reset_invalid_token",
AuditOutcome.FAILURE,
{
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
error: "invalid_or_expired_token",
}),
},
"Password reset attempt with invalid or expired token"
);
return NextResponse.json(
{
success: false,
@ -56,6 +91,22 @@ export async function POST(request: NextRequest) {
},
});
await securityAuditLogger.logPasswordReset(
"password_reset_completed",
AuditOutcome.SUCCESS,
{
userId: user.id,
companyId: user.companyId,
ipAddress: ip,
userAgent,
metadata: createAuditMetadata({
email: "[REDACTED]",
passwordChanged: true,
}),
},
"Password reset completed successfully"
);
return NextResponse.json(
{
success: true,
@ -65,6 +116,20 @@ export async function POST(request: NextRequest) {
);
} catch (error) {
console.error("Reset password error:", error);
await securityAuditLogger.logPasswordReset(
"password_reset_server_error",
AuditOutcome.FAILURE,
{
ipAddress: extractClientIP(request),
userAgent: request.headers.get("user-agent") || undefined,
metadata: createAuditMetadata({
error: "server_error",
}),
},
`Server error in password reset completion: ${error}`
);
return NextResponse.json(
{
success: false,

View File

@ -0,0 +1,556 @@
"use client";
import { formatDistanceToNow } from "date-fns";
import { useSession } from "next-auth/react";
import { useEffect, useState, useCallback } from "react";
import { Alert, AlertDescription } from "../../../components/ui/alert";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "../../../components/ui/card";
import { Input } from "../../../components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../../components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../../components/ui/table";
interface AuditLog {
id: string;
eventType: string;
action: string;
outcome: string;
severity: string;
userId?: string;
platformUserId?: string;
ipAddress?: string;
userAgent?: string;
country?: string;
metadata?: Record<string, unknown>;
errorMessage?: string;
sessionId?: string;
requestId?: string;
timestamp: string;
user?: {
id: string;
email: string;
name?: string;
role: string;
};
platformUser?: {
id: string;
email: string;
name?: string;
role: string;
};
}
interface AuditLogsResponse {
success: boolean;
data?: {
auditLogs: AuditLog[];
pagination: {
page: number;
limit: number;
totalCount: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
};
error?: string;
}
const eventTypeLabels: Record<string, string> = {
AUTHENTICATION: "Authentication",
AUTHORIZATION: "Authorization",
USER_MANAGEMENT: "User Management",
COMPANY_MANAGEMENT: "Company Management",
RATE_LIMITING: "Rate Limiting",
CSRF_PROTECTION: "CSRF Protection",
SECURITY_HEADERS: "Security Headers",
PASSWORD_RESET: "Password Reset",
PLATFORM_ADMIN: "Platform Admin",
DATA_PRIVACY: "Data Privacy",
SYSTEM_CONFIG: "System Config",
API_SECURITY: "API Security",
};
const outcomeColors: Record<string, string> = {
SUCCESS: "bg-green-100 text-green-800",
FAILURE: "bg-red-100 text-red-800",
BLOCKED: "bg-orange-100 text-orange-800",
RATE_LIMITED: "bg-yellow-100 text-yellow-800",
SUSPICIOUS: "bg-purple-100 text-purple-800",
};
const severityColors: Record<string, string> = {
INFO: "bg-blue-100 text-blue-800",
LOW: "bg-gray-100 text-gray-800",
MEDIUM: "bg-yellow-100 text-yellow-800",
HIGH: "bg-orange-100 text-orange-800",
CRITICAL: "bg-red-100 text-red-800",
};
export default function AuditLogsPage() {
const { data: session } = useSession();
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [pagination, setPagination] = useState({
page: 1,
limit: 50,
totalCount: 0,
totalPages: 0,
hasNext: false,
hasPrev: false,
});
// Filter states
const [filters, setFilters] = useState({
eventType: "",
outcome: "",
severity: "",
userId: "",
startDate: "",
endDate: "",
});
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
const fetchAuditLogs = useCallback(async () => {
try {
setLoading(true);
const params = new URLSearchParams({
page: pagination.page.toString(),
limit: pagination.limit.toString(),
...filters,
});
Object.keys(filters).forEach((key) => {
if (!filters[key as keyof typeof filters]) {
params.delete(key);
}
});
const response = await fetch(
`/api/admin/audit-logs?${params.toString()}`
);
const data: AuditLogsResponse = await response.json();
if (data.success && data.data) {
setAuditLogs(data.data.auditLogs);
setPagination(data.data.pagination);
setError(null);
} else {
setError(data.error || "Failed to fetch audit logs");
}
} catch (err) {
setError("An error occurred while fetching audit logs");
console.error("Audit logs fetch error:", err);
} finally {
setLoading(false);
}
}, [pagination.page, pagination.limit, filters]);
useEffect(() => {
if (session?.user?.role === "ADMIN") {
fetchAuditLogs();
}
}, [session, fetchAuditLogs]);
const handleFilterChange = (key: keyof typeof filters, value: string) => {
setFilters((prev) => ({ ...prev, [key]: value }));
setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page
};
const clearFilters = () => {
setFilters({
eventType: "",
outcome: "",
severity: "",
userId: "",
startDate: "",
endDate: "",
});
};
if (session?.user?.role !== "ADMIN") {
return (
<div className="container mx-auto py-8">
<Alert>
<AlertDescription>
You don&apos;t have permission to view audit logs. Only administrators
can access this page.
</AlertDescription>
</Alert>
</div>
);
}
return (
<div className="container mx-auto py-8 space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold">Security Audit Logs</h1>
<Button onClick={fetchAuditLogs} disabled={loading}>
{loading ? "Loading..." : "Refresh"}
</Button>
</div>
{/* Filters */}
<Card>
<CardHeader>
<CardTitle>Filters</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium">Event Type</label>
<Select
value={filters.eventType}
onValueChange={(value) =>
handleFilterChange("eventType", value)
}
>
<SelectTrigger>
<SelectValue placeholder="All event types" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All event types</SelectItem>
{Object.entries(eventTypeLabels).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium">Outcome</label>
<Select
value={filters.outcome}
onValueChange={(value) => handleFilterChange("outcome", value)}
>
<SelectTrigger>
<SelectValue placeholder="All outcomes" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All outcomes</SelectItem>
<SelectItem value="SUCCESS">Success</SelectItem>
<SelectItem value="FAILURE">Failure</SelectItem>
<SelectItem value="BLOCKED">Blocked</SelectItem>
<SelectItem value="RATE_LIMITED">Rate Limited</SelectItem>
<SelectItem value="SUSPICIOUS">Suspicious</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium">Severity</label>
<Select
value={filters.severity}
onValueChange={(value) => handleFilterChange("severity", value)}
>
<SelectTrigger>
<SelectValue placeholder="All severities" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All severities</SelectItem>
<SelectItem value="INFO">Info</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="CRITICAL">Critical</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium">Start Date</label>
<Input
type="datetime-local"
value={filters.startDate}
onChange={(e) =>
handleFilterChange("startDate", e.target.value)
}
/>
</div>
<div>
<label className="text-sm font-medium">End Date</label>
<Input
type="datetime-local"
value={filters.endDate}
onChange={(e) => handleFilterChange("endDate", e.target.value)}
/>
</div>
<div className="flex items-end">
<Button variant="outline" onClick={clearFilters}>
Clear Filters
</Button>
</div>
</div>
</CardContent>
</Card>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Audit Logs Table */}
<Card>
<CardHeader>
<CardTitle>Audit Logs ({pagination.totalCount} total)</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Timestamp</TableHead>
<TableHead>Event Type</TableHead>
<TableHead>Action</TableHead>
<TableHead>Outcome</TableHead>
<TableHead>Severity</TableHead>
<TableHead>User</TableHead>
<TableHead>IP Address</TableHead>
<TableHead>Details</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{auditLogs.map((log) => (
<TableRow
key={log.id}
className="cursor-pointer hover:bg-gray-50"
onClick={() => setSelectedLog(log)}
>
<TableCell className="font-mono text-sm">
{formatDistanceToNow(new Date(log.timestamp), {
addSuffix: true,
})}
</TableCell>
<TableCell>
<Badge variant="outline">
{eventTypeLabels[log.eventType] || log.eventType}
</Badge>
</TableCell>
<TableCell className="max-w-48 truncate">
{log.action}
</TableCell>
<TableCell>
<Badge
className={
outcomeColors[log.outcome] ||
"bg-gray-100 text-gray-800"
}
>
{log.outcome}
</Badge>
</TableCell>
<TableCell>
<Badge
className={
severityColors[log.severity] ||
"bg-gray-100 text-gray-800"
}
>
{log.severity}
</Badge>
</TableCell>
<TableCell>
{log.user?.email || log.platformUser?.email || "System"}
</TableCell>
<TableCell className="font-mono text-sm">
{log.ipAddress || "N/A"}
</TableCell>
<TableCell>
<Button variant="ghost" size="sm">
View
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Pagination */}
<div className="flex justify-between items-center mt-4">
<div className="text-sm text-gray-600">
Showing {(pagination.page - 1) * pagination.limit + 1} to{" "}
{Math.min(
pagination.page * pagination.limit,
pagination.totalCount
)}{" "}
of {pagination.totalCount} results
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={!pagination.hasPrev}
onClick={() =>
setPagination((prev) => ({ ...prev, page: prev.page - 1 }))
}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
disabled={!pagination.hasNext}
onClick={() =>
setPagination((prev) => ({ ...prev, page: prev.page + 1 }))
}
>
Next
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Log Detail Modal */}
{selectedLog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-auto">
<div className="p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Audit Log Details</h2>
<Button variant="ghost" onClick={() => setSelectedLog(null)}>
×
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="font-medium">Timestamp:</label>
<p className="font-mono text-sm">
{new Date(selectedLog.timestamp).toLocaleString()}
</p>
</div>
<div>
<label className="font-medium">Event Type:</label>
<p>
{eventTypeLabels[selectedLog.eventType] ||
selectedLog.eventType}
</p>
</div>
<div>
<label className="font-medium">Action:</label>
<p>{selectedLog.action}</p>
</div>
<div>
<label className="font-medium">Outcome:</label>
<Badge className={outcomeColors[selectedLog.outcome]}>
{selectedLog.outcome}
</Badge>
</div>
<div>
<label className="font-medium">Severity:</label>
<Badge className={severityColors[selectedLog.severity]}>
{selectedLog.severity}
</Badge>
</div>
<div>
<label className="font-medium">IP Address:</label>
<p className="font-mono text-sm">
{selectedLog.ipAddress || "N/A"}
</p>
</div>
{selectedLog.user && (
<div>
<label className="font-medium">User:</label>
<p>
{selectedLog.user.email} ({selectedLog.user.role})
</p>
</div>
)}
{selectedLog.platformUser && (
<div>
<label className="font-medium">Platform User:</label>
<p>
{selectedLog.platformUser.email} (
{selectedLog.platformUser.role})
</p>
</div>
)}
{selectedLog.country && (
<div>
<label className="font-medium">Country:</label>
<p>{selectedLog.country}</p>
</div>
)}
{selectedLog.sessionId && (
<div>
<label className="font-medium">Session ID:</label>
<p className="font-mono text-sm">{selectedLog.sessionId}</p>
</div>
)}
{selectedLog.requestId && (
<div>
<label className="font-medium">Request ID:</label>
<p className="font-mono text-sm">{selectedLog.requestId}</p>
</div>
)}
</div>
{selectedLog.errorMessage && (
<div className="mt-4">
<label className="font-medium">Error Message:</label>
<p className="text-red-600 bg-red-50 p-2 rounded text-sm">
{selectedLog.errorMessage}
</p>
</div>
)}
{selectedLog.userAgent && (
<div className="mt-4">
<label className="font-medium">User Agent:</label>
<p className="text-sm break-all">{selectedLog.userAgent}</p>
</div>
)}
{selectedLog.metadata && (
<div className="mt-4">
<label className="font-medium">Metadata:</label>
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto max-h-40">
{JSON.stringify(selectedLog.metadata, null, 2)}
</pre>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -470,7 +470,7 @@ function DashboardContent() {
const { data: session, status } = useSession();
const router = useRouter();
const [metrics, setMetrics] = useState<MetricsResult | null>(null);
const [company, setCompany] = useState<Company | null>(null);
const [company, _setCompany] = useState<Company | null>(null);
const [refreshing, setRefreshing] = useState<boolean>(false);
const [isInitialLoad, setIsInitialLoad] = useState<boolean>(true);
@ -499,13 +499,20 @@ function DashboardContent() {
useEffect(() => {
if (overviewData) {
// Map overview data to metrics format expected by the component
const mappedMetrics = {
const mappedMetrics: Partial<MetricsResult> = {
totalSessions: overviewData.totalSessions,
avgMessagesSent: overviewData.avgMessagesSent,
avgSessionsPerDay: 0, // Will be computed properly later
avgSessionLength: null,
days: { data: [], labels: [] },
languages: { data: [], labels: [] },
categories: { data: [], labels: [] },
countries: { data: [], labels: [] },
belowThresholdCount: 0,
// Map the available data
sentimentDistribution: overviewData.sentimentDistribution,
categoryDistribution: overviewData.categoryDistribution,
};
setMetrics(mappedMetrics as any); // Type assertion for compatibility
setMetrics(mappedMetrics as MetricsResult);
if (isInitialLoad) {
setIsInitialLoad(false);

View File

@ -21,6 +21,8 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { formatCategory } from "@/lib/format-enums";
import { trpc } from "@/lib/trpc-client";
import { sessionFilterSchema } from "@/lib/validation";
import type { z } from "zod";
import type { ChatSession } from "../../../lib/types";
interface FilterOptions {
@ -30,21 +32,21 @@ interface FilterOptions {
interface FilterSectionProps {
filtersExpanded: boolean;
setFiltersExpanded: (expanded: boolean) => void;
setFiltersExpanded: (_expanded: boolean) => void;
searchTerm: string;
setSearchTerm: (term: string) => void;
setSearchTerm: (_term: string) => void;
selectedCategory: string;
setSelectedCategory: (category: string) => void;
setSelectedCategory: (_category: string) => void;
selectedLanguage: string;
setSelectedLanguage: (language: string) => void;
setSelectedLanguage: (_language: string) => void;
startDate: string;
setStartDate: (date: string) => void;
setStartDate: (_date: string) => void;
endDate: string;
setEndDate: (date: string) => void;
setEndDate: (_date: string) => void;
sortKey: string;
setSortKey: (key: string) => void;
setSortKey: (_key: string) => void;
sortOrder: string;
setSortOrder: (order: string) => void;
setSortOrder: (_order: string) => void;
filterOptions: FilterOptions;
searchHeadingId: string;
filtersHeadingId: string;
@ -305,7 +307,7 @@ function SessionList({
)}
{!loading && !error && sessions.length > 0 && (
<ul className="space-y-4" role="list">
<ul className="space-y-4">
{sessions.map((session) => (
<li key={session.id}>
<Card>
@ -316,7 +318,7 @@ function SessionList({
<h3 className="font-medium text-base mb-1">
Session{" "}
{session.sessionId ||
session.id.substring(0, 8) + "..."}
`${session.id.substring(0, 8)}...`}
</h3>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
@ -382,7 +384,7 @@ function SessionList({
interface PaginationProps {
currentPage: number;
totalPages: number;
setCurrentPage: (page: number | ((prev: number) => number)) => void;
setCurrentPage: (_page: number | ((_prev: number) => number)) => void;
}
function Pagination({
@ -487,7 +489,7 @@ export default function SessionsPage() {
} = trpc.dashboard.getSessions.useQuery(
{
search: debouncedSearchTerm || undefined,
category: (selectedCategory as any) || undefined,
category: selectedCategory ? selectedCategory as z.infer<typeof sessionFilterSchema>["category"] : undefined,
// language: selectedLanguage || undefined, // Not supported in schema yet
startDate: startDate || undefined,
endDate: endDate || undefined,
@ -505,7 +507,7 @@ export default function SessionsPage() {
// Update state when data changes
useEffect(() => {
if (sessionsData) {
setSessions((sessionsData.sessions as any) || []);
setSessions(sessionsData.sessions || []);
setTotalPages(sessionsData.pagination.totalPages);
setError(null);
}

View File

@ -2,6 +2,8 @@
import "./globals.css";
import type { ReactNode } from "react";
import { Toaster } from "@/components/ui/sonner";
import { NonceProvider } from "@/lib/nonce-context";
import { getNonce } from "@/lib/nonce-utils";
import { Providers } from "./providers";
export const metadata = {
@ -88,7 +90,13 @@ export const metadata = {
},
};
export default function RootLayout({ children }: { children: ReactNode }) {
export default async function RootLayout({
children,
}: {
children: ReactNode;
}) {
const nonce = await getNonce();
const jsonLd = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
@ -126,7 +134,8 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<head>
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: Safe use for JSON-LD structured data
nonce={nonce}
// biome-ignore lint/security/noDangerouslySetInnerHtml: Safe use for JSON-LD structured data with CSP nonce
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
@ -138,7 +147,9 @@ export default function RootLayout({ children }: { children: ReactNode }) {
>
Skip to main content
</a>
<Providers>{children}</Providers>
<NonceProvider nonce={nonce}>
<Providers>{children}</Providers>
</NonceProvider>
<Toaster />
</body>
</html>

View File

@ -12,6 +12,7 @@ import {
Plus,
Search,
Settings,
Shield,
User,
Users,
} from "lucide-react";
@ -234,7 +235,7 @@ export default function PlatformDashboard() {
description: (
<div className="space-y-3">
<p className="font-medium">
Company "{companyName}" has been created.
Company &quot;{companyName}&quot; has been created.
</p>
<div className="space-y-2">
<div className="flex items-center justify-between bg-muted p-2 rounded">
@ -366,6 +367,15 @@ export default function PlatformDashboard() {
</p>
</div>
<div className="flex gap-4 items-center">
<Button
variant="outline"
size="sm"
onClick={() => router.push("/platform/security")}
>
<Shield className="w-4 h-4 mr-2" />
Security Monitoring
</Button>
<ThemeToggle />
{/* Search Filter */}
@ -491,7 +501,7 @@ export default function PlatformDashboard() {
<div className="flex items-center gap-2">
{searchTerm && (
<Badge variant="outline" className="text-xs">
Search: "{searchTerm}"
Search: &quot;{searchTerm}&quot;
</Badge>
)}
<Dialog open={showAddCompany} onOpenChange={setShowAddCompany}>
@ -693,7 +703,7 @@ export default function PlatformDashboard() {
<div className="text-center py-8 text-muted-foreground">
{searchTerm ? (
<div className="space-y-2">
<p>No companies match "{searchTerm}".</p>
<p>No companies match &quot;{searchTerm}&quot;.</p>
<Button
variant="link"
onClick={() => setSearchTerm("")}

View File

@ -0,0 +1,528 @@
"use client";
import {
Activity,
AlertTriangle,
Bell,
BellOff,
CheckCircle,
Download,
Settings,
Shield,
} from "lucide-react";
import { useEffect, useState, useCallback } from "react";
import { SecurityConfigModal } from "@/components/security/SecurityConfigModal";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
interface SecurityMetrics {
totalEvents: number;
criticalEvents: number;
activeAlerts: number;
resolvedAlerts: number;
securityScore: number;
threatLevel: string;
eventsByType: Record<string, number>;
alertsByType: Record<string, number>;
topThreats: Array<{ type: string; count: number }>;
geoDistribution: Record<string, number>;
timeDistribution: Array<{ hour: number; count: number }>;
userRiskScores: Array<{ userId: string; email: string; riskScore: number }>;
}
interface SecurityAlert {
id: string;
timestamp: string;
severity: string;
type: string;
title: string;
description: string;
eventType: string;
context: Record<string, unknown>;
metadata: Record<string, unknown>;
acknowledged: boolean;
}
export default function SecurityMonitoringPage() {
const [metrics, setMetrics] = useState<SecurityMetrics | null>(null);
const [alerts, setAlerts] = useState<SecurityAlert[]>([]);
const [loading, setLoading] = useState(true);
const [selectedTimeRange, setSelectedTimeRange] = useState("24h");
const [showConfig, setShowConfig] = useState(false);
const [autoRefresh, setAutoRefresh] = useState(true);
useEffect(() => {
loadSecurityData();
if (autoRefresh) {
const interval = setInterval(loadSecurityData, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}
}, [autoRefresh, loadSecurityData]);
const loadSecurityData = useCallback(async () => {
try {
const startDate = getStartDateForRange(selectedTimeRange);
const endDate = new Date().toISOString();
const response = await fetch(
`/api/admin/security-monitoring?startDate=${startDate}&endDate=${endDate}`
);
if (!response.ok) throw new Error("Failed to load security data");
const data = await response.json();
setMetrics(data.metrics);
setAlerts(data.alerts);
} catch (error) {
console.error("Error loading security data:", error);
} finally {
setLoading(false);
}
}, [selectedTimeRange]);
const acknowledgeAlert = async (alertId: string) => {
try {
const response = await fetch("/api/admin/security-monitoring/alerts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ alertId, action: "acknowledge" }),
});
if (response.ok) {
setAlerts(
alerts.map((alert) =>
alert.id === alertId ? { ...alert, acknowledged: true } : alert
)
);
}
} catch (error) {
console.error("Error acknowledging alert:", error);
}
};
const exportData = async (
format: "json" | "csv",
type: "alerts" | "metrics"
) => {
try {
const startDate = getStartDateForRange(selectedTimeRange);
const endDate = new Date().toISOString();
const response = await fetch(
`/api/admin/security-monitoring/export?format=${format}&type=${type}&startDate=${startDate}&endDate=${endDate}`
);
if (!response.ok) throw new Error("Export failed");
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `security-${type}-${new Date().toISOString().split("T")[0]}.${format}`;
a.click();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error("Error exporting data:", error);
}
};
const getStartDateForRange = (range: string): string => {
const now = new Date();
switch (range) {
case "1h":
return new Date(now.getTime() - 60 * 60 * 1000).toISOString();
case "24h":
return new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
case "7d":
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
case "30d":
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
default:
return new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
}
};
const getThreatLevelColor = (level: string) => {
switch (level?.toLowerCase()) {
case "critical":
return "bg-red-500";
case "high":
return "bg-orange-500";
case "moderate":
return "bg-yellow-500";
case "low":
return "bg-green-500";
default:
return "bg-gray-500";
}
};
const getSeverityColor = (severity: string) => {
switch (severity?.toLowerCase()) {
case "critical":
return "destructive";
case "high":
return "destructive";
case "medium":
return "secondary";
case "low":
return "outline";
default:
return "outline";
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900" />
</div>
);
}
return (
<div className="container mx-auto px-4 py-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">
Security Monitoring
</h1>
<p className="text-muted-foreground">
Real-time security monitoring and threat detection
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setAutoRefresh(!autoRefresh)}
>
{autoRefresh ? (
<Bell className="h-4 w-4" />
) : (
<BellOff className="h-4 w-4" />
)}
Auto Refresh
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setShowConfig(true)}
>
<Settings className="h-4 w-4" />
Configure
</Button>
<Button
variant="outline"
size="sm"
onClick={() => exportData("json", "alerts")}
>
<Download className="h-4 w-4" />
Export
</Button>
</div>
</div>
{/* Time Range Selector */}
<div className="flex gap-2">
{["1h", "24h", "7d", "30d"].map((range) => (
<Button
key={range}
variant={selectedTimeRange === range ? "default" : "outline"}
size="sm"
onClick={() => setSelectedTimeRange(range)}
>
{range}
</Button>
))}
</div>
{/* Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Security Score
</CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{metrics?.securityScore || 0}/100
</div>
<div
className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${getThreatLevelColor(metrics?.threatLevel || "")}`}
>
{metrics?.threatLevel || "Unknown"} Threat Level
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Alerts</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{metrics?.activeAlerts || 0}
</div>
<p className="text-xs text-muted-foreground">
{metrics?.resolvedAlerts || 0} resolved
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Security Events
</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{metrics?.totalEvents || 0}
</div>
<p className="text-xs text-muted-foreground">
{metrics?.criticalEvents || 0} critical
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Top Threat</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-sm font-bold">
{metrics?.topThreats?.[0]?.type?.replace(/_/g, " ") || "None"}
</div>
<p className="text-xs text-muted-foreground">
{metrics?.topThreats?.[0]?.count || 0} instances
</p>
</CardContent>
</Card>
</div>
<Tabs defaultValue="alerts" className="space-y-4">
<TabsList>
<TabsTrigger value="alerts">Active Alerts</TabsTrigger>
<TabsTrigger value="metrics">Security Metrics</TabsTrigger>
<TabsTrigger value="threats">Threat Analysis</TabsTrigger>
<TabsTrigger value="geography">Geographic View</TabsTrigger>
</TabsList>
<TabsContent value="alerts" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Active Security Alerts</CardTitle>
<CardDescription>
Real-time security alerts requiring attention
</CardDescription>
</CardHeader>
<CardContent>
{alerts.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<CheckCircle className="h-12 w-12 mx-auto mb-4" />
<p>No active alerts - system is secure</p>
</div>
) : (
<div className="space-y-4">
{alerts.map((alert) => (
<div
key={alert.id}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<Badge variant={getSeverityColor(alert.severity)}>
{alert.severity}
</Badge>
<span className="font-medium">{alert.title}</span>
</div>
<p className="text-sm text-muted-foreground">
{alert.description}
</p>
<p className="text-xs text-muted-foreground">
{new Date(alert.timestamp).toLocaleString()}
</p>
</div>
{!alert.acknowledged && (
<Button
size="sm"
onClick={() => acknowledgeAlert(alert.id)}
>
Acknowledge
</Button>
)}
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="metrics" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Event Distribution</CardTitle>
</CardHeader>
<CardContent>
{metrics?.eventsByType && (
<div className="space-y-2">
{Object.entries(metrics.eventsByType).map(
([type, count]) => (
<div key={type} className="flex justify-between">
<span className="text-sm">
{type.replace(/_/g, " ")}
</span>
<span className="font-medium">{count}</span>
</div>
)
)}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>High-Risk Users</CardTitle>
</CardHeader>
<CardContent>
{metrics?.userRiskScores?.length ? (
<div className="space-y-2">
{metrics.userRiskScores.slice(0, 5).map((user) => (
<div key={user.userId} className="flex justify-between">
<span className="text-sm truncate">{user.email}</span>
<Badge
variant={
user.riskScore > 70
? "destructive"
: user.riskScore > 40
? "secondary"
: "outline"
}
>
{user.riskScore}
</Badge>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">
No high-risk users detected
</p>
)}
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="threats" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Threat Analysis</CardTitle>
<CardDescription>
Analysis of current security threats and recommendations
</CardDescription>
</CardHeader>
<CardContent>
{metrics?.topThreats?.length ? (
<div className="space-y-4">
{metrics.topThreats.map((threat, index) => (
<div
key={threat.type}
className="flex items-center justify-between p-3 border rounded"
>
<div>
<span className="font-medium">
{threat.type.replace(/_/g, " ")}
</span>
<p className="text-sm text-muted-foreground">
{threat.count} occurrences
</p>
</div>
<Badge
variant={index === 0 ? "destructive" : "secondary"}
>
{index === 0 ? "Highest Priority" : "Monitor"}
</Badge>
</div>
))}
</div>
) : (
<p className="text-center py-8 text-muted-foreground">
No significant threats detected
</p>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="geography" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Geographic Distribution</CardTitle>
<CardDescription>
Security events by geographic location
</CardDescription>
</CardHeader>
<CardContent>
{metrics?.geoDistribution &&
Object.keys(metrics.geoDistribution).length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{Object.entries(metrics.geoDistribution)
.sort(([, a], [, b]) => b - a)
.slice(0, 12)
.map(([country, count]) => (
<div
key={country}
className="text-center p-3 border rounded"
>
<div className="text-2xl font-bold">{count}</div>
<div className="text-sm text-muted-foreground">
{country}
</div>
</div>
))}
</div>
) : (
<p className="text-center py-8 text-muted-foreground">
No geographic data available
</p>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
{showConfig && (
<SecurityConfigModal
onClose={() => setShowConfig(false)}
onSave={() => {
setShowConfig(false);
loadSecurityData();
}}
/>
)}
</div>
);
}

View File

@ -18,7 +18,15 @@ import { useToast } from "@/hooks/use-toast";
// Platform session hook - same as in dashboard
function usePlatformSession() {
const [session, setSession] = useState<any>(null);
const [session, setSession] = useState<{
user: {
id: string;
email: string;
name?: string;
role: string;
companyId?: string;
};
} | null>(null);
const [status, setStatus] = useState<
"loading" | "authenticated" | "unauthenticated"
>("loading");
@ -89,7 +97,7 @@ export default function PlatformSettings() {
title: "Profile Updated",
description: "Your profile has been updated successfully.",
});
} catch (error) {
} catch (_error) {
toast({
title: "Error",
description: "Failed to update profile. Please try again.",
@ -134,7 +142,7 @@ export default function PlatformSettings() {
newPassword: "",
confirmPassword: "",
});
} catch (error) {
} catch (_error) {
toast({
title: "Error",
description: "Failed to change password. Please try again.",

View File

@ -2,9 +2,9 @@
import { SessionProvider } from "next-auth/react";
import type { ReactNode } from "react";
import { CSRFProvider } from "@/components/providers/CSRFProvider";
import { TRPCProvider } from "@/components/providers/TRPCProvider";
import { ThemeProvider } from "@/components/theme-provider";
import { CSRFProvider } from "@/components/providers/CSRFProvider";
export function Providers({ children }: { children: ReactNode }) {
// Including error handling and refetch interval for better user experience