feat: enhance security, performance, and stability

This commit introduces a range of improvements across the application:

- **Security:**
  - Adds authentication to the CSP metrics endpoint.
  - Hardens CSP bypass detection regex to prevent ReDoS attacks.
  - Improves CORS headers for the CSP metrics API.
  - Adds filtering for acknowledged alerts in security monitoring.

- **Performance:**
  - Optimizes database connection pooling for NeonDB.
  - Improves session fetching with abort controller.

- **Stability:**
  - Adds error handling to the tRPC demo component.
  - Fixes type inconsistencies in session data mapping.

- **Docs & DX:**
  - Ignores  files in git.
  - Fixes a token placeholder in the documentation.
This commit is contained in:
2025-07-12 01:03:52 +02:00
parent 314326400e
commit 7a3eabccd9
9 changed files with 173 additions and 97 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
*-PROGRESS.md *-PROGRESS.md
pr-comments*.json
# Created by https://www.toptal.com/developers/gitignore/api/node,nextjs,react # Created by https://www.toptal.com/developers/gitignore/api/node,nextjs,react
# Edit at https://www.toptal.com/developers/gitignore?templates=node,nextjs,react # Edit at https://www.toptal.com/developers/gitignore?templates=node,nextjs,react

View File

@ -45,10 +45,18 @@ export async function GET(request: NextRequest) {
const context = await createAuditContext(request, session); const context = await createAuditContext(request, session);
// Get alerts based on filters // Get alerts based on filters
const alerts = securityMonitoring.getActiveAlerts( let alerts = securityMonitoring.getActiveAlerts(
query.severity as AlertSeverity query.severity as AlertSeverity
); );
// Apply acknowledged filter if provided
if (query.acknowledged !== undefined) {
const showAcknowledged = query.acknowledged === "true";
alerts = alerts.filter((alert) =>
showAcknowledged ? alert.acknowledged : !alert.acknowledged
);
}
// Apply pagination // Apply pagination
const limit = query.limit || 50; const limit = query.limit || 50;
const offset = query.offset || 0; const offset = query.offset || 0;

View File

@ -1,12 +1,19 @@
import { type NextRequest, NextResponse } from "next/server"; import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { cspMonitoring } from "@/lib/csp-monitoring"; import { cspMonitoring } from "@/lib/csp-monitoring";
import { rateLimiter } from "@/lib/rateLimiter"; import { extractClientIP, rateLimiter } from "@/lib/rateLimiter";
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
// Authentication check for security metrics endpoint
const session = await getServerSession(authOptions);
if (!session?.user || !session.user.isPlatformUser) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Rate limiting for metrics endpoint // Rate limiting for metrics endpoint
const ip = const ip = extractClientIP(request);
request.ip || request.headers.get("x-forwarded-for") || "unknown";
const rateLimitResult = await rateLimiter.check( const rateLimitResult = await rateLimiter.check(
`csp-metrics:${ip}`, `csp-metrics:${ip}`,
30, // 30 requests 30, // 30 requests
@ -102,9 +109,11 @@ export async function OPTIONS() {
return new NextResponse(null, { return new NextResponse(null, {
status: 200, status: 200,
headers: { headers: {
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin":
process.env.ALLOWED_ORIGINS || "https://livedash.notso.ai",
"Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type", "Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Credentials": "true",
}, },
}); });
} }

View File

@ -51,11 +51,11 @@ function mapPrismaSessionToChatSession(prismaSession: {
country: prismaSession.country ?? null, country: prismaSession.country ?? null,
ipAddress: prismaSession.ipAddress ?? null, ipAddress: prismaSession.ipAddress ?? null,
sentiment: prismaSession.sentiment ?? null, sentiment: prismaSession.sentiment ?? null,
messagesSent: prismaSession.messagesSent ?? undefined, // Use undefined if ChatSession expects number | undefined messagesSent: prismaSession.messagesSent ?? null, // Maintain consistency with other nullable fields
avgResponseTime: prismaSession.avgResponseTime ?? null, avgResponseTime: prismaSession.avgResponseTime ?? null,
escalated: prismaSession.escalated ?? undefined, escalated: prismaSession.escalated,
forwardedHr: prismaSession.forwardedHr ?? undefined, forwardedHr: prismaSession.forwardedHr,
initialMsg: prismaSession.initialMsg ?? undefined, initialMsg: prismaSession.initialMsg ?? null,
fullTranscriptUrl: prismaSession.fullTranscriptUrl ?? null, fullTranscriptUrl: prismaSession.fullTranscriptUrl ?? null,
summary: prismaSession.summary ?? null, // New field summary: prismaSession.summary ?? null, // New field
transcriptContent: null, // Not available in Session model transcriptContent: null, // Not available in Session model

View File

@ -25,6 +25,8 @@ function usePlatformSession() {
name?: string; name?: string;
role: string; role: string;
companyId?: string; companyId?: string;
isPlatformUser?: boolean;
platformRole?: string;
}; };
} | null>(null); } | null>(null);
const [status, setStatus] = useState< const [status, setStatus] = useState<
@ -32,26 +34,47 @@ function usePlatformSession() {
>("loading"); >("loading");
useEffect(() => { useEffect(() => {
const fetchSession = async () => { const abortController = new AbortController();
try {
const response = await fetch("/api/platform/auth/session");
const sessionData = await response.json();
const handleAuthSuccess = (sessionData: any) => {
if (sessionData?.user?.isPlatformUser) { if (sessionData?.user?.isPlatformUser) {
setSession(sessionData); setSession(sessionData);
setStatus("authenticated"); setStatus("authenticated");
} else { } else {
setSession(null); handleAuthFailure();
setStatus("unauthenticated");
} }
} catch (error) { };
console.error("Platform session fetch error:", error);
const handleAuthFailure = (error?: unknown) => {
if (error instanceof Error && error.name === "AbortError") return;
if (error) console.error("Platform session fetch error:", error);
setSession(null); setSession(null);
setStatus("unauthenticated"); setStatus("unauthenticated");
};
const fetchSession = async () => {
try {
const response = await fetch("/api/platform/auth/session", {
signal: abortController.signal,
});
if (!response.ok) {
if (response.status === 401) return handleAuthFailure();
throw new Error(`Failed to fetch session: ${response.status}`);
}
const sessionData = await response.json();
handleAuthSuccess(sessionData);
} catch (error) {
handleAuthFailure(error);
} }
}; };
fetchSession(); fetchSession();
return () => {
abortController.abort();
};
}, []); }, []);
return { data: session, status }; return { data: session, status };

View File

@ -31,11 +31,17 @@ export function TRPCDemo() {
refetch: refetchSessions, refetch: refetchSessions,
} = trpc.dashboard.getSessions.useQuery(sessionFilters); } = trpc.dashboard.getSessions.useQuery(sessionFilters);
const { data: overview, isLoading: overviewLoading } = const {
trpc.dashboard.getOverview.useQuery({}); data: overview,
isLoading: overviewLoading,
error: overviewError,
} = trpc.dashboard.getOverview.useQuery({});
const { data: topQuestions, isLoading: questionsLoading } = const {
trpc.dashboard.getTopQuestions.useQuery({ limit: 3 }); data: topQuestions,
isLoading: questionsLoading,
error: questionsError,
} = trpc.dashboard.getTopQuestions.useQuery({ limit: 3 });
// Mutations // Mutations
const refreshSessionsMutation = trpc.dashboard.refreshSessions.useMutation({ const refreshSessionsMutation = trpc.dashboard.refreshSessions.useMutation({
@ -84,6 +90,11 @@ export function TRPCDemo() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{overviewError && (
<div className="text-red-600 text-sm mb-2">
Error: {overviewError.message}
</div>
)}
{overviewLoading ? ( {overviewLoading ? (
<div className="flex items-center"> <div className="flex items-center">
<Loader2 className="h-4 w-4 animate-spin mr-2" /> <Loader2 className="h-4 w-4 animate-spin mr-2" />
@ -102,6 +113,11 @@ export function TRPCDemo() {
<CardTitle className="text-sm font-medium">Avg Messages</CardTitle> <CardTitle className="text-sm font-medium">Avg Messages</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{overviewError && (
<div className="text-red-600 text-sm mb-2">
Error: {overviewError.message}
</div>
)}
{overviewLoading ? ( {overviewLoading ? (
<div className="flex items-center"> <div className="flex items-center">
<Loader2 className="h-4 w-4 animate-spin mr-2" /> <Loader2 className="h-4 w-4 animate-spin mr-2" />
@ -122,6 +138,11 @@ export function TRPCDemo() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{overviewError && (
<div className="text-red-600 text-sm mb-2">
Error: {overviewError.message}
</div>
)}
{overviewLoading ? ( {overviewLoading ? (
<div className="flex items-center"> <div className="flex items-center">
<Loader2 className="h-4 w-4 animate-spin mr-2" /> <Loader2 className="h-4 w-4 animate-spin mr-2" />
@ -150,6 +171,11 @@ export function TRPCDemo() {
<CardTitle>Top Questions</CardTitle> <CardTitle>Top Questions</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{questionsError && (
<div className="text-red-600 mb-4">
Error loading questions: {questionsError.message}
</div>
)}
{questionsLoading ? ( {questionsLoading ? (
<div className="flex items-center"> <div className="flex items-center">
<Loader2 className="h-4 w-4 animate-spin mr-2" /> <Loader2 className="h-4 w-4 animate-spin mr-2" />

View File

@ -6,7 +6,7 @@ This document provides specific recommendations for optimizing database connecti
From your logs, we can see: From your logs, we can see:
``` ```bash
Can't reach database server at `ep-tiny-math-a2zsshve-pooler.eu-central-1.aws.neon.tech:5432` Can't reach database server at `ep-tiny-math-a2zsshve-pooler.eu-central-1.aws.neon.tech:5432`
[NODE-CRON] [WARN] missed execution at Sun Jun 29 2025 12:00:00 GMT+0200! Possible blocking IO or high CPU [NODE-CRON] [WARN] missed execution at Sun Jun 29 2025 12:00:00 GMT+0200! Possible blocking IO or high CPU
``` ```
@ -100,7 +100,7 @@ SESSION_PROCESSING_INTERVAL="0 */2 * * *" # Every 2 hours instead of 1
```bash ```bash
# Check connection health # Check connection health
curl -H "Authorization: Bearer your-token" \ curl -H "Authorization: Bearer YOUR_API_TOKEN" \
http://localhost:3000/api/admin/database-health http://localhost:3000/api/admin/database-health
``` ```

View File

@ -401,30 +401,30 @@ export function parseCSPViolation(report: CSPViolationReport): {
} }
/** /**
* CSP bypass detection patterns * CSP bypass detection patterns - optimized to prevent ReDoS attacks
*/ */
export const CSP_BYPASS_PATTERNS = [ export const CSP_BYPASS_PATTERNS = [
// Common XSS bypass attempts // Common XSS bypass attempts (exact matches to prevent ReDoS)
/javascript:/i, /^javascript:/i,
/data:text\/html/i, /^data:text\/html/i,
/vbscript:/i, /^vbscript:/i,
/livescript:/i, /^livescript:/i,
// Base64 encoded attempts // Base64 encoded attempts (limited quantifiers to prevent ReDoS)
/data:.*base64.*script/i, /^data:[^;]{0,50};base64[^,]{0,100},.*script/i,
/data:text\/javascript/i, /^data:text\/javascript/i,
/data:application\/javascript/i, /^data:application\/javascript/i,
// JSONP callback manipulation // JSONP callback manipulation (limited lookahead)
/callback=.*script/i, /callback=[^&]{0,200}script/i,
// Common CSP bypass techniques // Common CSP bypass techniques (limited quantifiers)
/location\.href.*javascript/i, /location\.href[^;]{0,100}javascript/i,
/document\.write.*script/i, /document\.write[^;]{0,100}script/i,
/eval\(/i, /\beval\s*\(/i,
/\bnew\s+Function\s*\(/i, /\bnew\s+Function\s*\(/i,
/setTimeout\s*\(\s*['"`].*['"`]/i, /setTimeout\s*\(\s*['"`][^'"`]{0,500}['"`]/i,
/setInterval\s*\(\s*['"`].*['"`]/i, /setInterval\s*\(\s*['"`][^'"`]{0,500}['"`]/i,
]; ];
/** /**
@ -550,14 +550,14 @@ export function detectCSPBypass(content: string): {
} }
} }
// Determine risk level based on pattern types // Determine risk level based on pattern types (ReDoS-safe patterns)
const highRiskPatterns = [ const highRiskPatterns = [
/javascript:/i, /^javascript:/i,
/eval\(/i, /\beval\s*\(/i,
/\bnew\s+Function\s*\(/i, /\bnew\s+Function\s*\(/i,
/data:text\/javascript/i, /^data:text\/javascript/i,
/data:application\/javascript/i, /^data:application\/javascript/i,
/data:.*base64.*script/i, /^data:[^;]{0,50};base64[^,]{0,100},.*script/i,
]; ];
const hasHighRiskPattern = detectedPatterns.some((pattern) => const hasHighRiskPattern = detectedPatterns.some((pattern) =>

View File

@ -21,15 +21,24 @@ export const createEnhancedPrismaClient = () => {
? { rejectUnauthorized: false } ? { rejectUnauthorized: false }
: undefined, : undefined,
// Connection pool settings // Connection pool settings optimized for Neon
max: env.DATABASE_CONNECTION_LIMIT || 20, // Maximum number of connections max: env.DATABASE_CONNECTION_LIMIT || 15, // Maximum number of connections (reduced for Neon)
min: 2, // Minimum connections to keep warm (prevent auto-pause)
idleTimeoutMillis: env.DATABASE_POOL_TIMEOUT * 1000 || 30000, // Use env timeout idleTimeoutMillis: env.DATABASE_POOL_TIMEOUT * 1000 || 30000, // Use env timeout
connectionTimeoutMillis: 5000, // 5 seconds connectionTimeoutMillis: 10000, // 10 seconds (increased for Neon cold starts)
query_timeout: 10000, // 10 seconds query_timeout: 15000, // 15 seconds (increased for Neon)
statement_timeout: 10000, // 10 seconds statement_timeout: 15000, // 15 seconds (increased for Neon)
// Keepalive settings to prevent Neon auto-pause
keepAlive: true,
keepAliveInitialDelayMillis: 10000,
// Application name for monitoring in Neon dashboard
application_name:
dbUrl.searchParams.get("application_name") || "livedash-app",
// Connection lifecycle // Connection lifecycle
allowExitOnIdle: true, allowExitOnIdle: false, // Keep minimum connections alive for Neon
}; };
const adapter = new PrismaPg(poolConfig); const adapter = new PrismaPg(poolConfig);