mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
163 lines
5.7 KiB
TypeScript
163 lines
5.7 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { requireAdminAuth, getClientIP } from '@/lib/admin/session';
|
|
import { auditLog } from '@/lib/admin/audit';
|
|
import { logger } from '@/lib/logger';
|
|
import {
|
|
getTheme,
|
|
getThemeRegistry,
|
|
saveTheme,
|
|
deleteTheme as removeTheme,
|
|
themeCSSResolver,
|
|
type ServerTheme,
|
|
} from '@/lib/admin/plugin-registry';
|
|
import { forgetExtensionInPolicy } from '@/lib/admin/extension-cleanup';
|
|
import { keptVersions, listHistory } from '@/lib/admin/extension-history';
|
|
|
|
import { readThemePackage } from '@/lib/extension-package';
|
|
import { configReadOnlyResponse } from '@/lib/admin/config-read-only-response';
|
|
|
|
/**
|
|
* GET /api/admin/themes - List all admin-managed themes
|
|
*/
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const result = await requireAdminAuth(request);
|
|
if ('error' in result) return result.error;
|
|
|
|
const registry = await getThemeRegistry();
|
|
const themes = await Promise.all(registry.themes.map(async t => ({
|
|
...t,
|
|
...keptVersions(await listHistory('theme', t.id).catch(() => [])),
|
|
})));
|
|
return NextResponse.json(themes, {
|
|
headers: { 'Cache-Control': 'no-store' },
|
|
});
|
|
} catch (error) {
|
|
logger.error('Theme list error', { error: error instanceof Error ? error.message : 'Unknown error' });
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/admin/themes - Upload and install a theme ZIP
|
|
*/
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const result = await requireAdminAuth(request);
|
|
if ('error' in result) return result.error;
|
|
|
|
const ip = getClientIP(request);
|
|
const formData = await request.formData();
|
|
const file = formData.get('file') as File | null;
|
|
|
|
if (!file) {
|
|
return NextResponse.json({ error: 'Missing file' }, { status: 400 });
|
|
}
|
|
|
|
const read = await readThemePackage(await file.arrayBuffer(), {
|
|
resolveExtends: await themeCSSResolver(),
|
|
});
|
|
if (!read.valid || !read.manifest) {
|
|
return NextResponse.json({ error: read.errors.join('; ') }, { status: 400 });
|
|
}
|
|
const { manifest, css, skin, warnings } = read;
|
|
|
|
const existing = await getTheme(manifest.id);
|
|
const now = new Date().toISOString();
|
|
const theme: ServerTheme = {
|
|
id: manifest.id,
|
|
name: manifest.name,
|
|
version: manifest.version,
|
|
author: manifest.author,
|
|
description: manifest.description || '',
|
|
variants: manifest.variants,
|
|
enabled: existing?.enabled ?? true,
|
|
...(existing?.forceEnabled !== undefined ? { forceEnabled: existing.forceEnabled } : {}),
|
|
installedAt: existing?.installedAt ?? now,
|
|
updatedAt: now,
|
|
source: { kind: 'upload' },
|
|
};
|
|
|
|
await saveTheme(theme, css, skin);
|
|
await auditLog('theme.install', { id: theme.id, name: theme.name, version: theme.version, warnings }, ip);
|
|
|
|
return NextResponse.json({ theme, warnings });
|
|
} catch (error) {
|
|
const readOnly = configReadOnlyResponse(error);
|
|
if (readOnly) return readOnly;
|
|
logger.error('Theme install error', { error: error instanceof Error ? error.message : 'Unknown error' });
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PATCH /api/admin/themes - Update theme metadata (enable/disable)
|
|
* Body: { id: string, enabled: boolean }
|
|
*/
|
|
export async function PATCH(request: NextRequest) {
|
|
try {
|
|
const result = await requireAdminAuth(request);
|
|
if ('error' in result) return result.error;
|
|
|
|
const ip = getClientIP(request);
|
|
const { id, enabled, forceEnabled } = await request.json();
|
|
|
|
if (!id || typeof id !== 'string') {
|
|
return NextResponse.json({ error: 'Missing theme id' }, { status: 400 });
|
|
}
|
|
if (typeof enabled !== 'boolean' && typeof forceEnabled !== 'boolean') {
|
|
return NextResponse.json({ error: 'enabled or forceEnabled must be a boolean' }, { status: 400 });
|
|
}
|
|
|
|
const updates: { enabled?: boolean; forceEnabled?: boolean } = {};
|
|
if (typeof enabled === 'boolean') updates.enabled = enabled;
|
|
if (typeof forceEnabled === 'boolean') updates.forceEnabled = forceEnabled;
|
|
|
|
const { updateThemeMeta } = await import('@/lib/admin/plugin-registry');
|
|
const updated = await updateThemeMeta(id, updates);
|
|
if (!updated) {
|
|
return NextResponse.json({ error: 'Theme not found' }, { status: 404 });
|
|
}
|
|
|
|
await auditLog('theme.update', { id, ...updates }, ip);
|
|
return NextResponse.json({ theme: updated });
|
|
} catch (error) {
|
|
const readOnly = configReadOnlyResponse(error);
|
|
if (readOnly) return readOnly;
|
|
logger.error('Theme update error', { error: error instanceof Error ? error.message : 'Unknown error' });
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DELETE /api/admin/themes - Remove a theme
|
|
* Body: { id: string }
|
|
*/
|
|
export async function DELETE(request: NextRequest) {
|
|
try {
|
|
const result = await requireAdminAuth(request);
|
|
if ('error' in result) return result.error;
|
|
|
|
const ip = getClientIP(request);
|
|
const { id } = await request.json();
|
|
|
|
if (!id || typeof id !== 'string') {
|
|
return NextResponse.json({ error: 'Missing theme id' }, { status: 400 });
|
|
}
|
|
|
|
const removed = await removeTheme(id);
|
|
if (!removed) {
|
|
return NextResponse.json({ error: 'Theme not found' }, { status: 404 });
|
|
}
|
|
await forgetExtensionInPolicy('theme', id);
|
|
|
|
await auditLog('theme.delete', { id }, ip);
|
|
return NextResponse.json({ success: true });
|
|
} catch (error) {
|
|
const readOnly = configReadOnlyResponse(error);
|
|
if (readOnly) return readOnly;
|
|
logger.error('Theme delete error', { error: error instanceof Error ? error.message : 'Unknown error' });
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
}
|
|
}
|