86 lines
3.7 KiB
TypeScript
86 lines
3.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 { listApprovals, decideApproval, revokeApproval } from '@/lib/admin/plugin-approvals';
|
|
|
|
/**
|
|
* Admin-protected CRUD for the per-(pluginId, bundleHash) approval table.
|
|
*
|
|
* GET /api/admin/plugin-approvals → list all entries
|
|
* POST /api/admin/plugin-approvals → { pluginId, bundleHash, decision: 'approved'|'denied' }
|
|
* DELETE /api/admin/plugin-approvals?pluginId=…&bundleHash=… → revoke
|
|
*/
|
|
|
|
function isValidId(s: unknown): s is string {
|
|
return typeof s === 'string' && /^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(s) && s.length <= 64;
|
|
}
|
|
function isValidHash(s: unknown): s is string {
|
|
return typeof s === 'string' && /^[a-f0-9]{16,128}$/i.test(s);
|
|
}
|
|
|
|
export async function GET() {
|
|
try {
|
|
const result = await requireAdminAuth();
|
|
if ('error' in result) return result.error;
|
|
const entries = await listApprovals();
|
|
return NextResponse.json({ entries }, { headers: { 'Cache-Control': 'no-store' } });
|
|
} catch (err) {
|
|
logger.error('plugin-approvals GET', { error: err instanceof Error ? err.message : String(err) });
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const result = await requireAdminAuth();
|
|
if ('error' in result) return result.error;
|
|
// AdminSessionPayload carries only role/iat/exp; we use a stable label
|
|
// for the audit trail rather than a per-user identity.
|
|
const adminUser = 'admin';
|
|
void result;
|
|
const ip = getClientIP(request);
|
|
|
|
let body: unknown;
|
|
try { body = await request.json(); } catch { body = null; }
|
|
const b = (body ?? {}) as { pluginId?: unknown; bundleHash?: unknown; decision?: unknown };
|
|
if (!isValidId(b.pluginId) || !isValidHash(b.bundleHash)) {
|
|
return NextResponse.json({ error: 'invalid pluginId or bundleHash' }, { status: 400 });
|
|
}
|
|
if (b.decision !== 'approved' && b.decision !== 'denied') {
|
|
return NextResponse.json({ error: 'decision must be "approved" or "denied"' }, { status: 400 });
|
|
}
|
|
|
|
const entry = await decideApproval(b.pluginId, b.bundleHash, b.decision, adminUser);
|
|
await auditLog('plugin.approval', { pluginId: entry.pluginId, bundleHash: entry.bundleHash, decision: entry.status }, ip);
|
|
return NextResponse.json({ entry });
|
|
} catch (err) {
|
|
logger.error('plugin-approvals POST', { error: err instanceof Error ? err.message : String(err) });
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function DELETE(request: NextRequest) {
|
|
try {
|
|
const result = await requireAdminAuth();
|
|
if ('error' in result) return result.error;
|
|
// AdminSessionPayload carries only role/iat/exp; we use a stable label
|
|
// for the audit trail rather than a per-user identity.
|
|
const adminUser = 'admin';
|
|
void result;
|
|
const ip = getClientIP(request);
|
|
|
|
const pluginId = request.nextUrl.searchParams.get('pluginId');
|
|
const bundleHash = request.nextUrl.searchParams.get('bundleHash');
|
|
if (!isValidId(pluginId) || !isValidHash(bundleHash)) {
|
|
return NextResponse.json({ error: 'invalid pluginId or bundleHash' }, { status: 400 });
|
|
}
|
|
await revokeApproval(pluginId, bundleHash);
|
|
await auditLog('plugin.approval.revoke', { pluginId, bundleHash, by: adminUser }, ip);
|
|
return NextResponse.json({ ok: true });
|
|
} catch (err) {
|
|
logger.error('plugin-approvals DELETE', { error: err instanceof Error ? err.message : String(err) });
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
}
|
|
}
|