Files
Momento/memento-note/app/api/uploads/[...path]/route.ts
Antigravity 45297da333 fix: images, couverture, slash, agents, wizard, Stripe et admin
- Collage/accès images: ownership path userId + meta legacy, 403 non caché
- Couverture note: explicite (choix/aucune), plus d’auto depuis le corps
- Éditeur: suppression image TipTap, sync immédiate, toolbar réordonnée
- Slash: listes par titre (plus d’index), keywords nettoyés
- Agents: nextRun à la réactivation, cron sans stampede null
- Wizard: titres courts + mode Expert plus robuste
- Stripe checkout hosted fiable; admin métriques live; dark mode IA/settings
- Indexation notes courtes + test unit aligné
2026-07-16 16:58:07 +00:00

82 lines
2.4 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { readFile, stat } from 'fs/promises'
import path from 'path'
import { auth } from '@/auth'
import { canAccessUploadedNoteImage } from '@/lib/upload-access'
const UPLOAD_DIR = path.join(process.cwd(), 'data', 'uploads')
const MIME_MAP: Record<string, string> = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
}
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const session = await auth()
const { path: segments } = await params
// Only serve from uploads/notes/ (flat or userId/file)
if (!segments.length || segments[0] !== 'notes') {
return new NextResponse('Not found', { status: 404 })
}
// Reject path traversal
if (segments.some((s) => s === '..' || s.includes('\0'))) {
return new NextResponse('Forbidden', { status: 403 })
}
const filename = segments[segments.length - 1]
// Never serve meta sidecars as images
if (filename.endsWith('.meta.json')) {
return new NextResponse('Not found', { status: 404 })
}
const allowed = await canAccessUploadedNoteImage(segments, session?.user?.id)
if (!allowed) {
return new NextResponse(session?.user?.id ? 'Forbidden' : 'Unauthorized', {
status: session?.user?.id ? 403 : 401,
headers: {
// Do not let browsers / CDNs cache a denial (would stick a broken image)
'Cache-Control': 'no-store',
},
})
}
const ext = path.extname(filename).toLowerCase()
const contentType = MIME_MAP[ext]
if (!contentType) {
return new NextResponse('Unsupported file type', { status: 400 })
}
const safePath = path.join(UPLOAD_DIR, ...segments)
if (!safePath.startsWith(UPLOAD_DIR + path.sep) && safePath !== UPLOAD_DIR) {
return new NextResponse('Forbidden', { status: 403 })
}
try {
const fileStats = await stat(safePath)
if (!fileStats.isFile()) {
return new NextResponse('Not found', { status: 404 })
}
const buffer = await readFile(safePath)
return new NextResponse(buffer, {
headers: {
'Content-Type': contentType,
// private: only for authenticated session fetches; still long cache after auth ok
'Cache-Control': 'private, max-age=31536000, immutable',
'Content-Length': String(buffer.length),
},
})
} catch {
return new NextResponse('Not found', { status: 404 })
}
}