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 abortController = new AbortController();
const handleAuthSuccess = (sessionData: any) => {
if (sessionData?.user?.isPlatformUser) {
setSession(sessionData);
setStatus("authenticated");
} else {
handleAuthFailure();
}
};
const handleAuthFailure = (error?: unknown) => {
if (error instanceof Error && error.name === "AbortError") return;
if (error) console.error("Platform session fetch error:", error);
setSession(null);
setStatus("unauthenticated");
};
const fetchSession = async () => { const fetchSession = async () => {
try { try {
const response = await fetch("/api/platform/auth/session"); const response = await fetch("/api/platform/auth/session", {
const sessionData = await response.json(); signal: abortController.signal,
});
if (sessionData?.user?.isPlatformUser) { if (!response.ok) {
setSession(sessionData); if (response.status === 401) return handleAuthFailure();
setStatus("authenticated"); throw new Error(`Failed to fetch session: ${response.status}`);
} else {
setSession(null);
setStatus("unauthenticated");
} }
const sessionData = await response.json();
handleAuthSuccess(sessionData);
} catch (error) { } catch (error) {
console.error("Platform session fetch error:", error); handleAuthFailure(error);
setSession(null);
setStatus("unauthenticated");
} }
}; };
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
``` ```
@ -15,21 +15,21 @@ Can't reach database server at `ep-tiny-math-a2zsshve-pooler.eu-central-1.aws.ne
### 1. Neon Connection Limits ### 1. Neon Connection Limits
- **Free Tier**: 20 concurrent connections - **Free Tier**: 20 concurrent connections
- **Pro Tier**: 100 concurrent connections - **Pro Tier**: 100 concurrent connections
- **Multiple schedulers** can quickly exhaust connections - **Multiple schedulers** can quickly exhaust connections
### 2. Connection Pooling Issues ### 2. Connection Pooling Issues
- Each scheduler was creating separate PrismaClient instances - Each scheduler was creating separate PrismaClient instances
- No connection reuse between operations - No connection reuse between operations
- No retry logic for temporary failures - No retry logic for temporary failures
### 3. Neon-Specific Challenges ### 3. Neon-Specific Challenges
- **Auto-pause**: Databases pause after inactivity - **Auto-pause**: Databases pause after inactivity
- **Cold starts**: First connection after pause takes longer - **Cold starts**: First connection after pause takes longer
- **Regional latency**: eu-central-1 may have variable latency - **Regional latency**: eu-central-1 may have variable latency
## Solutions Implemented ## Solutions Implemented
@ -100,15 +100,15 @@ 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
``` ```
### 2. Neon Dashboard Monitoring ### 2. Neon Dashboard Monitoring
- Monitor "Active connections" in Neon dashboard - Monitor "Active connections" in Neon dashboard
- Check for connection spikes during scheduler runs - Check for connection spikes during scheduler runs
- Review query performance and slow queries - Review query performance and slow queries
### 3. Application Logs ### 3. Application Logs
@ -158,44 +158,44 @@ const prisma = new PrismaClient({
**Causes:** **Causes:**
- Neon database auto-paused - Neon database auto-paused
- Connection limit exceeded - Connection limit exceeded
- Network issues - Network issues
**Solutions:** **Solutions:**
1. Enable enhanced pooling: `USE_ENHANCED_POOLING=true` 1. Enable enhanced pooling: `USE_ENHANCED_POOLING=true`
2. Reduce connection limit: `DATABASE_CONNECTION_LIMIT=15` 2. Reduce connection limit: `DATABASE_CONNECTION_LIMIT=15`
3. Implement retry logic (already done) 3. Implement retry logic (already done)
4. Check Neon dashboard for database status 4. Check Neon dashboard for database status
### "Connection terminated" ### "Connection terminated"
**Causes:** **Causes:**
- Idle connection timeout - Idle connection timeout
- Neon maintenance - Neon maintenance
- Long-running transactions - Long-running transactions
**Solutions:** **Solutions:**
1. Increase pool timeout: `DATABASE_POOL_TIMEOUT=30` 1. Increase pool timeout: `DATABASE_POOL_TIMEOUT=30`
2. Add connection cycling 2. Add connection cycling
3. Break large operations into smaller batches 3. Break large operations into smaller batches
### "Missed cron execution" ### "Missed cron execution"
**Causes:** **Causes:**
- Blocking database operations - Blocking database operations
- Scheduler overlap - Scheduler overlap
- High CPU usage - High CPU usage
**Solutions:** **Solutions:**
1. Reduce scheduler frequency 1. Reduce scheduler frequency
2. Add concurrency limits 2. Add concurrency limits
3. Monitor scheduler execution time 3. Monitor scheduler execution time
## Recommended Production Settings ## Recommended Production Settings
@ -223,17 +223,17 @@ SESSION_PROCESSING_INTERVAL="0 */2 * * *"
## Next Steps ## Next Steps
1. **Immediate**: Apply the new environment variables 1. **Immediate**: Apply the new environment variables
2. **Short-term**: Monitor connection usage via health endpoint 2. **Short-term**: Monitor connection usage via health endpoint
3. **Long-term**: Consider upgrading to Neon Pro for more connections 3. **Long-term**: Consider upgrading to Neon Pro for more connections
4. **Optional**: Implement read replicas for analytics queries 4. **Optional**: Implement read replicas for analytics queries
## Monitoring Checklist ## Monitoring Checklist
- [ ] Check Neon dashboard for connection spikes - [ ] Check Neon dashboard for connection spikes
- [ ] Monitor scheduler execution times - [ ] Monitor scheduler execution times
- [ ] Review error logs for connection patterns - [ ] Review error logs for connection patterns
- [ ] Test health endpoint regularly - [ ] Test health endpoint regularly
- [ ] Set up alerts for connection failures - [ ] Set up alerts for connection failures
With these optimizations, your Neon database connections should be much more stable and efficient! With these optimizations, your Neon database connections should be much more stable and efficient!

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