Files
kjol/web/auth/ProtectedRoute.ts
2026-07-13 09:17:11 -04:00

62 lines
3.0 KiB
TypeScript

import { createEffect, createSignal, createMemo, JSXElement } from "solid-js";
import html from "solid-js/html";
import { useAuth } from "./AuthContext.ts";
import { getToken } from "./useAuthFetch.js";
import { hasPermission } from "./permissions.ts";
import { Loader } from "../uikit/General.tsx";
interface ProtectedRouteProps {
permissions?: string[];
children?: JSXElement;
}
export function ProtectedRoute(props: ProtectedRouteProps) {
const auth = useAuth();
const [isRedirecting, setIsRedirecting] = createSignal(false);
createEffect(() => {
// Redirect to login ONLY when there is genuinely no session. A real
// 401 clears the token (so getToken() is null here), but a transient
// check failure preserves it -- in that case we keep the user here and
// show a retry screen instead of logging them out. -mta 6/3/26
if (!auth.loading() && !auth.isAuthenticated() && !getToken() && !isRedirecting()) {
setIsRedirecting(true);
const returnUrl = encodeURIComponent(window.location.pathname + window.location.search);
window.location.replace(`/login?redirect=${returnUrl}`);
}
});
const hasRequiredPermissions = createMemo(() => {
const permissions = props.permissions || [];
if (permissions.length === 0) return true;
const userPermissions = auth.identity()?.permissions || [];
return permissions.every((perm) => hasPermission(userPermissions, perm));
});
const state = createMemo(() => {
if (isRedirecting() || auth.loading()) return 'loading';
if (!auth.isAuthenticated()) {
// Token still present => the check failed transiently => offer a
// retry. No token => the redirect effect is taking over.
return getToken() ? 'error' : 'loading';
}
if (!hasRequiredPermissions()) return 'unauthorized';
return 'ready';
});
return html`<div>
${() => state() === 'loading' && html`<${Loader} />`}
${() => state() === 'error' && html`<div class="flex flex-col items-center justify-center min-h-[50vh] text-center p-8">
<h1 class="text-2xl font-semibold text-neutral-800 mb-2">Connection problem</h1>
<p class="text-neutral-600 mb-4">We couldn't verify your session. You're still signed in -- please try again.</p>
<button onclick=${() => auth.checkAuth()} class="text-sky-600 hover:text-sky-800 hover:underline">Retry</button>
</div>`}
${() => state() === 'unauthorized' && html`<div class="flex flex-col items-center justify-center min-h-[50vh] text-center p-8">
<h1 class="text-2xl font-semibold text-neutral-800 mb-2">Access Denied</h1>
<p class="text-neutral-600 mb-4">You don't have permission to access this page.</p>
<a href="/app/dashboard" class="text-sky-600 hover:text-sky-800 hover:underline">Return to Dashboard</a>
</div>`}
${() => state() === 'ready' && props.children}
</div>`;
}