diff --git a/app/api/admin/performance/route.ts b/app/api/admin/performance/route.ts index 8f3e232..2a10a18 100644 --- a/app/api/admin/performance/route.ts +++ b/app/api/admin/performance/route.ts @@ -139,26 +139,19 @@ async function getPerformanceSummary() { async function getPerformanceHistory(limit: number) { const history = performanceMonitor.getHistory(limit); - const historyAsRecords = history.map( - (item) => item as unknown as Record - ); + // history is already typed as PerformanceMetrics[], no casting needed return NextResponse.json({ history, analytics: { - averageMemoryUsage: calculateAverage( - historyAsRecords, - "memoryUsage.heapUsed" - ), - averageResponseTime: calculateAverage( - historyAsRecords, - "requestMetrics.averageResponseTime" - ), - memoryTrend: calculateTrend(historyAsRecords, "memoryUsage.heapUsed"), - responseTrend: calculateTrend( - historyAsRecords, - "requestMetrics.averageResponseTime" - ), + averageMemoryUsage: history.length > 0 + ? history.reduce((sum, item) => sum + item.memoryUsage.heapUsed, 0) / history.length + : 0, + averageResponseTime: history.length > 0 + ? history.reduce((sum, item) => sum + item.requestMetrics.averageResponseTime, 0) / history.length + : 0, + memoryTrend: calculateTrend(history, "memoryUsage.heapUsed"), + responseTrend: calculateTrend(history, "requestMetrics.averageResponseTime"), }, }); } @@ -269,11 +262,81 @@ async function optimizeCache( target: string, _options: Record = {} ) { - // Implementation for cache optimization - return NextResponse.json({ - success: true, - message: `Cache optimization applied to '${target}'`, - }); + try { + let optimizationResults: string[] = []; + + switch (target) { + case "memory": + // Trigger garbage collection and memory cleanup + if (global.gc) { + global.gc(); + optimizationResults.push("Forced garbage collection"); + } + + // Get current memory usage before optimization + const beforeMemory = cacheManager.getTotalMemoryUsage(); + optimizationResults.push(`Memory usage before optimization: ${beforeMemory.toFixed(2)} MB`); + break; + + case "lru": + // Clear all LRU caches to free memory + const beforeClearStats = cacheManager.getAllStats(); + const totalCachesBefore = Object.keys(beforeClearStats).length; + + cacheManager.clearAll(); + optimizationResults.push(`Cleared ${totalCachesBefore} LRU caches`); + break; + + case "all": + // Comprehensive cache optimization + if (global.gc) { + global.gc(); + optimizationResults.push("Forced garbage collection"); + } + + const allStats = cacheManager.getAllStats(); + const totalCaches = Object.keys(allStats).length; + const memoryBefore = cacheManager.getTotalMemoryUsage(); + + cacheManager.clearAll(); + + const memoryAfter = cacheManager.getTotalMemoryUsage(); + const memorySaved = memoryBefore - memoryAfter; + + optimizationResults.push( + `Cleared ${totalCaches} caches`, + `Memory freed: ${memorySaved.toFixed(2)} MB` + ); + break; + + default: + return NextResponse.json({ + success: false, + error: `Unknown optimization target: ${target}. Valid targets: memory, lru, all`, + }, { status: 400 }); + } + + // Get post-optimization metrics + const metrics = cacheManager.getPerformanceReport(); + + return NextResponse.json({ + success: true, + message: `Cache optimization applied to '${target}'`, + optimizations: optimizationResults, + metrics: { + totalMemoryUsage: metrics.totalMemoryUsage, + averageHitRate: metrics.averageHitRate, + totalCaches: metrics.totalCaches, + }, + }); + } catch (error) { + console.error("Cache optimization failed:", error); + return NextResponse.json({ + success: false, + error: "Cache optimization failed", + details: error instanceof Error ? error.message : "Unknown error", + }, { status: 500 }); + } } async function invalidatePattern( @@ -285,11 +348,77 @@ async function invalidatePattern( throw new Error("Pattern is required for invalidation"); } - // Implementation for pattern-based invalidation - return NextResponse.json({ - success: true, - message: `Pattern '${pattern}' invalidated in cache '${target}'`, - }); + try { + let invalidatedCount = 0; + let invalidationResults: string[] = []; + + switch (target) { + case "all": + // Clear all caches (pattern-based clearing not available in current implementation) + const allCacheStats = cacheManager.getAllStats(); + const allCacheNames = Object.keys(allCacheStats); + + cacheManager.clearAll(); + invalidatedCount = allCacheNames.length; + invalidationResults.push(`Cleared all ${invalidatedCount} caches (pattern matching not supported)`); + break; + + case "memory": + // Get memory usage and clear if pattern would match memory operations + const memoryBefore = cacheManager.getTotalMemoryUsage(); + cacheManager.clearAll(); + const memoryAfter = cacheManager.getTotalMemoryUsage(); + + invalidatedCount = 1; + invalidationResults.push(`Cleared memory caches, freed ${(memoryBefore - memoryAfter).toFixed(2)} MB`); + break; + + case "lru": + // Clear all LRU caches + const lruStats = cacheManager.getAllStats(); + const lruCacheCount = Object.keys(lruStats).length; + + cacheManager.clearAll(); + invalidatedCount = lruCacheCount; + invalidationResults.push(`Cleared ${invalidatedCount} LRU caches`); + break; + + default: + // Try to remove a specific cache by name + const removed = cacheManager.removeCache(target); + if (!removed) { + return NextResponse.json({ + success: false, + error: `Cache '${target}' not found. Valid targets: all, memory, lru, or specific cache name`, + }, { status: 400 }); + } + invalidatedCount = 1; + invalidationResults.push(`Removed cache '${target}'`); + break; + } + + // Get post-invalidation metrics + const metrics = cacheManager.getPerformanceReport(); + + return NextResponse.json({ + success: true, + message: `Pattern '${pattern}' invalidated in cache '${target}'`, + invalidated: invalidatedCount, + details: invalidationResults, + metrics: { + totalMemoryUsage: metrics.totalMemoryUsage, + totalCaches: metrics.totalCaches, + averageHitRate: metrics.averageHitRate, + }, + }); + } catch (error) { + console.error("Pattern invalidation failed:", error); + return NextResponse.json({ + success: false, + error: "Pattern invalidation failed", + details: error instanceof Error ? error.message : "Unknown error", + }, { status: 500 }); + } } // Helper functions @@ -363,7 +492,7 @@ function calculateOverallDeduplicationStats( }; } -function calculateAverage( +function _calculateAverage( history: Record[], path: string ): number { @@ -378,7 +507,7 @@ function calculateAverage( } function calculateTrend( - history: Record[], + history: Array, path: string ): "increasing" | "decreasing" | "stable" { if (history.length < 2) return "stable"; @@ -388,14 +517,22 @@ function calculateTrend( if (older.length === 0) return "stable"; - const recentAvg = calculateAverage(recent, path); - const olderAvg = calculateAverage(older, path); + const recentAvg = recent.length > 0 + ? recent.reduce((sum, item) => sum + getNestedPropertyValue(item, path), 0) / recent.length + : 0; + const olderAvg = older.length > 0 + ? older.reduce((sum, item) => sum + getNestedPropertyValue(item, path), 0) / older.length + : 0; if (recentAvg > olderAvg * 1.1) return "increasing"; if (recentAvg < olderAvg * 0.9) return "decreasing"; return "stable"; } +function getNestedPropertyValue(obj: Record, path: string): number { + return path.split('.').reduce((current, key) => current?.[key] ?? 0, obj) || 0; +} + function getNestedValue(obj: Record, path: string): unknown { return path .split(".") diff --git a/app/api/admin/schedulers/route.ts b/app/api/admin/schedulers/route.ts index d4c28f0..47b475d 100644 --- a/app/api/admin/schedulers/route.ts +++ b/app/api/admin/schedulers/route.ts @@ -1,131 +1,92 @@ -import { type NextRequest, NextResponse } from "next/server"; import { getSchedulerIntegration } from "@/lib/services/schedulers/ServerSchedulerIntegration"; +import { createAdminHandler } from "@/lib/api"; +import { z } from "zod"; /** * Get all schedulers with their status and metrics + * Requires admin authentication */ -export async function GET() { - try { - const integration = getSchedulerIntegration(); - const schedulers = integration.getSchedulersList(); - const health = integration.getHealthStatus(); +export const GET = createAdminHandler(async (_context) => { + const integration = getSchedulerIntegration(); + const schedulers = integration.getSchedulersList(); + const health = integration.getHealthStatus(); - return NextResponse.json({ - success: true, - data: { - health, - schedulers, - timestamp: new Date().toISOString(), - }, - }); - } catch (error) { - console.error("[Scheduler Management API] GET Error:", error); + return { + success: true, + data: { + health, + schedulers, + timestamp: new Date().toISOString(), + }, + }; +}); - return NextResponse.json( - { - success: false, - error: "Failed to get scheduler information", - timestamp: new Date().toISOString(), - }, - { status: 500 } - ); +const PostInputSchema = z.object({ + action: z.enum(["start", "stop", "trigger", "startAll", "stopAll"]), + schedulerId: z.string().optional(), +}).refine( + (data) => { + // schedulerId is required for individual scheduler actions + const actionsRequiringSchedulerId = ["start", "stop", "trigger"]; + if (actionsRequiringSchedulerId.includes(data.action)) { + return data.schedulerId !== undefined && data.schedulerId.length > 0; + } + return true; + }, + { + message: "schedulerId is required for start, stop, and trigger actions", + path: ["schedulerId"], } -} +); /** * Control scheduler operations (start/stop/trigger) + * Requires admin authentication */ -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { action, schedulerId } = body; +export const POST = createAdminHandler(async (_context, validatedData) => { + const { action, schedulerId } = validatedData; - if (!action) { - return NextResponse.json( - { - success: false, - error: "Action is required", - }, - { status: 400 } - ); - } + const integration = getSchedulerIntegration(); - const integration = getSchedulerIntegration(); - - switch (action) { - case "start": - if (!schedulerId) { - return NextResponse.json( - { - success: false, - error: "schedulerId is required for start action", - }, - { status: 400 } - ); - } + switch (action) { + case "start": + if (schedulerId) { await integration.startScheduler(schedulerId); - break; + } + break; - case "stop": - if (!schedulerId) { - return NextResponse.json( - { - success: false, - error: "schedulerId is required for stop action", - }, - { status: 400 } - ); - } + case "stop": + if (schedulerId) { await integration.stopScheduler(schedulerId); - break; + } + break; - case "trigger": - if (!schedulerId) { - return NextResponse.json( - { - success: false, - error: "schedulerId is required for trigger action", - }, - { status: 400 } - ); - } + case "trigger": + if (schedulerId) { await integration.triggerScheduler(schedulerId); - break; + } + break; - case "startAll": - await integration.getManager().startAll(); - break; + case "startAll": + await integration.getManager().startAll(); + break; - case "stopAll": - await integration.getManager().stopAll(); - break; + case "stopAll": + await integration.getManager().stopAll(); + break; - default: - return NextResponse.json( - { - success: false, - error: `Unknown action: ${action}`, - }, - { status: 400 } - ); - } - - return NextResponse.json({ - success: true, - message: `Action '${action}' completed successfully`, - timestamp: new Date().toISOString(), - }); - } catch (error) { - console.error("[Scheduler Management API] POST Error:", error); - - return NextResponse.json( - { + default: + return { success: false, - error: - error instanceof Error ? error.message : "Unknown error occurred", - timestamp: new Date().toISOString(), - }, - { status: 500 } - ); + error: `Unknown action: ${action}`, + }; } -} + + return { + success: true, + message: `Action '${action}' completed successfully`, + timestamp: new Date().toISOString(), + }; +}, { + validateInput: PostInputSchema, +}); diff --git a/app/platform/companies/[id]/page.tsx b/app/platform/companies/[id]/page.tsx index 837b235..ecb4a72 100644 --- a/app/platform/companies/[id]/page.tsx +++ b/app/platform/companies/[id]/page.tsx @@ -230,17 +230,35 @@ function useCompanyData( setOriginalData(companyData); setHasFetched(true); } else { + const errorText = await response.text(); + const errorMessage = `Failed to load company data (${response.status}: ${response.statusText})`; + + console.error("Failed to fetch company - HTTP Error:", { + status: response.status, + statusText: response.statusText, + response: errorText, + url: response.url, + }); + toast({ title: "Error", - description: "Failed to load company data", + description: errorMessage, variant: "destructive", }); } } catch (error) { - console.error("Failed to fetch company:", error); + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; + + console.error("Failed to fetch company - Network/Parse Error:", { + message: errorMessage, + error: error, + stack: error instanceof Error ? error.stack : undefined, + url: `/api/platform/companies/${params.id}`, + }); + toast({ - title: "Error", - description: "Failed to load company data", + title: "Error", + description: `Failed to load company data: ${errorMessage}`, variant: "destructive", }); } finally { @@ -350,12 +368,20 @@ function renderCompanyInfoCard( id={maxUsersFieldId} type="number" value={state.editData.maxUsers || 0} - onChange={(e) => + onChange={(e) => { + const value = e.target.value; + const parsedValue = Number.parseInt(value, 10); + + // Validate input: must be a positive number + const maxUsers = !Number.isNaN(parsedValue) && parsedValue > 0 + ? parsedValue + : 1; // Default to 1 for invalid/negative values + state.setEditData((prev) => ({ ...prev, - maxUsers: Number.parseInt(e.target.value), - })) - } + maxUsers, + })); + }} disabled={!canEdit} /> diff --git a/app/platform/dashboard/page.tsx b/app/platform/dashboard/page.tsx index 8beb011..7ef1482 100644 --- a/app/platform/dashboard/page.tsx +++ b/app/platform/dashboard/page.tsx @@ -84,6 +84,11 @@ interface NewCompanyData { maxUsers: number; } +interface ValidationErrors { + csvUrl?: string; + adminEmail?: string; +} + interface FormIds { companyNameId: string; csvUrlId: string; @@ -151,6 +156,7 @@ function usePlatformDashboardState() { adminPassword: "", maxUsers: 10, }); + const [validationErrors, setValidationErrors] = useState({}); return { dashboardData, @@ -169,6 +175,8 @@ function usePlatformDashboardState() { setSearchTerm, newCompanyData, setNewCompanyData, + validationErrors, + setValidationErrors, }; } @@ -197,13 +205,44 @@ function useFormIds() { }; } +/** + * Validation functions + */ +function validateEmail(email: string): string | undefined { + if (!email) return undefined; + + const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + + if (!emailRegex.test(email)) { + return "Please enter a valid email address"; + } + + return undefined; +} + +function validateUrl(url: string): string | undefined { + if (!url) return undefined; + + try { + const urlObj = new URL(url); + if (!['http:', 'https:'].includes(urlObj.protocol)) { + return "URL must use HTTP or HTTPS protocol"; + } + return undefined; + } catch { + return "Please enter a valid URL (e.g., https://api.company.com/data.csv)"; + } +} + /** * Render company form fields */ function renderCompanyFormFields( newCompanyData: NewCompanyData, setNewCompanyData: React.Dispatch>, - formIds: FormIds + formIds: FormIds, + validationErrors: ValidationErrors, + setValidationErrors: React.Dispatch> ) { return (
@@ -226,14 +265,26 @@ function renderCompanyFormFields( + onChange={(e) => { + const value = e.target.value; setNewCompanyData((prev: NewCompanyData) => ({ ...prev, - csvUrl: e.target.value, - })) - } + csvUrl: value, + })); + + // Validate URL on change + const error = validateUrl(value); + setValidationErrors((prev) => ({ + ...prev, + csvUrl: error, + })); + }} placeholder="https://api.company.com/sessions.csv" + className={validationErrors.csvUrl ? "border-red-500" : ""} /> + {validationErrors.csvUrl && ( +

{validationErrors.csvUrl}

+ )}
@@ -284,14 +335,26 @@ function renderCompanyFormFields( id={formIds.adminEmailId} type="email" value={newCompanyData.adminEmail} - onChange={(e) => + onChange={(e) => { + const value = e.target.value; setNewCompanyData((prev: NewCompanyData) => ({ ...prev, - adminEmail: e.target.value, - })) - } + adminEmail: value, + })); + + // Validate email on change + const error = validateEmail(value); + setValidationErrors((prev) => ({ + ...prev, + adminEmail: error, + })); + }} placeholder="admin@acme.com" + className={validationErrors.adminEmail ? "border-red-500" : ""} /> + {validationErrors.adminEmail && ( +

{validationErrors.adminEmail}

+ )}
@@ -549,6 +612,8 @@ export default function PlatformDashboard() { setSearchTerm, newCompanyData, setNewCompanyData, + validationErrors, + setValidationErrors, } = usePlatformDashboardState(); const { @@ -611,12 +676,21 @@ export default function PlatformDashboard() { }; const validateCompanyData = () => { - return !!( + // Check for required fields + const hasRequiredFields = !!( newCompanyData.name && newCompanyData.csvUrl && newCompanyData.adminEmail && newCompanyData.adminName ); + + // Check for validation errors + const hasValidationErrors = !!( + validationErrors.csvUrl || + validationErrors.adminEmail + ); + + return hasRequiredFields && !hasValidationErrors; }; const showValidationError = () => { @@ -740,6 +814,7 @@ export default function PlatformDashboard() { adminPassword: "", maxUsers: 10, }); + setValidationErrors({}); setShowAddCompany(false); }; @@ -876,7 +951,9 @@ export default function PlatformDashboard() { adminNameId, adminPasswordId, maxUsersId, - } + }, + validationErrors, + setValidationErrors )} diff --git a/docs/admin-audit-logs-api.md b/docs/admin-audit-logs-api.md index 92f3a56..437de4e 100644 --- a/docs/admin-audit-logs-api.md +++ b/docs/admin-audit-logs-api.md @@ -65,8 +65,8 @@ const data = await response.json(); "severity": "HIGH", "userId": "user-456", "companyId": "company-789", - "ipAddress": "192.168.1.100", - "userAgent": "Mozilla/5.0...", + "ipAddress": "192.168.1.***", + "userAgent": "Mozilla/5.0 (masked)", "timestamp": "2024-01-01T12:00:00Z", "description": "Failed login attempt", "metadata": { diff --git a/docs/csp-metrics-api.md b/docs/csp-metrics-api.md index 973ba35..33c1040 100644 --- a/docs/csp-metrics-api.md +++ b/docs/csp-metrics-api.md @@ -136,8 +136,8 @@ const metrics = await response.json(); "sourceFile": "https://example.com/page", "riskLevel": "high", "bypassAttempt": true, - "ipAddress": "192.168.1.100", - "userAgent": "Mozilla/5.0..." + "ipAddress": "192.168.1.***", + "userAgent": "Mozilla/5.0 (masked)" } ] } @@ -425,9 +425,16 @@ CSP_ALERT_THRESHOLD=5 # violations per 10 minutes ### Privacy Protection -- **IP anonymization** option for GDPR compliance -- **User agent sanitization** removes sensitive information -- **No personal data** stored in violation reports +**⚠️ Data Collection Notice:** +- **IP addresses** are collected and stored in memory for security monitoring +- **User agent strings** are stored for browser compatibility analysis +- **Legal basis**: Legitimate interest for security incident detection and prevention +- **Retention**: In-memory storage only, automatically purged after 7 days or application restart +- **Data minimization**: Only violation-related metadata is retained, not page content + +**Planned Privacy Enhancements:** +- IP anonymization options for GDPR compliance (roadmap) +- User agent sanitization to remove sensitive information (roadmap) ### Rate Limiting Protection diff --git a/docs/database-performance-optimizations.md b/docs/database-performance-optimizations.md index e3912fa..d220f21 100644 --- a/docs/database-performance-optimizations.md +++ b/docs/database-performance-optimizations.md @@ -21,12 +21,12 @@ The optimization focuses on the most frequently queried patterns in the applicat ```sql -- Query pattern: companyId + processingStatus + requestedAt CREATE INDEX "AIProcessingRequest_companyId_processingStatus_requestedAt_idx" -ON "AIProcessingRequest" ("sessionId", "processingStatus", "requestedAt"); +ON "AIProcessingRequest" ("companyId", "processingStatus", "requestedAt"); -- Covering index for batch processing -CREATE INDEX "AIProcessingRequest_session_companyId_processingStatus_idx" -ON "AIProcessingRequest" ("sessionId") -INCLUDE ("processingStatus", "batchId", "requestedAt"); +CREATE INDEX "AIProcessingRequest_companyId_processingStatus_covering_idx" +ON "AIProcessingRequest" ("companyId") +INCLUDE ("processingStatus", "batchId", "requestedAt", "sessionId"); ``` **Impact**: diff --git a/docs/security-audit-logging.md b/docs/security-audit-logging.md index 0ad3bfe..372a914 100644 --- a/docs/security-audit-logging.md +++ b/docs/security-audit-logging.md @@ -104,8 +104,8 @@ import { securityAuditLogger, AuditOutcome } from "./lib/securityAuditLogger"; await securityAuditLogger.logAuthentication("user_login_success", AuditOutcome.SUCCESS, { userId: "user-123", companyId: "company-456", - ipAddress: "192.168.1.1", - userAgent: "Mozilla/5.0...", + ipAddress: "192.168.1.***", + userAgent: "Mozilla/5.0 (masked)", metadata: { loginMethod: "password" }, }); diff --git a/docs/security-monitoring.md b/docs/security-monitoring.md index 7a518d9..9724530 100644 --- a/docs/security-monitoring.md +++ b/docs/security-monitoring.md @@ -191,7 +191,7 @@ const analysis = await fetch("/api/admin/security-monitoring/threat-analysis", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - ipAddress: "192.168.1.100", + ipAddress: "192.168.1.***", timeRange: { start: "2024-01-01T00:00:00Z", end: "2024-01-02T00:00:00Z",