import { NextResponse } from 'next/server' import { execFileSync } from 'child_process' import { readFileSync } from 'fs' import { join } from 'path' import { requireRole } from '@/lib/auth' import { getDatabase } from '@/lib/db' import { APP_VERSION } from '@/lib/version' const UPDATE_TIMEOUT = 5 * 60 * 1000 // 5 minutes const MAX_BUFFER = 10 * 1024 * 1024 // 10 MB const EXEC_OPTS = { timeout: UPDATE_TIMEOUT, maxBuffer: MAX_BUFFER, encoding: 'utf-8' as const, } function git(args: string[], cwd: string): string { return execFileSync('git', args, { ...EXEC_OPTS, cwd }).trim() } function pnpm(args: string[], cwd: string): string { return execFileSync('pnpm', args, { ...EXEC_OPTS, cwd }).trim() } export async function POST(request: Request) { const auth = requireRole(request, 'admin') if (auth.error) { return NextResponse.json({ error: auth.error }, { status: auth.status }) } const user = auth.user! const cwd = process.cwd() const steps: { step: string; output: string }[] = [] try { // Parse target version from request body const body = await request.json().catch(() => ({})) const targetVersion: string | undefined = body.targetVersion if (!targetVersion) { return NextResponse.json( { error: 'Missing targetVersion in request body' }, { status: 400 } ) } // Normalize to tag format (e.g. "1.2.0" -> "v1.2.0") const tag = targetVersion.startsWith('v') ? targetVersion : `v${targetVersion}` // 1. Check for uncommitted changes const status = git(['status', '--porcelain'], cwd) if (status) { return NextResponse.json( { error: 'Working tree has uncommitted changes. Please commit or stash them before updating.', dirty: true, files: status.split('\n').slice(0, 20), }, { status: 409 } ) } // 2. Fetch tags and release code from origin const fetchOut = git(['fetch', 'origin', '--tags', '--force'], cwd) steps.push({ step: 'git fetch', output: fetchOut || 'OK' }) // 3. Verify the tag exists try { git(['rev-parse', '--verify', `refs/tags/${tag}`], cwd) } catch { return NextResponse.json( { error: `Release tag ${tag} not found in remote` }, { status: 404 } ) } // 4. Checkout the release tag const checkoutOut = git(['checkout', tag], cwd) steps.push({ step: `git checkout ${tag}`, output: checkoutOut }) // 5. Install dependencies const installOut = pnpm(['install', '--frozen-lockfile'], cwd) steps.push({ step: 'pnpm install', output: installOut }) // 6. Build const buildOut = pnpm(['build'], cwd) steps.push({ step: 'pnpm build', output: buildOut }) // 7. Read new version from package.json const newPkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8')) const newVersion: string = newPkg.version ?? targetVersion // 8. Log to audit_log try { const db = getDatabase() db.prepare( 'INSERT INTO audit_log (action, actor, detail) VALUES (?, ?, ?)' ).run( 'system.update', user.username, JSON.stringify({ previousVersion: APP_VERSION, newVersion, tag, }) ) } catch { // Non-critical -- don't fail the update if audit logging fails } return NextResponse.json({ success: true, previousVersion: APP_VERSION, newVersion, tag, steps, restartRequired: true, }) } catch (err: any) { const message = err?.stderr?.toString?.()?.trim() || err?.stdout?.toString?.()?.trim() || err?.message || 'Unknown error during update' return NextResponse.json( { error: 'Update failed', detail: message, steps, }, { status: 500 } ) } }