mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 10:52:08 +01:00
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:
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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!
|
||||||
|
|||||||
48
lib/csp.ts
48
lib/csp.ts
@ -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) =>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user