feat: allow file uploads on the wizard branding step

This commit is contained in:
Linus Rath
2026-05-09 21:40:39 +02:00
parent 1dcdeeae86
commit 876ea370e4
5 changed files with 392 additions and 31 deletions
+7 -4
View File
@@ -1,8 +1,11 @@
import { NextRequest, NextResponse } from 'next/server';
import { readFile, stat } from 'node:fs/promises';
import path from 'node:path';
import { getConfigDir } from '@/lib/admin/paths';
const BRANDING_DIR = path.join(process.cwd(), 'data', 'admin', 'branding');
function getBrandingDir(): string {
return path.join(getConfigDir(), 'branding');
}
const MIME_TYPES: Record<string, string> = {
'.svg': 'image/svg+xml',
@@ -38,11 +41,11 @@ export async function GET(
return NextResponse.json({ error: 'Unsupported file type' }, { status: 400 });
}
const filePath = path.join(BRANDING_DIR, safe);
const filePath = path.join(getBrandingDir(), safe);
// Ensure resolved path is still within BRANDING_DIR
// Ensure resolved path is still within getBrandingDir()
const resolved = path.resolve(filePath);
if (!resolved.startsWith(path.resolve(BRANDING_DIR))) {
if (!resolved.startsWith(path.resolve(getBrandingDir()))) {
return NextResponse.json({ error: 'Invalid filename' }, { status: 400 });
}
+8 -5
View File
@@ -2,12 +2,15 @@ import { NextRequest, NextResponse } from 'next/server';
import { requireAdminAuth, getClientIP } from '@/lib/admin/session';
import { auditLog } from '@/lib/admin/audit';
import { configManager } from '@/lib/admin/config-manager';
import { getConfigDir } from '@/lib/admin/paths';
import { logger } from '@/lib/logger';
import { writeFile, unlink, mkdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import path from 'node:path';
const BRANDING_DIR = path.join(process.cwd(), 'data', 'admin', 'branding');
function getBrandingDir(): string {
return path.join(getConfigDir(), 'branding');
}
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2 MB
const ALLOWED_MIME_TYPES = new Set([
'image/svg+xml',
@@ -79,11 +82,11 @@ export async function POST(request: NextRequest) {
};
const ext = extMap[file.type] || '.png';
const safeName = sanitizeFilename(`${slot}${ext}`);
const filePath = path.join(BRANDING_DIR, safeName);
const filePath = path.join(getBrandingDir(), safeName);
// Ensure branding directory exists
if (!existsSync(BRANDING_DIR)) {
await mkdir(BRANDING_DIR, { recursive: true });
if (!existsSync(getBrandingDir())) {
await mkdir(getBrandingDir(), { recursive: true });
}
// Write file to disk
@@ -125,7 +128,7 @@ export async function DELETE(request: NextRequest) {
const possibleExts = ['.svg', '.png', '.jpg', '.webp', '.ico'];
let removed = false;
for (const ext of possibleExts) {
const filePath = path.join(BRANDING_DIR, `${slot}${ext}`);
const filePath = path.join(getBrandingDir(), `${slot}${ext}`);
if (existsSync(filePath)) {
await unlink(filePath);
removed = true;