247 lines
6.8 KiB
TypeScript
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);
|
|
}
|
|
}
|