20216b37c6
Replaces the per-device DeviceRecord lookup with a per-user discriminated union (loading | needs-setup | needs-unlock | unlocked). Heartbeat block deleted (telemetry no longer device-bound); webPush keyed by install-id.
44 lines
1.5 KiB
TypeScript
44 lines
1.5 KiB
TypeScript
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
|
|
|
import { useAuth } from '../context/AuthContext';
|
|
import { SpinnerIcon } from './icons';
|
|
|
|
function FullScreenSpinner({ label }: { label?: string }) {
|
|
return (
|
|
<main className="flex min-h-screen items-center justify-center bg-ink-950 p-6">
|
|
<div className="flex items-center gap-3 text-neutral-400">
|
|
<SpinnerIcon className="h-5 w-5 text-brand-400" />
|
|
{label && <span className="text-sm font-medium">{label}</span>}
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
// Forces a session. Sends to /auth otherwise.
|
|
export function RequireAuth() {
|
|
const { session, ready } = useAuth();
|
|
const loc = useLocation();
|
|
if (!ready) return <FullScreenSpinner />;
|
|
if (!session) return <Navigate to="/auth" replace state={{ from: loc.pathname }} />;
|
|
return <Outlet />;
|
|
}
|
|
|
|
// Forces a usable per-user encrypted key blob on this install. Sends to
|
|
// /device (the setup/unlock page) when the blob is missing or locked.
|
|
export function RequireDevice() {
|
|
const { userKeyState } = useAuth();
|
|
if (userKeyState.status === 'loading') return <FullScreenSpinner />;
|
|
if (userKeyState.status === 'needs-setup' || userKeyState.status === 'needs-unlock') {
|
|
return <Navigate to="/device" replace />;
|
|
}
|
|
return <Outlet />;
|
|
}
|
|
|
|
// Admin-only route gate. Non-admins bounce to /chats — RLS still enforces
|
|
// server-side, this is purely a UX shortcut.
|
|
export function RequireAdmin() {
|
|
const { profile } = useAuth();
|
|
if (profile && !profile.isAdmin) return <Navigate to="/chats" replace />;
|
|
return <Outlet />;
|
|
}
|