chahinebrini 2e056c7257 feat(devices): Apple-style two-device approval flow + email fallback
iCloud-Sign-In Pattern: wenn ein neues Gerät versucht sich anzumelden
und das Plan-Limit erreicht ist, kann der User auf einem bereits
angemeldeten Gerät bestätigen — Code wird auf BEIDEN Geräten gezeigt
für visuellen Vergleich (verhindert Code-Forwarding-Attacken).

Backend:
- New table device_approval_requests + supabase_realtime + RLS
- POST   /api/devices/approvals               — create (new device)
- GET    /api/devices/approvals               — list pending (existing devices)
- GET    /api/devices/approvals/:id           — status poll (new device)
- POST   /api/devices/approvals/:id/approve   — approve + atomic evict
- POST   /api/devices/approvals/:id/reject    — reject
- POST   /api/devices/approvals/:id/email     — trigger email fallback
- POST   /api/devices/approvals/email/:token  — magic-link approve (no auth)
- Email-Template via Resend (lyra-neutral, security-formal)
- 10min TTL, 6-digit numeric codes (crypto-random)

Frontend (rebreak-native):
- DeviceApprovalIncomingSheet — existing devices: code + device-picker + Allow/Reject
- DeviceApprovalPendingSheet  — new device: code + spinner + 'Send via email'
- useDeviceApprovalRealtime   — postgres_changes subscription
- DeviceLimitReachedSheet     — neues CTA 'Auf anderem Gerät bestätigen'
- i18n DE/EN/FR/AR

Migration läuft automatisch via prisma migrate deploy bei push.
2026-06-01 02:36:28 +02:00

47 lines
1.5 KiB
TypeScript

import { createApprovalRequest } from "../../../db/device-approvals";
/**
* POST /api/devices/approvals
*
* Aufgerufen vom NEUEN Gerät nachdem `register` mit DEVICE_LIMIT_REACHED
* fehlgeschlagen ist und der User "Auf anderem Gerät bestätigen" gewählt hat.
*
* Body: { deviceId, platform, model?, name?, osVersion? }
*
* Returnt das Approval-Record (mit code für UI-Anzeige + expiresAt).
* Existierende Geräte des Users werden via supabase_realtime (postgres_changes
* INSERT on device_approval_requests filtered by user_id) automatisch
* benachrichtigt — kein extra Push nötig.
*
* Bootstrap: skipDeviceCheck=true weil das Device noch nicht registriert ist.
*/
export default defineEventHandler(async (event) => {
const user = await requireUser(event, { skipDeviceCheck: true });
const body = await readBody(event);
const { deviceId, platform, model, name, osVersion } = body as {
deviceId?: string;
platform?: string;
model?: string;
name?: string;
osVersion?: string;
};
if (!deviceId || !platform) {
throw createError({ statusCode: 400, message: "deviceId + platform required" });
}
if (!["ios", "android", "web"].includes(platform)) {
throw createError({ statusCode: 400, message: "invalid platform" });
}
const approval = await createApprovalRequest({
userId: user.id,
newDeviceId: deviceId,
newPlatform: platform,
newModel: model ?? null,
newName: name ?? null,
newOsVersion: osVersion ?? null,
});
return { approval };
});