import { type NextRequest } from 'next/server'; import { randomBytes } from 'node:crypto'; import { createSupabaseServerClient } from '@/lib/supabase/server'; import { getSupabaseAdmin } from '@/lib/supabase/admin'; import { jsonNoStore } from '@/lib/admin/response'; import { validateSubdomain } from '@/lib/validation'; import { setTunnelActive } from '@/lib/redis'; import { normalizeUserCode } from '@/lib/device'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; type ApproveBody = { user_code?: unknown; subdomain?: unknown }; type Pairing = { id: number; status: string; expires_at: string; }; /** * Approve a device pairing (AUTHENTICATED + AAL2). The signed-in user binds a * pending user_code to their account and chooses/reuses a subdomain. */ export async function POST(req: NextRequest) { const supabase = createSupabaseServerClient(); const { data: { user }, } = await supabase.auth.getUser(); if (!user) return jsonNoStore({ error: 'unauthorized' }, { status: 401 }); const { data: aal } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel(); if (aal?.currentLevel !== 'aal2') { return jsonNoStore({ error: 'aal2_required' }, { status: 403 }); } let body: ApproveBody; try { body = (await req.json()) as ApproveBody; } catch { return jsonNoStore({ error: 'invalid json' }, { status: 400 }); } const userCode = normalizeUserCode( typeof body.user_code === 'string' ? body.user_code : '', ); if (!userCode) { return jsonNoStore({ error: 'invalid_user_code' }, { status: 400 }); } const admin = getSupabaseAdmin(); const { data: pairing, error: pairingErr } = await admin .from('device_pairings') .select('id, status, expires_at') .eq('user_code', userCode) .eq('status', 'pending') .maybeSingle(); if (pairingErr) { return jsonNoStore({ error: 'failed' }, { status: 500 }); } if (!pairing) { return jsonNoStore({ error: 'invalid_user_code' }, { status: 400 }); } if (new Date(pairing.expires_at).getTime() < Date.now()) { await admin .from('device_pairings') .update({ status: 'expired' }) .eq('id', pairing.id); return jsonNoStore({ error: 'expired' }, { status: 400 }); } let subdomain: string; if (body.subdomain !== undefined) { const v = validateSubdomain(body.subdomain); if (!v.ok) { return jsonNoStore({ error: v.error }, { status: 400 }); } subdomain = v.value; // Reject if the subdomain is already owned by another user. const { data: existing, error: existingErr } = await admin .from('tunnels') .select('user_id') .eq('subdomain', subdomain) .maybeSingle<{ user_id: string }>(); if (existingErr) { return jsonNoStore({ error: 'failed' }, { status: 500 }); } if (existing && existing.user_id !== user.id) { return jsonNoStore({ error: 'subdomain taken' }, { status: 409 }); } const token = randomBytes(32).toString('hex'); const { error: upsertErr } = await admin .from('tunnels') .upsert( { user_id: user.id, subdomain, token, is_active: true }, { onConflict: 'user_id' }, ) .select('subdomain') .single(); if (upsertErr) { if ((upsertErr as { code?: string }).code === '23505') { return jsonNoStore({ error: 'subdomain taken' }, { status: 409 }); } return jsonNoStore({ error: 'failed' }, { status: 500 }); } await setTunnelActive(subdomain, true); } else { // Reuse the user's existing subdomain without rotating the frp token. const { data: tunnel, error: tunnelErr } = await admin .from('tunnels') .select('subdomain') .eq('user_id', user.id) .maybeSingle<{ subdomain: string }>(); if (tunnelErr) { return jsonNoStore({ error: 'failed' }, { status: 500 }); } if (!tunnel) { return jsonNoStore({ error: 'subdomain_required' }, { status: 400 }); } subdomain = tunnel.subdomain; } const { error: updateErr } = await admin .from('device_pairings') .update({ user_id: user.id, subdomain, status: 'approved', approved_at: new Date().toISOString(), }) .eq('id', pairing.id); if (updateErr) { return jsonNoStore({ error: 'failed' }, { status: 500 }); } return jsonNoStore({ ok: true, subdomain }); }