mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 16:52:08 +01:00
feat: implement cache layer, CSP improvements, and database performance optimizations
- Add Redis cache implementation with LRU eviction - Enhance Content Security Policy with nonce generation - Optimize database queries with connection pooling - Add cache invalidation API endpoints - Improve security monitoring performance
This commit is contained in:
@ -16,8 +16,9 @@ import {
|
||||
|
||||
// GET /api/admin/audit-logs/retention - Get retention statistics and policy status
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const ip = extractClientIP(request);
|
||||
const userAgent = request.headers.get("user-agent") || undefined;
|
||||
|
||||
@ -127,8 +128,9 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// POST /api/admin/audit-logs/retention - Execute retention policies manually
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const ip = extractClientIP(request);
|
||||
const userAgent = request.headers.get("user-agent") || undefined;
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
* Validates user authorization for audit logs access
|
||||
*/
|
||||
async function validateAuditLogAccess(
|
||||
session: { user?: { id: string; companyId: string; role: string } } | null,
|
||||
session: { user?: { id?: string; companyId?: string; role?: string } } | null,
|
||||
ip: string,
|
||||
userAgent?: string
|
||||
) {
|
||||
@ -33,17 +33,17 @@ async function validateAuditLogAccess(
|
||||
return { valid: false, status: 401, error: "Unauthorized" };
|
||||
}
|
||||
|
||||
if (session.user.role !== "ADMIN") {
|
||||
if (session?.user?.role !== "ADMIN") {
|
||||
await securityAuditLogger.logAuthorization(
|
||||
"audit_logs_insufficient_permissions",
|
||||
AuditOutcome.BLOCKED,
|
||||
{
|
||||
userId: session.user.id,
|
||||
companyId: session.user.companyId,
|
||||
userId: session?.user?.id,
|
||||
companyId: session?.user?.companyId,
|
||||
ipAddress: ip,
|
||||
userAgent,
|
||||
metadata: createAuditMetadata({
|
||||
userRole: session.user.role,
|
||||
userRole: session?.user?.role,
|
||||
requiredRole: "ADMIN",
|
||||
}),
|
||||
},
|
||||
@ -121,8 +121,9 @@ function buildAuditLogWhereClause(
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const ip = extractClientIP(request);
|
||||
const userAgent = request.headers.get("user-agent") || undefined;
|
||||
|
||||
@ -137,11 +138,23 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const url = new URL(request.url);
|
||||
const filters = parseAuditLogFilters(url);
|
||||
const { page, limit } = filters;
|
||||
const {
|
||||
page,
|
||||
limit,
|
||||
eventType,
|
||||
outcome,
|
||||
severity,
|
||||
userId,
|
||||
startDate,
|
||||
endDate,
|
||||
} = filters;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build filter conditions
|
||||
const where = buildAuditLogWhereClause(session.user.companyId, filters);
|
||||
const where = buildAuditLogWhereClause(
|
||||
session?.user?.companyId || "",
|
||||
filters
|
||||
);
|
||||
|
||||
// Get audit logs with pagination
|
||||
const [auditLogs, totalCount] = await Promise.all([
|
||||
@ -177,8 +190,8 @@ export async function GET(request: NextRequest) {
|
||||
"audit_logs_accessed",
|
||||
AuditOutcome.SUCCESS,
|
||||
{
|
||||
userId: session.user.id,
|
||||
companyId: session.user.companyId,
|
||||
userId: session?.user?.id,
|
||||
companyId: session?.user?.companyId,
|
||||
ipAddress: ip,
|
||||
userAgent,
|
||||
metadata: createAuditMetadata({
|
||||
|
||||
230
app/api/admin/cache/invalidate/route.ts
vendored
Normal file
230
app/api/admin/cache/invalidate/route.ts
vendored
Normal file
@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Cache Invalidation API Endpoint
|
||||
*
|
||||
* Allows administrators to manually invalidate cache entries or patterns
|
||||
* for troubleshooting and cache management.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { authOptions } from "../../../../../lib/auth";
|
||||
import { invalidateCompanyCache } from "../../../../../lib/batchProcessorOptimized";
|
||||
import { Cache } from "../../../../../lib/cache";
|
||||
import {
|
||||
AuditOutcome,
|
||||
AuditSeverity,
|
||||
createAuditMetadata,
|
||||
SecurityEventType,
|
||||
} from "../../../../../lib/securityAuditLogger";
|
||||
import { enhancedSecurityLog } from "../../../../../lib/securityMonitoring";
|
||||
|
||||
const invalidationSchema = z.object({
|
||||
type: z.enum(["key", "pattern", "company", "user", "all"]),
|
||||
value: z.string().optional(),
|
||||
});
|
||||
|
||||
async function validateCacheAccess(
|
||||
session: { user?: { id?: string; companyId?: string; role?: string } } | null
|
||||
) {
|
||||
if (!session?.user) {
|
||||
await enhancedSecurityLog(
|
||||
SecurityEventType.AUTHORIZATION,
|
||||
"cache_invalidation_access_denied",
|
||||
AuditOutcome.BLOCKED,
|
||||
{
|
||||
metadata: createAuditMetadata({
|
||||
endpoint: "/api/admin/cache/invalidate",
|
||||
reason: "not_authenticated",
|
||||
}),
|
||||
},
|
||||
AuditSeverity.MEDIUM,
|
||||
"Unauthenticated access attempt to cache invalidation endpoint"
|
||||
);
|
||||
return { valid: false, status: 401, error: "Authentication required" };
|
||||
}
|
||||
|
||||
if (session.user.role !== "ADMIN") {
|
||||
await enhancedSecurityLog(
|
||||
SecurityEventType.AUTHORIZATION,
|
||||
"cache_invalidation_access_denied",
|
||||
AuditOutcome.BLOCKED,
|
||||
{
|
||||
userId: session.user.id,
|
||||
companyId: session.user.companyId,
|
||||
metadata: createAuditMetadata({
|
||||
endpoint: "/api/admin/cache/invalidate",
|
||||
userRole: session.user.role,
|
||||
reason: "insufficient_privileges",
|
||||
}),
|
||||
},
|
||||
AuditSeverity.HIGH,
|
||||
"Non-admin user attempted to access cache invalidation"
|
||||
);
|
||||
return { valid: false, status: 403, error: "Admin access required" };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
async function performCacheInvalidation(type: string, value?: string) {
|
||||
let deletedCount = 0;
|
||||
let operation = "";
|
||||
|
||||
switch (type) {
|
||||
case "key": {
|
||||
if (!value) {
|
||||
return {
|
||||
error: "Key value required for key invalidation",
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
const deleted = await Cache.delete(value);
|
||||
deletedCount = deleted ? 1 : 0;
|
||||
operation = `key: ${value}`;
|
||||
break;
|
||||
}
|
||||
case "pattern": {
|
||||
if (!value) {
|
||||
return {
|
||||
error: "Pattern value required for pattern invalidation",
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
deletedCount = await Cache.invalidatePattern(value);
|
||||
operation = `pattern: ${value}`;
|
||||
break;
|
||||
}
|
||||
case "company": {
|
||||
if (!value) {
|
||||
return {
|
||||
error: "Company ID required for company invalidation",
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
deletedCount = await Cache.invalidateCompany(value);
|
||||
await invalidateCompanyCache();
|
||||
operation = `company: ${value}`;
|
||||
break;
|
||||
}
|
||||
case "user": {
|
||||
if (!value) {
|
||||
return { error: "User ID required for user invalidation", status: 400 };
|
||||
}
|
||||
await Cache.invalidateUser(value);
|
||||
await Cache.invalidatePattern("user:email:*");
|
||||
deletedCount = 1;
|
||||
operation = `user: ${value}`;
|
||||
break;
|
||||
}
|
||||
case "all": {
|
||||
await Promise.all([
|
||||
Cache.invalidatePattern("user:*"),
|
||||
Cache.invalidatePattern("company:*"),
|
||||
Cache.invalidatePattern("session:*"),
|
||||
Cache.invalidatePattern("*"),
|
||||
invalidateCompanyCache(),
|
||||
]);
|
||||
deletedCount = 1;
|
||||
operation = "all caches";
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return { error: "Invalid invalidation type", status: 400 };
|
||||
}
|
||||
|
||||
return { success: true, deletedCount, operation };
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
const authResult = await validateCacheAccess(session);
|
||||
if (!authResult.valid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: authResult.error },
|
||||
{ status: authResult.status }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const validation = invalidationSchema.safeParse(body);
|
||||
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Invalid request format",
|
||||
details: validation.error.issues,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { type, value } = validation.data;
|
||||
const result = await performCacheInvalidation(type, value);
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: result.error },
|
||||
{ status: result.status }
|
||||
);
|
||||
}
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
data: {
|
||||
type,
|
||||
value,
|
||||
deletedCount: result.deletedCount,
|
||||
operation: result.operation,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
await enhancedSecurityLog(
|
||||
SecurityEventType.PLATFORM_ADMIN,
|
||||
"cache_invalidation_executed",
|
||||
AuditOutcome.SUCCESS,
|
||||
{
|
||||
userId: session?.user?.id,
|
||||
companyId: session?.user?.companyId,
|
||||
metadata: createAuditMetadata({
|
||||
endpoint: "/api/admin/cache/invalidate",
|
||||
invalidationType: type,
|
||||
invalidationValue: value,
|
||||
deletedCount: result.deletedCount,
|
||||
}),
|
||||
},
|
||||
AuditSeverity.MEDIUM,
|
||||
`Cache invalidation executed: ${result.operation}`
|
||||
);
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error("[Cache Invalidation API] Error:", error);
|
||||
|
||||
await enhancedSecurityLog(
|
||||
SecurityEventType.API_SECURITY,
|
||||
"cache_invalidation_error",
|
||||
AuditOutcome.FAILURE,
|
||||
{
|
||||
metadata: createAuditMetadata({
|
||||
endpoint: "/api/admin/cache/invalidate",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}),
|
||||
},
|
||||
AuditSeverity.HIGH,
|
||||
"Cache invalidation API encountered an error"
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Internal server error",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
157
app/api/admin/cache/stats/route.ts
vendored
Normal file
157
app/api/admin/cache/stats/route.ts
vendored
Normal file
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Cache Statistics API Endpoint
|
||||
*
|
||||
* Provides comprehensive cache performance metrics and health status
|
||||
* for monitoring Redis + in-memory cache performance.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "../../../../../lib/auth";
|
||||
import { Cache } from "../../../../../lib/cache";
|
||||
import {
|
||||
AuditOutcome,
|
||||
AuditSeverity,
|
||||
createAuditMetadata,
|
||||
SecurityEventType,
|
||||
} from "../../../../../lib/securityAuditLogger";
|
||||
import { enhancedSecurityLog } from "../../../../../lib/securityMonitoring";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user) {
|
||||
await enhancedSecurityLog(
|
||||
SecurityEventType.AUTHORIZATION,
|
||||
"cache_stats_access_denied",
|
||||
AuditOutcome.BLOCKED,
|
||||
{
|
||||
metadata: createAuditMetadata({
|
||||
endpoint: "/api/admin/cache/stats",
|
||||
reason: "not_authenticated",
|
||||
}),
|
||||
},
|
||||
AuditSeverity.MEDIUM,
|
||||
"Unauthenticated access attempt to cache stats endpoint"
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Authentication required" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
if (session.user.role !== "ADMIN") {
|
||||
await enhancedSecurityLog(
|
||||
SecurityEventType.AUTHORIZATION,
|
||||
"cache_stats_access_denied",
|
||||
AuditOutcome.BLOCKED,
|
||||
{
|
||||
userId: session.user.id,
|
||||
companyId: session.user.companyId,
|
||||
metadata: createAuditMetadata({
|
||||
endpoint: "/api/admin/cache/stats",
|
||||
userRole: session.user.role,
|
||||
reason: "insufficient_privileges",
|
||||
}),
|
||||
},
|
||||
AuditSeverity.HIGH,
|
||||
"Non-admin user attempted to access cache stats"
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Admin access required" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get cache statistics and health information
|
||||
const [stats, healthCheck] = await Promise.all([
|
||||
Cache.getStats(),
|
||||
Cache.healthCheck(),
|
||||
]);
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
data: {
|
||||
performance: {
|
||||
hits: stats.hits,
|
||||
misses: stats.misses,
|
||||
sets: stats.sets,
|
||||
deletes: stats.deletes,
|
||||
errors: stats.errors,
|
||||
hitRate: Number((stats.hitRate * 100).toFixed(2)), // Convert to percentage
|
||||
redisHits: stats.redisHits,
|
||||
memoryHits: stats.memoryHits,
|
||||
},
|
||||
health: {
|
||||
redis: {
|
||||
connected: healthCheck.redis.connected,
|
||||
latency: healthCheck.redis.latency,
|
||||
error: healthCheck.redis.error,
|
||||
},
|
||||
memory: {
|
||||
available: healthCheck.memory.available,
|
||||
size: healthCheck.memory.size,
|
||||
valid: healthCheck.memory.valid,
|
||||
expired: healthCheck.memory.expired,
|
||||
},
|
||||
overall: {
|
||||
available: healthCheck.overall.available,
|
||||
fallbackMode: healthCheck.overall.fallbackMode,
|
||||
},
|
||||
},
|
||||
configuration: {
|
||||
redisAvailable: stats.redisAvailable,
|
||||
fallbackActive: !stats.redisAvailable,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
// Log successful access
|
||||
await enhancedSecurityLog(
|
||||
SecurityEventType.PLATFORM_ADMIN,
|
||||
"cache_stats_accessed",
|
||||
AuditOutcome.SUCCESS,
|
||||
{
|
||||
userId: session.user.id,
|
||||
companyId: session.user.companyId,
|
||||
metadata: createAuditMetadata({
|
||||
endpoint: "/api/admin/cache/stats",
|
||||
hitRate: response.data.performance.hitRate,
|
||||
redisConnected: response.data.health.redis.connected,
|
||||
}),
|
||||
},
|
||||
AuditSeverity.INFO,
|
||||
"Cache statistics accessed by admin"
|
||||
);
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error("[Cache Stats API] Error:", error);
|
||||
|
||||
await enhancedSecurityLog(
|
||||
SecurityEventType.API_SECURITY,
|
||||
"cache_stats_error",
|
||||
AuditOutcome.FAILURE,
|
||||
{
|
||||
metadata: createAuditMetadata({
|
||||
endpoint: "/api/admin/cache/stats",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}),
|
||||
},
|
||||
AuditSeverity.HIGH,
|
||||
"Cache stats API encountered an error"
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Internal server error",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -66,11 +66,12 @@ export async function GET(request: NextRequest) {
|
||||
await securityAuditLogger.logPlatformAdmin(
|
||||
"security_alerts_access",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
undefined,
|
||||
{
|
||||
alertCount: alerts.length,
|
||||
filters: query,
|
||||
...context,
|
||||
metadata: {
|
||||
alertCount: alerts.length,
|
||||
filters: query,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -85,7 +86,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid query parameters", details: error.errors },
|
||||
{ error: "Invalid query parameters", details: error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@ -101,7 +102,7 @@ export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user || !session.user.isPlatformUser) {
|
||||
if (!session?.user || !session.user.isPlatformUser || !session.user.id) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
@ -123,9 +124,10 @@ export async function POST(request: NextRequest) {
|
||||
await securityAuditLogger.logPlatformAdmin(
|
||||
"security_alert_acknowledged",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
undefined,
|
||||
{ alertId }
|
||||
{
|
||||
...context,
|
||||
metadata: { alertId },
|
||||
}
|
||||
);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
@ -137,7 +139,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid request", details: error.errors },
|
||||
{ error: "Invalid request", details: error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
@ -55,13 +55,14 @@ export async function GET(request: NextRequest) {
|
||||
await securityAuditLogger.logPlatformAdmin(
|
||||
"security_data_export",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
undefined,
|
||||
{
|
||||
exportType: query.type,
|
||||
format: query.format,
|
||||
timeRange,
|
||||
dataSize: data.length,
|
||||
...context,
|
||||
metadata: {
|
||||
exportType: query.type,
|
||||
format: query.format,
|
||||
timeRange,
|
||||
dataSize: data.length,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -77,7 +78,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid query parameters", details: error.errors },
|
||||
{ error: "Invalid query parameters", details: error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,10 +8,19 @@ import {
|
||||
securityAuditLogger,
|
||||
} from "@/lib/securityAuditLogger";
|
||||
import {
|
||||
AlertChannel,
|
||||
type AlertSeverity,
|
||||
type MonitoringConfig,
|
||||
securityMonitoring,
|
||||
} from "@/lib/securityMonitoring";
|
||||
|
||||
// Type for partial config updates that allows optional nested properties
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
||||
};
|
||||
|
||||
type ConfigUpdate = DeepPartial<MonitoringConfig>;
|
||||
|
||||
const metricsQuerySchema = z.object({
|
||||
startDate: z.string().datetime().optional(),
|
||||
endDate: z.string().datetime().optional(),
|
||||
@ -34,9 +43,7 @@ const configUpdateSchema = z.object({
|
||||
alerting: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
channels: z
|
||||
.array(z.enum(["EMAIL", "WEBHOOK", "SLACK", "DISCORD", "PAGERDUTY"]))
|
||||
.optional(),
|
||||
channels: z.array(z.nativeEnum(AlertChannel)).optional(),
|
||||
suppressDuplicateMinutes: z.number().min(1).max(1440).optional(),
|
||||
escalationTimeoutMinutes: z.number().min(5).max(1440).optional(),
|
||||
})
|
||||
@ -107,7 +114,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid query parameters", details: error.errors },
|
||||
{ error: "Invalid query parameters", details: error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@ -132,19 +139,35 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const config = configUpdateSchema.parse(body);
|
||||
const validatedConfig = configUpdateSchema.parse(body);
|
||||
const context = await createAuditContext(request, session);
|
||||
|
||||
// Build the config update object with proper type safety
|
||||
const configUpdate: ConfigUpdate = {};
|
||||
|
||||
if (validatedConfig.thresholds) {
|
||||
configUpdate.thresholds = validatedConfig.thresholds;
|
||||
}
|
||||
|
||||
if (validatedConfig.alerting) {
|
||||
configUpdate.alerting = validatedConfig.alerting;
|
||||
}
|
||||
|
||||
if (validatedConfig.retention) {
|
||||
configUpdate.retention = validatedConfig.retention;
|
||||
}
|
||||
|
||||
// Update monitoring configuration
|
||||
securityMonitoring.updateConfig(config);
|
||||
securityMonitoring.updateConfig(configUpdate);
|
||||
|
||||
// Log configuration change
|
||||
await securityAuditLogger.logPlatformAdmin(
|
||||
"security_monitoring_config_update",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
undefined,
|
||||
{ configChanges: config }
|
||||
{
|
||||
...context,
|
||||
metadata: { configChanges: validatedConfig },
|
||||
}
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
@ -156,7 +179,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid configuration", details: error.errors },
|
||||
{ error: "Invalid configuration", details: error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,10 +11,11 @@ import {
|
||||
type AlertType,
|
||||
type SecurityMetrics,
|
||||
securityMonitoring,
|
||||
type ThreatLevel,
|
||||
} from "@/lib/securityMonitoring";
|
||||
|
||||
const threatAnalysisSchema = z.object({
|
||||
ipAddress: z.string().ip().optional(),
|
||||
ipAddress: z.string().optional(),
|
||||
userId: z.string().uuid().optional(),
|
||||
timeRange: z
|
||||
.object({
|
||||
@ -39,9 +40,10 @@ export async function POST(request: NextRequest) {
|
||||
interface ThreatAnalysisResults {
|
||||
ipThreatAnalysis?: {
|
||||
ipAddress: string;
|
||||
threatLevel: number;
|
||||
threatLevel: ThreatLevel;
|
||||
isBlacklisted: boolean;
|
||||
riskFactors: string[];
|
||||
recommendations: string[];
|
||||
};
|
||||
timeRangeAnalysis?: {
|
||||
timeRange: { start: Date; end: Date };
|
||||
@ -111,11 +113,12 @@ export async function POST(request: NextRequest) {
|
||||
await securityAuditLogger.logPlatformAdmin(
|
||||
"threat_analysis_performed",
|
||||
AuditOutcome.SUCCESS,
|
||||
context,
|
||||
undefined,
|
||||
{
|
||||
analysisType: Object.keys(analysis),
|
||||
threatLevel: results.overallThreatLandscape?.currentThreatLevel,
|
||||
...context,
|
||||
metadata: {
|
||||
analysisType: Object.keys(analysis),
|
||||
threatLevel: results.overallThreatLandscape?.currentThreatLevel,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -125,7 +128,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid request", details: error.errors },
|
||||
{ error: "Invalid request", details: error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
@ -10,8 +10,7 @@ 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 ip = request.headers.get("x-forwarded-for") || "unknown";
|
||||
const rateLimitResult = await rateLimiter.check(
|
||||
`csp-report:${ip}`,
|
||||
10, // 10 reports
|
||||
|
||||
@ -45,20 +45,22 @@ function mapPrismaSessionToChatSession(prismaSession: {
|
||||
updatedAt: new Date(prismaSession.createdAt), // Fallback to createdAt
|
||||
// Prisma.Session does not have a `userId` field.
|
||||
userId: null, // Explicitly set to null or map if available from another source
|
||||
// Prisma.Session does not have a `companyId` field.
|
||||
companyId: "", // Explicitly set to empty string - should be resolved from session context
|
||||
// Ensure nullable fields from Prisma are correctly mapped to ChatSession's optional or nullable fields
|
||||
category: prismaSession.category ?? null,
|
||||
language: prismaSession.language ?? null,
|
||||
country: prismaSession.country ?? null,
|
||||
ipAddress: prismaSession.ipAddress ?? null,
|
||||
sentiment: prismaSession.sentiment ?? null,
|
||||
messagesSent: prismaSession.messagesSent ?? null, // Maintain consistency with other nullable fields
|
||||
messagesSent: prismaSession.messagesSent ?? undefined, // Maintain consistency with other nullable fields
|
||||
avgResponseTime: prismaSession.avgResponseTime ?? null,
|
||||
escalated: prismaSession.escalated,
|
||||
forwardedHr: prismaSession.forwardedHr,
|
||||
initialMsg: prismaSession.initialMsg ?? null,
|
||||
fullTranscriptUrl: prismaSession.fullTranscriptUrl ?? null,
|
||||
summary: prismaSession.summary ?? null, // New field
|
||||
transcriptContent: null, // Not available in Session model
|
||||
initialMsg: prismaSession.initialMsg ?? undefined,
|
||||
fullTranscriptUrl: prismaSession.fullTranscriptUrl ?? undefined,
|
||||
summary: prismaSession.summary ?? undefined, // New field
|
||||
transcriptContent: undefined, // Not available in Session model
|
||||
messages:
|
||||
prismaSession.messages?.map((msg) => ({
|
||||
id: msg.id,
|
||||
|
||||
@ -12,8 +12,10 @@ import {
|
||||
|
||||
// GET /api/platform/companies - List all companies
|
||||
export async function GET(request: NextRequest) {
|
||||
let session: any = null;
|
||||
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
session = await getServerSession(platformAuthOptions);
|
||||
const ip = extractClientIP(request);
|
||||
const userAgent = request.headers.get("user-agent") || undefined;
|
||||
|
||||
@ -137,8 +139,10 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// POST /api/platform/companies - Create new company
|
||||
export async function POST(request: NextRequest) {
|
||||
let session: any = null;
|
||||
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
session = await getServerSession(platformAuthOptions);
|
||||
const ip = extractClientIP(request);
|
||||
const userAgent = request.headers.get("user-agent") || undefined;
|
||||
|
||||
|
||||
@ -136,8 +136,11 @@ export default function AuditLogsPage() {
|
||||
});
|
||||
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
|
||||
const [hasFetched, setHasFetched] = useState(false);
|
||||
|
||||
const fetchAuditLogs = useCallback(async () => {
|
||||
if (hasFetched) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const params = new URLSearchParams({
|
||||
@ -161,6 +164,7 @@ export default function AuditLogsPage() {
|
||||
setAuditLogs(data.data.auditLogs);
|
||||
setPagination(data.data.pagination);
|
||||
setError(null);
|
||||
setHasFetched(true);
|
||||
} else {
|
||||
setError(data.error || "Failed to fetch audit logs");
|
||||
}
|
||||
@ -170,17 +174,23 @@ export default function AuditLogsPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [pagination.page, pagination.limit, filters]);
|
||||
}, [pagination.page, pagination.limit, filters, hasFetched]);
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user?.role === "ADMIN") {
|
||||
if (session?.user?.role === "ADMIN" && !hasFetched) {
|
||||
fetchAuditLogs();
|
||||
}
|
||||
}, [session, fetchAuditLogs]);
|
||||
}, [session?.user?.role, hasFetched, fetchAuditLogs]);
|
||||
|
||||
// Function to refresh audit logs (for filter changes)
|
||||
const refreshAuditLogs = useCallback(() => {
|
||||
setHasFetched(false);
|
||||
}, []);
|
||||
|
||||
const handleFilterChange = (key: keyof typeof filters, value: string) => {
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page
|
||||
refreshAuditLogs(); // Trigger fresh fetch with new filters
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
@ -192,6 +202,7 @@ export default function AuditLogsPage() {
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
});
|
||||
refreshAuditLogs(); // Trigger fresh fetch with cleared filters
|
||||
};
|
||||
|
||||
if (session?.user?.role !== "ADMIN") {
|
||||
@ -424,9 +435,10 @@ export default function AuditLogsPage() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!pagination.hasPrev}
|
||||
onClick={() =>
|
||||
setPagination((prev) => ({ ...prev, page: prev.page - 1 }))
|
||||
}
|
||||
onClick={() => {
|
||||
setPagination((prev) => ({ ...prev, page: prev.page - 1 }));
|
||||
refreshAuditLogs();
|
||||
}}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
@ -434,9 +446,10 @@ export default function AuditLogsPage() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!pagination.hasNext}
|
||||
onClick={() =>
|
||||
setPagination((prev) => ({ ...prev, page: prev.page + 1 }))
|
||||
}
|
||||
onClick={() => {
|
||||
setPagination((prev) => ({ ...prev, page: prev.page + 1 }));
|
||||
refreshAuditLogs();
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
||||
@ -503,14 +503,34 @@ function DashboardContent() {
|
||||
totalSessions: overviewData.totalSessions,
|
||||
avgSessionsPerDay: 0, // Will be computed properly later
|
||||
avgSessionLength: null,
|
||||
days: { data: [], labels: [] },
|
||||
languages: { data: [], labels: [] },
|
||||
categories: { data: [], labels: [] },
|
||||
countries: { data: [], labels: [] },
|
||||
days: {},
|
||||
languages: {},
|
||||
categories: {},
|
||||
countries: {},
|
||||
belowThresholdCount: 0,
|
||||
// Map the available data
|
||||
sentimentDistribution: overviewData.sentimentDistribution,
|
||||
categoryDistribution: overviewData.categoryDistribution,
|
||||
// Map sentiment data to individual counts
|
||||
sentimentPositiveCount:
|
||||
overviewData.sentimentDistribution?.find(
|
||||
(s) => s.sentiment === "positive"
|
||||
)?.count || 0,
|
||||
sentimentNeutralCount:
|
||||
overviewData.sentimentDistribution?.find(
|
||||
(s) => s.sentiment === "neutral"
|
||||
)?.count || 0,
|
||||
sentimentNegativeCount:
|
||||
overviewData.sentimentDistribution?.find(
|
||||
(s) => s.sentiment === "negative"
|
||||
)?.count || 0,
|
||||
// Map category data to CategoryMetrics format
|
||||
...(overviewData.categoryDistribution && {
|
||||
categories: overviewData.categoryDistribution.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.category] = item.count;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
),
|
||||
}),
|
||||
};
|
||||
setMetrics(mappedMetrics as MetricsResult);
|
||||
|
||||
|
||||
@ -49,12 +49,16 @@ interface FilterSectionProps {
|
||||
setSortOrder: (_order: string) => void;
|
||||
filterOptions: FilterOptions;
|
||||
searchHeadingId: string;
|
||||
searchId: string;
|
||||
filtersHeadingId: string;
|
||||
filterContentId: string;
|
||||
categoryFilterId: string;
|
||||
categoryHelpId: string;
|
||||
languageFilterId: string;
|
||||
languageHelpId: string;
|
||||
startDateId: string;
|
||||
endDateId: string;
|
||||
sortById: string;
|
||||
sortOrderId: string;
|
||||
sortOrderHelpId: string;
|
||||
}
|
||||
@ -78,12 +82,16 @@ function FilterSection({
|
||||
setSortOrder,
|
||||
filterOptions,
|
||||
searchHeadingId,
|
||||
searchId,
|
||||
filtersHeadingId,
|
||||
filterContentId,
|
||||
categoryFilterId,
|
||||
categoryHelpId,
|
||||
languageFilterId,
|
||||
languageHelpId,
|
||||
startDateId,
|
||||
endDateId,
|
||||
sortById,
|
||||
sortOrderId,
|
||||
sortOrderHelpId,
|
||||
}: FilterSectionProps) {
|
||||
@ -433,12 +441,16 @@ export default function SessionsPage() {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const searchHeadingId = useId();
|
||||
const searchId = useId();
|
||||
const filtersHeadingId = useId();
|
||||
const filterContentId = useId();
|
||||
const categoryFilterId = useId();
|
||||
const categoryHelpId = useId();
|
||||
const languageFilterId = useId();
|
||||
const languageHelpId = useId();
|
||||
const startDateId = useId();
|
||||
const endDateId = useId();
|
||||
const sortById = useId();
|
||||
const sortOrderId = useId();
|
||||
const sortOrderHelpId = useId();
|
||||
const resultsHeadingId = useId();
|
||||
@ -556,12 +568,16 @@ export default function SessionsPage() {
|
||||
setSortOrder={setSortOrder}
|
||||
filterOptions={filterOptions}
|
||||
searchHeadingId={searchHeadingId}
|
||||
searchId={searchId}
|
||||
filtersHeadingId={filtersHeadingId}
|
||||
filterContentId={filterContentId}
|
||||
categoryFilterId={categoryFilterId}
|
||||
categoryHelpId={categoryHelpId}
|
||||
languageFilterId={languageFilterId}
|
||||
languageHelpId={languageHelpId}
|
||||
startDateId={startDateId}
|
||||
endDateId={endDateId}
|
||||
sortById={sortById}
|
||||
sortOrderId={sortOrderId}
|
||||
sortOrderHelpId={sortOrderHelpId}
|
||||
/>
|
||||
|
||||
@ -209,20 +209,26 @@ function useCompanyData(
|
||||
toast: ToastFunction,
|
||||
state: CompanyManagementState
|
||||
) {
|
||||
const { setCompany, setEditData, setOriginalData, setIsLoading } = state;
|
||||
const [hasFetched, setHasFetched] = useState(false);
|
||||
|
||||
const fetchCompany = useCallback(async () => {
|
||||
if (hasFetched) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/platform/companies/${params.id}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
state.setCompany(data);
|
||||
setCompany(data);
|
||||
const companyData = {
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
status: data.status,
|
||||
maxUsers: data.maxUsers,
|
||||
};
|
||||
state.setEditData(companyData);
|
||||
state.setOriginalData(companyData);
|
||||
setEditData(companyData);
|
||||
setOriginalData(companyData);
|
||||
setHasFetched(true);
|
||||
} else {
|
||||
toast({
|
||||
title: "Error",
|
||||
@ -238,9 +244,17 @@ function useCompanyData(
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
state.setIsLoading(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [params.id, toast, state]);
|
||||
}, [
|
||||
params.id,
|
||||
hasFetched,
|
||||
toast,
|
||||
setCompany,
|
||||
setEditData,
|
||||
setOriginalData,
|
||||
setIsLoading,
|
||||
]);
|
||||
|
||||
return { fetchCompany };
|
||||
}
|
||||
@ -254,6 +268,8 @@ function useNavigationControl(
|
||||
hasUnsavedChanges: () => boolean,
|
||||
state: CompanyManagementState
|
||||
) {
|
||||
const { setPendingNavigation, setShowUnsavedChangesDialog } = state;
|
||||
|
||||
const handleNavigation = useCallback(
|
||||
(url: string) => {
|
||||
if (url.includes(`/platform/companies/${params.id}`)) {
|
||||
@ -262,13 +278,19 @@ function useNavigationControl(
|
||||
}
|
||||
|
||||
if (hasUnsavedChanges()) {
|
||||
state.setPendingNavigation(url);
|
||||
state.setShowUnsavedChangesDialog(true);
|
||||
setPendingNavigation(url);
|
||||
setShowUnsavedChangesDialog(true);
|
||||
} else {
|
||||
router.push(url);
|
||||
}
|
||||
},
|
||||
[router, params.id, hasUnsavedChanges, state]
|
||||
[
|
||||
router,
|
||||
params.id,
|
||||
hasUnsavedChanges,
|
||||
setPendingNavigation,
|
||||
setShowUnsavedChangesDialog,
|
||||
]
|
||||
);
|
||||
|
||||
return { handleNavigation };
|
||||
@ -462,10 +484,14 @@ export default function CompanyManagement() {
|
||||
state.editData,
|
||||
state.originalData
|
||||
);
|
||||
const { fetchCompany } = useCompanyData(params, toast, state);
|
||||
const { fetchCompany } = useCompanyData(
|
||||
{ id: params.id as string },
|
||||
toast,
|
||||
state
|
||||
);
|
||||
const { handleNavigation } = useNavigationControl(
|
||||
router,
|
||||
params,
|
||||
{ id: params.id as string },
|
||||
hasUnsavedChanges,
|
||||
state
|
||||
);
|
||||
@ -479,7 +505,7 @@ export default function CompanyManagement() {
|
||||
}
|
||||
|
||||
fetchCompany();
|
||||
}, [session, status, router, fetchCompany]);
|
||||
}, [status, session?.user?.isPlatformUser, fetchCompany, router.push]);
|
||||
|
||||
const handleSave = async () => {
|
||||
state.setIsSaving(true);
|
||||
@ -576,7 +602,14 @@ export default function CompanyManagement() {
|
||||
if (response.ok) {
|
||||
state.setShowInviteUser(false);
|
||||
state.setInviteData({ name: "", email: "", role: "USER" });
|
||||
fetchCompany();
|
||||
// Refresh company data to show new user
|
||||
const updatedResponse = await fetch(
|
||||
`/api/platform/companies/${params.id}`
|
||||
);
|
||||
if (updatedResponse.ok) {
|
||||
const updatedData = await updatedResponse.json();
|
||||
state.setCompany(updatedData);
|
||||
}
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "User invited successfully",
|
||||
|
||||
@ -346,7 +346,15 @@ function renderCompanyListItem(
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="font-semibold">{company.name}</h3>
|
||||
<Badge variant={getStatusBadgeVariant(company.status)}>
|
||||
<Badge
|
||||
variant={
|
||||
getStatusBadgeVariant(company.status) as
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
}
|
||||
>
|
||||
{company.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@ -36,9 +36,11 @@ function usePlatformSession() {
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
const handleAuthSuccess = (sessionData: any) => {
|
||||
const handleAuthSuccess = (sessionData: {
|
||||
user?: { isPlatformUser?: boolean };
|
||||
}) => {
|
||||
if (sessionData?.user?.isPlatformUser) {
|
||||
setSession(sessionData);
|
||||
setSession(sessionData as any);
|
||||
setStatus("authenticated");
|
||||
} else {
|
||||
handleAuthFailure();
|
||||
|
||||
Reference in New Issue
Block a user