homelab-dashboard/app/api/config/[[...path]]/route.ts
Bilal Teke 69c2057252 v4
2026-04-20 14:34:07 +02:00

247 lines
6.8 KiB
TypeScript

import { auth } from '@/auth';
import { NextResponse } from 'next/server';
import { createBackup, listBackups, restoreBackup } from '@/src/lib/config/backup-config';
import { backupConfig, exportConfig, importConfig, loadFullConfig, saveAppConfig, saveServices } from '@/src/lib/config';
import { appConfigSchema, servicesArraySchema } from '@/src/lib/config/schema';
import { CONFIG_FILE_PATH } from '@/src/lib/config/paths';
import { resolveProjectPath } from '@/src/lib/config/load-config';
import { promises as fs } from 'fs';
import path from 'path';
const CONFIG_PATH = CONFIG_FILE_PATH;
function jsonError(message: string, status: number) {
return NextResponse.json({ error: message }, { status });
}
function normalizePath(pathname: string) {
return pathname.replace(/\/+$/, '');
}
async function requireAuth() {
const session = await auth();
if (!session) {
return null;
}
return session;
}
export async function GET(request: Request) {
const pathname = new URL(request.url).pathname;
const cleanPath = normalizePath(pathname);
if (cleanPath === '/api/config') {
const session = await requireAuth();
if (!session) {
return jsonError('Unauthorized', 401);
}
try {
const result = await loadFullConfig();
return NextResponse.json(result);
} catch (error) {
console.error('GET /api/config failed:', error);
return jsonError('Failed to load configuration', 500);
}
}
if (cleanPath === '/api/config/backups') {
const session = await requireAuth();
if (!session) {
return jsonError('Unauthorized', 401);
}
try {
const backups = await listBackups();
return NextResponse.json({ backups });
} catch (error) {
console.error('GET /api/config/backups failed:', error);
return jsonError('Failed to list backups', 500);
}
}
if (cleanPath === '/api/config/export') {
const session = await requireAuth();
if (!session) {
return jsonError('Unauthorized', 401);
}
try {
const payload = await exportConfig();
const body = JSON.stringify(payload, null, 2);
return new NextResponse(body, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="homelab-config-export-${new Date().toISOString().replace(/[:.]/g, '-')}.json"`,
},
});
} catch (error) {
console.error('GET /api/config/export failed:', error);
return jsonError('Failed to export configuration', 500);
}
}
return jsonError('Not found', 404);
}
export async function POST(request: Request) {
const session = await requireAuth();
if (!session) {
return jsonError('Unauthorized', 401);
}
const pathname = new URL(request.url).pathname;
const cleanPath = normalizePath(pathname);
if (cleanPath === '/api/config/backup') {
try {
const backupFile = await createBackup();
return NextResponse.json({
success: true,
message: 'Backup created successfully',
backupFilename: path.basename(backupFile),
});
} catch (error) {
console.error('POST /api/config/backup failed:', error);
return jsonError(
error instanceof Error ? error.message : 'Failed to create backup',
500
);
}
}
if (cleanPath === '/api/config/import') {
let payload: unknown;
try {
const contentType = request.headers.get('content-type') || '';
if (contentType.includes('multipart/form-data')) {
const formData = await request.formData();
const file = formData.get('file');
if (!(file instanceof File)) {
return jsonError('No file uploaded', 400);
}
const fileText = await file.text();
payload = JSON.parse(fileText);
} else {
payload = await request.json();
}
} catch (error) {
return jsonError(
'Request body must be valid JSON or a valid JSON upload',
400
);
}
try {
await importConfig(payload);
return NextResponse.json({
success: true,
message: 'Configuration imported successfully',
});
} catch (error) {
console.error('POST /api/config/import failed:', error);
return jsonError(
error instanceof Error ? error.message : 'Failed to import configuration',
400
);
}
}
if (cleanPath === '/api/config/restore') {
let payload: any;
try {
payload = await request.json();
} catch (error) {
return jsonError('Request body must be valid JSON', 400);
}
const { backupFilename } = payload;
if (!backupFilename || typeof backupFilename !== 'string') {
return jsonError('backupFilename is required and must be a string', 400);
}
if (
!backupFilename.startsWith('config-backup-') ||
!backupFilename.endsWith('.json') ||
backupFilename.includes('/') ||
backupFilename.includes('\\')
) {
return jsonError('Invalid backup filename format', 400);
}
try {
await restoreBackup(backupFilename);
return NextResponse.json({
success: true,
message: 'Configuration restored successfully',
});
} catch (error) {
console.error('POST /api/config/restore failed:', error);
return jsonError(
error instanceof Error ? error.message : 'Failed to restore backup',
500
);
}
}
return jsonError('Not found', 404);
}
export async function PUT(request: Request) {
const session = await requireAuth();
if (!session) {
return jsonError('Unauthorized', 401);
}
const pathname = new URL(request.url).pathname;
const cleanPath = normalizePath(pathname);
if (cleanPath !== '/api/config') {
return jsonError('Not found', 404);
}
let payload: any;
try {
payload = await request.json();
} catch (error) {
return jsonError('Request body must be valid JSON', 400);
}
const appConfigResult = appConfigSchema.safeParse(payload.appConfig);
if (!appConfigResult.success) {
return jsonError(
`App config validation failed: ${appConfigResult.error.message}`,
400
);
}
const servicesResult = servicesArraySchema.safeParse(payload.services);
if (!servicesResult.success) {
return jsonError(
`Services validation failed: ${servicesResult.error.message}`,
400
);
}
try {
await fs.access(CONFIG_PATH);
await backupConfig();
} catch (error) {
// ignore missing config file
}
try {
const servicesPath = resolveProjectPath(appConfigResult.data.servicesFile);
await saveAppConfig(appConfigResult.data, CONFIG_PATH);
await saveServices(servicesResult.data, servicesPath);
return NextResponse.json({ success: true });
} catch (error) {
console.error('PUT /api/config failed:', error);
return jsonError('Failed to save configuration', 500);
}
}