feat(device): OAuth2 device-pairing flow for HA add-on (code/approve/token/tunnel + /activate)
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user