import { NextResponse } from 'next/server'; import { publicBaseUrl } from '@/lib/config'; import { generateDeviceToken, generateFriendlyId, hashToken, normaliseMac } from '@/lib/device/auth'; import { clientIp, deviceHeader } from '@/lib/device/headers'; import { checkTransport } from '@/lib/device/transport'; import { prisma } from '@/lib/db'; import { rateLimit } from '@/lib/ratelimit'; import { buildCurrentScreen, renderAndStore } from '@/lib/screen/service'; export const dynamic = 'force-dynamic'; /** * First contact. The firmware sends its MAC in the `ID` header and expects a * token back, which it then stores and presents on every later call. * * A device that is already registered is answered with an empty `api_key`: we * only ever stored the digest, so the original cannot be handed out again. If * a panel ever loses its token, an administrator re-pairs it from the settings * page — which is the correct outcome, not a gap. */ export async function GET(request: Request) { const transport = checkTransport(request); if (!transport.ok) { return transport.response; } const limit = rateLimit(`setup:${clientIp(request)}`, 10, 60_000); if (!limit.allowed) { return NextResponse.json( { status: 429, message: 'Trop de tentatives' }, { status: 429, headers: { 'Retry-After': String(limit.retryAfter) } }, ); } const mac = normaliseMac(deviceHeader(request, 'id')); if (!mac) { return NextResponse.json( { status: 404, message: 'Adresse MAC absente ou invalide' }, { status: 200 }, ); } const baseUrl = publicBaseUrl(request); const existing = await prisma.device.findUnique({ where: { macAddress: mac } }); if (existing) { return NextResponse.json({ status: 200, api_key: '', friendly_id: existing.friendlyId, image_url: await welcomeImageUrl(baseUrl), message: 'Appareil déjà appairé', }); } const token = generateDeviceToken(); const device = await prisma.device.create({ data: { macAddress: mac, friendlyId: await uniqueFriendlyId(), apiKeyHash: hashToken(token), }, }); return NextResponse.json({ status: 200, api_key: token, friendly_id: device.friendlyId, image_url: await welcomeImageUrl(baseUrl), message: 'Bienvenue', }); } /** * Never let a rendering failure cost us the pairing. * * The token is issued once and only its digest is kept, so if the response * that carries it fails the device is stranded: registered, but holding no * credential, and unable to register again. The welcome image is worth far * less than that, and the next /api/display call will produce one anyway. */ async function welcomeImageUrl(baseUrl: string): Promise { try { const { payload, settings } = await buildCurrentScreen(new Date(), baseUrl); const image = await renderAndStore(payload, settings.imageFormat); return `${baseUrl}/api/device/image/${image.filename}`; } catch (error) { console.error('Could not render the welcome image', error); return ''; } } async function uniqueFriendlyId(): Promise { for (let attempt = 0; attempt < 10; attempt += 1) { const candidate = generateFriendlyId(); const taken = await prisma.device.findUnique({ where: { friendlyId: candidate } }); if (!taken) { return candidate; } } throw new Error('Could not allocate a friendly id'); }