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.
47 lines
1.5 KiB
TypeScript
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 };
|
|
});
|