import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { NextResponse } from 'next/server'; import { maxRetentionSeconds, maxUploadBytes, publicBaseUrl, shareDir, uploadTtlSeconds, } from '@/src/lib/config.js'; import { get, logEvent, run, runCleanupIfNeeded } from '@/src/lib/db.js'; import { safeBaseName } from '@/src/lib/files.js'; import { sendUploadRequestCompletedMail } from '@/src/lib/mailer.js'; import { streamMultipartToPath } from '@/src/lib/multipart.js'; import { getRequestMeta } from '@/src/lib/security.js'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; function normalizeRequestId(value) { return String(value || '').trim().toUpperCase(); } function isValidRequestId(value) { return /^[A-Z2-7]{6,24}$/.test(value); } function requestPageHref(requestId, params = {}) { const query = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value) { query.set(key, String(value)); } } const encodedId = encodeURIComponent(requestId); const serialized = query.toString(); return serialized ? `/_request/${encodedId}?${serialized}` : `/_request/${encodedId}`; } function redirectToRequest(requestId, params = {}) { return new NextResponse(null, { status: 303, headers: { location: requestPageHref(requestId, params), }, }); } function firstForwardedPart(value) { if (!value) { return ''; } return value.split(',')[0].trim(); } function requestOrigin(request) { const forwardedHost = firstForwardedPart(request.headers.get('x-forwarded-host')); const host = forwardedHost || request.headers.get('host') || ''; if (!host) { return ''; } const forwardedProto = firstForwardedPart(request.headers.get('x-forwarded-proto')); const proto = forwardedProto || (host.startsWith('localhost') || host.startsWith('127.0.0.1') ? 'http' : 'https'); return `${proto}://${host}`; } function resolvePublicOrigin(request) { if (publicBaseUrl) { return publicBaseUrl; } const fromHeaders = requestOrigin(request); if (fromHeaders) { return fromHeaders; } try { return new URL(request.url).origin; } catch { return ''; } } function toBase32(buffer) { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = 0; let value = 0; let output = ''; for (const byte of buffer) { value = (value << 8) | byte; bits += 8; while (bits >= 5) { output += alphabet[(value >>> (bits - 5)) & 31]; bits -= 5; } } if (bits > 0) { output += alphabet[(value << (5 - bits)) & 31]; } return output; } function createRandomId() { return toBase32(crypto.randomBytes(5)); } export async function POST(request, { params }) { await runCleanupIfNeeded(); const resolvedParams = await params; const requestId = normalizeRequestId(resolvedParams.id); if (!isValidRequestId(requestId)) { return new NextResponse('Ungültige Anfrage-ID', { status: 400 }); } const uploadRequest = await get( `SELECT id, owner, note, expires_at, completed_at FROM upload_requests WHERE id = ?`, [requestId] ); if (!uploadRequest) { return redirectToRequest(requestId, { error: 'Anfrage nicht gefunden.' }); } if (Number(uploadRequest.completed_at || 0) > 0) { return redirectToRequest(requestId, { success: 'Diese Anfrage wurde bereits erfüllt.' }); } const now = Date.now(); if (Number(uploadRequest.expires_at || 0) <= now) { return redirectToRequest(requestId, { error: 'Diese Anfrage ist bereits abgelaufen.' }); } let storedName = ''; let storedPath = ''; for (let attempts = 0; attempts < 12; attempts += 1) { const candidate = createRandomId(); const candidatePath = path.join(shareDir, candidate); try { await fs.promises.access(candidatePath, fs.constants.F_OK); } catch { storedName = candidate; storedPath = candidatePath; break; } } if (!storedName || !storedPath) { return redirectToRequest(requestId, { error: 'Upload-ID konnte nicht erzeugt werden.' }); } let parsed; try { parsed = await streamMultipartToPath(request, { fileFieldName: 'file', targetPath: storedPath, maxFileBytes: maxUploadBytes, }); } catch (error) { if (error && error.message === 'file-too-large') { return redirectToRequest(requestId, { error: `Datei überschreitet das Größenlimit (${maxUploadBytes} Bytes).`, }); } if (error && error.message === 'missing-file') { return redirectToRequest(requestId, { error: 'Keine Datei hochgeladen.' }); } return redirectToRequest(requestId, { error: 'Upload fehlgeschlagen.' }); } const fulfilledBy = String(parsed.fields.fulfilledBy || '').trim().slice(0, 200); const originalName = safeBaseName(parsed.file.filename, 'upload'); const uploadExpiry = Math.min(now + uploadTtlSeconds * 1000, now + maxRetentionSeconds * 1000); let uploadId = null; const sizeBytes = parsed.file.sizeBytes; try { const insertResult = await run( `INSERT INTO uploads (owner, original_name, stored_name, stored_path, size_bytes, uploaded_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [uploadRequest.owner, originalName, storedName, storedPath, sizeBytes, now, uploadExpiry] ); uploadId = insertResult.lastID; } catch { await fs.promises.rm(storedPath, { force: true }).catch(() => undefined); return redirectToRequest(requestId, { error: 'Upload fehlgeschlagen.' }); } const updateResult = await run( `UPDATE upload_requests SET completed_at = ?, upload_id = ?, uploaded_original_name = ?, uploaded_stored_name = ?, uploaded_stored_path = ?, uploaded_size_bytes = ?, fulfilled_by = ? WHERE id = ? AND completed_at IS NULL`, [now, uploadId, originalName, storedName, storedPath, sizeBytes, fulfilledBy || null, requestId] ); if (!updateResult || updateResult.changes < 1) { await run('DELETE FROM uploads WHERE id = ?', [uploadId]).catch(() => undefined); await fs.promises.rm(storedPath, { force: true }).catch(() => undefined); return redirectToRequest(requestId, { success: 'Diese Anfrage wurde bereits erfüllt.' }); } await logEvent( 'request_fulfilled', uploadRequest.owner, { request_id: requestId, upload_id: uploadId, stored_name: storedName, fulfilled_by: fulfilledBy || null }, await getRequestMeta() ); const baseUrl = resolvePublicOrigin(request); const downloadUrl = `${baseUrl}/_share/${encodeURIComponent(storedName)}`; const mailResult = await sendUploadRequestCompletedMail({ recipient: uploadRequest.owner, requestId, fileName: originalName, downloadUrl, requesterNote: uploadRequest.note || '', fulfilledBy, }); if (mailResult.ok) { await run('UPDATE upload_requests SET notification_sent_at = ? WHERE id = ?', [Date.now(), requestId]); return redirectToRequest(requestId, { success: 'Datei erfolgreich hochgeladen. Vielen Dank!' }); } await logEvent( 'request_mail_failed', uploadRequest.owner, { request_id: requestId, reason: mailResult.reason || 'unknown' }, await getRequestMeta() ); return redirectToRequest(requestId, { success: 'Datei hochgeladen. Hinweis: E-Mail-Benachrichtigung konnte nicht gesendet werden.', }); }