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

56 lines
2.5 KiB
SQL

-- Apple-Style Two-Device-Approval (iCloud Sign-In pattern)
-- Tracks pending approval requests when a new device tries to register
-- but the user's plan device limit is reached. Code is shown on BOTH the
-- new and an existing active device for visual comparison.
CREATE TABLE "rebreak"."device_approval_requests" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"user_id" UUID NOT NULL,
"new_device_id" TEXT NOT NULL,
"new_platform" TEXT NOT NULL,
"new_model" TEXT,
"new_name" TEXT,
"new_os_version" TEXT,
"code" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"approved_by_device_row_id" UUID,
"approved_at" TIMESTAMP(3),
"rejected_at" TIMESTAMP(3),
"evicted_device_row_id" UUID,
"email_sent_at" TIMESTAMP(3),
"email_token" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "device_approval_requests_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "device_approval_requests_email_token_key"
ON "rebreak"."device_approval_requests"("email_token");
CREATE INDEX "device_approval_requests_user_id_status_idx"
ON "rebreak"."device_approval_requests"("user_id", "status");
CREATE INDEX "device_approval_requests_user_id_created_at_idx"
ON "rebreak"."device_approval_requests"("user_id", "created_at" DESC);
-- FK with cascade — user delete (DSGVO Art. 17) removes all approval requests
ALTER TABLE "rebreak"."device_approval_requests"
ADD CONSTRAINT "device_approval_requests_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
-- Enable Supabase Realtime so existing devices get instant push when a new
-- approval request appears (postgres_changes subscription on
-- user_id=eq.<currentUser>).
ALTER TABLE "rebreak"."device_approval_requests" REPLICA IDENTITY FULL;
ALTER PUBLICATION supabase_realtime ADD TABLE "rebreak"."device_approval_requests";
-- RLS: user can only see their own approval requests
ALTER TABLE "rebreak"."device_approval_requests" ENABLE ROW LEVEL SECURITY;
CREATE POLICY "device_approval_requests_select_own"
ON "rebreak"."device_approval_requests"
FOR SELECT
USING (auth.uid() = user_id);