fix(audit): sécurité + perf + DB — 40 problèmes adressés
SÉCURITÉ (CRITIQUE): - link-preview: auth() obligatoire + filtre IP privées (SSRF fix) - metrics: !metricsToken → 401 au lieu d'etre ouvert si token absent - contextual-ai-chat: DOMPurify.sanitize() sur dangerouslySetInnerHTML (XSS fix) PERFORMANCE: - graph/route.ts: take:500 + orderBy updatedAt desc (pas de full table scan) - syncAllEmbeddings: batch parallèle Promise.allSettled × 5 (pas séquentiel) - 80 console.log supprimés du code production BASE DE DONNÉES: - Note: index contentUpdatedAt + isPublic ajoutés au schema - DocumentChunk: commentaire index vectoriel HNSW (à exécuter manuellement)
This commit is contained in:
@@ -31,7 +31,6 @@ interface SuggestChartsResponse {
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
console.log('[suggest-charts] ===== REQUEST START =====')
|
||||
|
||||
// 1. Auth check
|
||||
const session = await auth()
|
||||
@@ -47,16 +46,13 @@ export async function POST(req: Request) {
|
||||
})
|
||||
}
|
||||
const userId = session.user.id
|
||||
console.log('[suggest-charts] userId:', userId)
|
||||
|
||||
// 1.5 Quota check
|
||||
try {
|
||||
const sysConfigEarly = await getSystemConfig()
|
||||
const { usedByok: willUseByok } = await willUseByokForLane('chat', sysConfigEarly, userId)
|
||||
console.log('[suggest-charts] BYOK:', willUseByok)
|
||||
if (!willUseByok) {
|
||||
await reserveUsageOrThrow(userId, 'suggest_charts')
|
||||
console.log('[suggest-charts] Quota OK')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[suggest-charts] QUOTA ERROR:', err)
|
||||
@@ -84,7 +80,6 @@ export async function POST(req: Request) {
|
||||
} satisfies SuggestChartsResponse, { status: 400 })
|
||||
}
|
||||
const { content, selection, noteId } = body
|
||||
console.log('[suggest-charts] contentLen:', content?.length, 'selectionLen:', selection?.length)
|
||||
|
||||
if (!content || content.trim().length === 0) {
|
||||
console.error('[suggest-charts] EMPTY CONTENT')
|
||||
@@ -98,7 +93,6 @@ export async function POST(req: Request) {
|
||||
}
|
||||
|
||||
const textToAnalyze = selection && selection.trim() ? selection.trim() : content.trim()
|
||||
console.log('[suggest-charts] analyzeLen:', textToAnalyze.length, 'preview:', textToAnalyze.substring(0, 100))
|
||||
|
||||
// 3. Build AI context
|
||||
let provider, model
|
||||
@@ -106,7 +100,6 @@ export async function POST(req: Request) {
|
||||
const sysConfig = await getSystemConfig()
|
||||
provider = getChatProvider(sysConfig)
|
||||
model = provider.getModel()
|
||||
console.log('[suggest-charts] AI model:', model)
|
||||
} catch (e) {
|
||||
console.error('[suggest-charts] AI ROUTE ERROR:', e)
|
||||
return Response.json({
|
||||
@@ -154,7 +147,6 @@ Response format (COPY this structure):
|
||||
})
|
||||
|
||||
// 5. Parse AI response - be very lenient
|
||||
console.log('[suggest-charts] AI response:', text.substring(0, 500))
|
||||
|
||||
let parsed: SuggestChartsResponse
|
||||
try {
|
||||
|
||||
@@ -56,7 +56,6 @@ export async function POST(req: NextRequest) {
|
||||
// If notebookId is provided, use contextual suggestions (IA2)
|
||||
if (notebookId) {
|
||||
try {
|
||||
console.log('[/api/ai/tags] contextual request — notebookId:', notebookId, 'content length:', content.length)
|
||||
const suggestions = await contextualAutoTagService.suggestLabels(
|
||||
content,
|
||||
notebookId,
|
||||
@@ -64,7 +63,6 @@ export async function POST(req: NextRequest) {
|
||||
language
|
||||
);
|
||||
|
||||
console.log('[/api/ai/tags] suggestions:', suggestions.length, suggestions.map(s => `${s.label}(${s.confidence})`))
|
||||
|
||||
const convertedTags = suggestions.map(s => ({
|
||||
tag: s.label,
|
||||
|
||||
@@ -75,7 +75,6 @@ async function buildPptx(spec: any): Promise<Buffer> {
|
||||
}
|
||||
|
||||
case 'bullets': {
|
||||
console.log('[PPTX] Rendering bullets slide:', slide.title, 'items:', slide.items?.length)
|
||||
s.background = { color: p.bg }
|
||||
s.addShape(pptx.ShapeType.rect, {
|
||||
x: 0, y: 0, w: 0.06, h: H,
|
||||
@@ -90,7 +89,6 @@ async function buildPptx(spec: any): Promise<Buffer> {
|
||||
fill: { color: p.accent }, line: { type: 'none' },
|
||||
})
|
||||
const items = slide.items ?? []
|
||||
console.log('[PPTX] Bullet items:', items.length, items[0])
|
||||
// Use simpler bullet format - each item as separate text call with bullet option
|
||||
items.forEach((item: string, i: number) => {
|
||||
s.addText(item, {
|
||||
|
||||
@@ -4,10 +4,8 @@ import { prisma } from '@/lib/prisma'
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const canvasId = req.nextUrl.searchParams.get('id')
|
||||
console.log('[Slides API] Request for id:', canvasId)
|
||||
|
||||
if (!canvasId) {
|
||||
console.log('[Slides API] ERROR: Missing id')
|
||||
return new Response(JSON.stringify({ error: 'Missing id' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -19,39 +17,31 @@ export async function GET(req: NextRequest) {
|
||||
})
|
||||
|
||||
if (!canvas) {
|
||||
console.log('[Slides API] ERROR: Canvas not found for id:', canvasId)
|
||||
return new Response(JSON.stringify({ error: 'Not found' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
console.log('[Slides API] Canvas found:', canvas.name, '| data length:', canvas.data?.length)
|
||||
console.log('[Slides API] Raw data start:', canvas.data?.substring(0, 200))
|
||||
|
||||
let parsed: any
|
||||
try {
|
||||
parsed = JSON.parse(canvas.data)
|
||||
} catch (parseErr) {
|
||||
console.log('[Slides API] ERROR: JSON parse failed:', parseErr)
|
||||
return new Response(JSON.stringify({ error: 'Invalid data' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
console.log('[Slides API] Parsed type:', parsed.type, '| has html:', !!parsed.html, '| html length:', parsed.html?.length)
|
||||
console.log('[Slides API] HTML start:', parsed.html?.substring(0, 150))
|
||||
|
||||
if (parsed.type !== 'slides' || !parsed.html) {
|
||||
console.log('[Slides API] ERROR: Not a slides canvas. type:', parsed.type, 'html exists:', !!parsed.html)
|
||||
return new Response(JSON.stringify({ error: 'Not a slides canvas' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
console.log('[Slides API] SUCCESS: Returning HTML, length:', parsed.html.length)
|
||||
|
||||
return new Response(parsed.html, {
|
||||
status: 200,
|
||||
|
||||
@@ -47,14 +47,12 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ success: true, executed: 0 })
|
||||
}
|
||||
|
||||
console.log(`[CronAgents] Found ${dueAgents.length} due agent(s): ${dueAgents.map(a => a.id).join(', ')}`)
|
||||
|
||||
const results: { id: string; success: boolean; error?: string }[] = []
|
||||
|
||||
// Execute agents sequentially (max 3 per cycle)
|
||||
for (const agent of dueAgents.slice(0, 3)) {
|
||||
try {
|
||||
console.log(`[CronAgents] Executing agent ${agent.id} (${agent.frequency})`)
|
||||
const { executeAgent } = await import('@/lib/ai/services/agent-executor.service')
|
||||
const result = await executeAgent(agent.id, agent.userId)
|
||||
|
||||
@@ -66,7 +64,6 @@ export async function POST(request: NextRequest) {
|
||||
timezone: agent.timezone,
|
||||
})
|
||||
|
||||
console.log(`[CronAgents] Agent ${agent.id} done. success=${result.success}, nextRun=${nextRun?.toISOString() ?? 'null'}`)
|
||||
|
||||
await prisma.agent.update({
|
||||
where: { id: agent.id },
|
||||
|
||||
@@ -56,6 +56,8 @@ export async function GET(request: NextRequest) {
|
||||
labelRelations: { select: { id: true } },
|
||||
notebook: { select: { id: true, name: true } },
|
||||
},
|
||||
take: 500,
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
})
|
||||
|
||||
if (notes.length === 0) return NextResponse.json({ nodes: [], edges: [] })
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const url = req.nextUrl.searchParams.get('url')
|
||||
if (!url) {
|
||||
return NextResponse.json({ error: 'Missing url' }, { status: 400 })
|
||||
@@ -12,6 +18,36 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'Invalid protocol' }, { status: 400 })
|
||||
}
|
||||
|
||||
const hostname = parsed.hostname
|
||||
if (
|
||||
hostname === 'localhost' ||
|
||||
hostname === '127.0.0.1' ||
|
||||
hostname.startsWith('10.') ||
|
||||
hostname.startsWith('192.168.') ||
|
||||
hostname.startsWith('169.254.') ||
|
||||
hostname.startsWith('172.16.') ||
|
||||
hostname.startsWith('172.17.') ||
|
||||
hostname.startsWith('172.18.') ||
|
||||
hostname.startsWith('172.19.') ||
|
||||
hostname.startsWith('172.20.') ||
|
||||
hostname.startsWith('172.21.') ||
|
||||
hostname.startsWith('172.22.') ||
|
||||
hostname.startsWith('172.23.') ||
|
||||
hostname.startsWith('172.24.') ||
|
||||
hostname.startsWith('172.25.') ||
|
||||
hostname.startsWith('172.26.') ||
|
||||
hostname.startsWith('172.27.') ||
|
||||
hostname.startsWith('172.28.') ||
|
||||
hostname.startsWith('172.29.') ||
|
||||
hostname.startsWith('172.30.') ||
|
||||
hostname.startsWith('172.31.') ||
|
||||
hostname === '0.0.0.0' ||
|
||||
hostname.endsWith('.local') ||
|
||||
hostname.endsWith('.internal')
|
||||
) {
|
||||
return NextResponse.json({ error: 'Blocked' }, { status: 403 })
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 8000)
|
||||
|
||||
|
||||
@@ -7,12 +7,13 @@ export const dynamic = 'force-dynamic'
|
||||
export async function GET(req: Request) {
|
||||
// Secure endpoint with bearer token (METRICS_TOKEN env var)
|
||||
const metricsToken = process.env.METRICS_TOKEN
|
||||
if (metricsToken) {
|
||||
const authHeader = req.headers.get('authorization') ?? ''
|
||||
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : ''
|
||||
if (token !== metricsToken) {
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
}
|
||||
if (!metricsToken) {
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
}
|
||||
const authHeader = req.headers.get('authorization') ?? ''
|
||||
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : ''
|
||||
if (token !== metricsToken) {
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
}
|
||||
|
||||
const lines: string[] = []
|
||||
|
||||
@@ -45,7 +45,6 @@ export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const result: FetchModelsResult = await fetchLiveModelsForProvider(provider, apiKey, baseUrl);
|
||||
|
||||
console.log(`[verify] ${provider}: fromApi=${result.fromApi}, models=${result.models.length}`, result.models);
|
||||
|
||||
// Only consider key valid if we got models from the REAL API (not fallbacks)
|
||||
// Exception: Anthropic/Google/custom don't guarantee public /models endpoints, so we accept fallbacks for them
|
||||
|
||||
Reference in New Issue
Block a user