Devices/Magic: - Offline-Profil-Enroll deaktiviert (410) — Lock-PW würde im Klartext im Download landen; stationärer Schutz läuft jetzt nur über Rebreak Magic - Mac-DNS-Template: ProhibitDisablement (Filter nicht abschaltbar) - Push "Neues Gerät verbunden" an mobile Geräte bei neuer Bindung - Realtime auf user_devices → Settings aktualisiert Magic-Bindings live - Geräte-Detail-Sheet (Tap auf Gerät): Status, verbunden-seit, Schutz-Donut Hard-Lock (server-gehaltenes Removal-PW, User sieht es nie): - magic_removal_password generiert/gespeichert + in Profil injiziert (Lazy-Backfill) - Reveal NUR bei Account-Löschung (user/delete) + Kündigung (stripe webhook), per Resend-Mail + in-Response - Signing config-gated (inaktiv ohne Cert; Lock greift auch unsigniert) Migrations: user_devices-Realtime-Publication + magic_removal_password-Spalten Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
2.2 KiB
TypeScript
72 lines
2.2 KiB
TypeScript
import { useEffect } from "react";
|
|
import { supabase } from "../lib/supabase";
|
|
import type { RealtimeChannel } from "@supabase/supabase-js";
|
|
import { useDevicesStore } from "../stores/devices";
|
|
|
|
/**
|
|
* Realtime-Subscription auf `rebreak.user_devices` für den eingeloggten User.
|
|
*
|
|
* Trigger-Fall: Die Magic-Desktop-App (Mac/Windows) ruft serverseitig
|
|
* /api/magic/register → neues UserDevice-INSERT. Diese Subscription lässt die
|
|
* Geräte-Liste in den Settings sofort nachladen, ohne dass der User pullt.
|
|
*
|
|
* Spiegelt [[useProtectedDevicesRealtime]]: publication-only (keine RLS), wir
|
|
* reagieren auf jedes Event und reloaden den Store — keine Abhängigkeit von
|
|
* payload.old (REPLICA IDENTITY irrelevant).
|
|
*/
|
|
export function useUserDevicesRealtime(enabled: boolean = true) {
|
|
useEffect(() => {
|
|
if (!enabled) return;
|
|
let channel: RealtimeChannel | null = null;
|
|
let cancelled = false;
|
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
async function subscribe() {
|
|
const { data } = await supabase.auth.getSession();
|
|
const session = data.session;
|
|
if (!session?.access_token || cancelled) return;
|
|
|
|
const userId = session.user.id;
|
|
|
|
channel = supabase
|
|
.channel(`user-devices:${userId}:${Date.now()}`)
|
|
.on(
|
|
"postgres_changes",
|
|
{
|
|
event: "*",
|
|
schema: "rebreak",
|
|
table: "user_devices",
|
|
filter: `user_id=eq.${userId}`,
|
|
},
|
|
() => {
|
|
useDevicesStore.getState().loadDevices();
|
|
},
|
|
)
|
|
.subscribe((status: string) => {
|
|
if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
|
|
cleanup();
|
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
reconnectTimer = setTimeout(() => {
|
|
if (!cancelled) subscribe();
|
|
}, 3000);
|
|
}
|
|
});
|
|
}
|
|
|
|
function cleanup() {
|
|
if (channel) {
|
|
supabase.removeChannel(channel);
|
|
channel = null;
|
|
}
|
|
}
|
|
|
|
subscribe();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
cleanup();
|
|
};
|
|
}, [enabled]);
|
|
}
|