Files
SRCmail/app/api/plugin-approval-status/route.ts
T

90 lines
4.3 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { decryptSession } from '@/lib/auth/crypto';
import { sessionCookieName } from '@/lib/auth/session-cookie';
import { readStalwartAuthContextFromStore } from '@/lib/stalwart/auth-context';
import { MAX_ACCOUNT_SLOTS } from '@/lib/account-utils';
import { logger } from '@/lib/logger';
import { getApprovalStatus, requestApproval, type ApprovalEntry } from '@/lib/admin/plugin-approvals';
/**
* GET /api/plugin-approval-status?pluginId=X&bundleHash=Y
*
* Any logged-in user may query the server-side approval state for a plugin
* they want to enable. The client uses this BEFORE running `enablePlugin`
* when the `requirePluginApproval` policy is set.
*
* POST same path with body `{ pluginId, bundleHash, manifest }` creates a
* pending approval entry (or returns the existing one).
*/
async function resolveUsername(): Promise<string | null> {
const cookieStore = await cookies();
for (let slot = 0; slot < MAX_ACCOUNT_SLOTS; slot++) {
const token = cookieStore.get(sessionCookieName(slot))?.value;
if (token) {
const sess = decryptSession(token);
if (sess?.username) return sess.username;
}
const ctx = readStalwartAuthContextFromStore(cookieStore, slot);
if (ctx?.username) return ctx.username;
}
return null;
}
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(request: NextRequest) {
try {
const username = await resolveUsername();
if (!username) return NextResponse.json({ error: 'unauthenticated' }, { status: 401 });
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 });
}
const status = await getApprovalStatus(pluginId, bundleHash);
return NextResponse.json(status, { headers: { 'Cache-Control': 'no-store' } });
} catch (err) {
logger.error('plugin-approval-status 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 username = await resolveUsername();
if (!username) return NextResponse.json({ error: 'unauthenticated' }, { status: 401 });
let body: unknown;
try { body = await request.json(); } catch { body = null; }
const b = (body ?? {}) as { pluginId?: unknown; bundleHash?: unknown; manifest?: unknown };
if (!isValidId(b.pluginId) || !isValidHash(b.bundleHash)) {
return NextResponse.json({ error: 'invalid pluginId or bundleHash' }, { status: 400 });
}
const m = (b.manifest ?? {}) as Record<string, unknown>;
const manifest: ApprovalEntry['manifest'] = {
name: typeof m.name === 'string' ? m.name.slice(0, 200) : undefined,
version: typeof m.version === 'string' ? m.version.slice(0, 64) : undefined,
author: typeof m.author === 'string' ? m.author.slice(0, 200) : undefined,
description: typeof m.description === 'string' ? m.description.slice(0, 500) : undefined,
permissions: Array.isArray(m.permissions) ? (m.permissions as unknown[]).filter((x): x is string => typeof x === 'string').slice(0, 50) : undefined,
httpOrigins: Array.isArray(m.httpOrigins) ? (m.httpOrigins as unknown[]).filter((x): x is string => typeof x === 'string').slice(0, 20) : undefined,
apiPostPaths: Array.isArray(m.apiPostPaths) ? (m.apiPostPaths as unknown[]).filter((x): x is string => typeof x === 'string').slice(0, 20) : undefined,
};
const entry = await requestApproval(b.pluginId as string, b.bundleHash as string, manifest, username);
return NextResponse.json({ status: entry.status, requestedAt: entry.requestedAt, decidedAt: entry.decidedAt });
} catch (err) {
logger.error('plugin-approval-status POST', { error: err instanceof Error ? err.message : String(err) });
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}