62 lines
3.0 KiB
TypeScript
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.js";
|
|
import { Loader } from "../ui/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>`;
|
|
}
|