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.
56 lines
2.5 KiB
SQL
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);
|