import { type NextRequest } from 'next/server'; import { getSupabaseAdmin } from '@/lib/supabase/admin'; import { jsonNoStore } from '@/lib/admin/response'; import { getAppOrigin } from '@/lib/auth/mfa'; import { generateDeviceCode, generateUserCode, hashToken, DEVICE_CODE_TTL_SECONDS, DEVICE_POLL_INTERVAL_SECONDS, } from '@/lib/device'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; /** * Device-authorization endpoint (PUBLIC, no auth). The HA add-on calls this to * obtain a device_code + a short user_code. The user then visits * verification_uri and approves the pairing while signed in. */ export async function POST(_req: NextRequest) { const admin = getSupabaseAdmin(); const deviceCode = generateDeviceCode(); const deviceCodeHash = hashToken(deviceCode); const expiresAt = new Date( Date.now() + DEVICE_CODE_TTL_SECONDS * 1000, ).toISOString(); // Retry on the (rare) user_code unique-violation with a fresh code. let userCode = ''; let lastError: string | null = null; for (let attempt = 0; attempt < 5; attempt++) { userCode = generateUserCode(); const { error } = await admin.from('device_pairings').insert({ device_code_hash: deviceCodeHash, user_code: userCode, status: 'pending', interval_seconds: DEVICE_POLL_INTERVAL_SECONDS, expires_at: expiresAt, }); if (!error) { lastError = null; break; } lastError = error.message; if ((error as { code?: string }).code !== '23505') { return jsonNoStore({ error: 'failed' }, { status: 500 }); } } if (lastError) { return jsonNoStore({ error: 'failed' }, { status: 500 }); } const appOrigin = getAppOrigin(); return jsonNoStore({ device_code: deviceCode, user_code: userCode, verification_uri: `${appOrigin}/activate`, verification_uri_complete: `${appOrigin}/activate?code=${userCode}`, interval: DEVICE_POLL_INTERVAL_SECONDS, expires_in: DEVICE_CODE_TTL_SECONDS, }); }