restructure project, add claudemd
This commit is contained in:
61
web/auth/ProtectedRoute.ts
Normal file
61
web/auth/ProtectedRoute.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
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>`;
|
||||
}
|
||||
Reference in New Issue
Block a user