64 lines
2.0 KiB
TypeScript
64 lines
2.0 KiB
TypeScript
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,
|
|
});
|
|
}
|