Files
ss-tools/frontend/src/components/auth/ProtectedRoute.svelte

98 lines
3.4 KiB
Svelte

<!-- [DEF:ProtectedRoute:Component] -->
<!--
@TIER: STANDARD
@SEMANTICS: auth, guard, route, protection, permission
@PURPOSE: Wraps content to ensure only authenticated and authorized users can access it.
@LAYER: Component
@RELATION: USES -> authStore
@RELATION: CALLS -> goto
@RELATION: DEPENDS_ON -> frontend.src.lib.auth.permissions.hasPermission
@INVARIANT: Redirects to /login if user is not authenticated and to fallback route when permission is denied.
@UX_STATE: Loading -> Shows spinner while session/permission check is in progress.
@UX_STATE: Authorized -> Renders protected slot content.
@UX_RECOVERY: Invalid token triggers logout and redirect to /login.
-->
<script lang="ts">
import { onMount } from "svelte";
import { auth } from "../../lib/auth/store";
import { api } from "../../lib/api";
import { goto } from "$app/navigation";
import { hasPermission } from "$lib/auth/permissions.js";
export let requiredPermission: string | null = null;
export let fallbackPath: string = "/profile";
let hasRouteAccess = false;
let isCheckingAccess = true;
// [DEF:verifySessionAndAccess:Function]
/**
* @purpose Validates active session and optional route permission before rendering protected slot.
* @pre Auth store is initialized.
* @post hasRouteAccess is true only when session and permission checks pass.
* @side_effect May update auth store, perform redirect, and fetch current user.
*/
async function verifySessionAndAccess(): Promise<void> {
isCheckingAccess = true;
try {
if (!$auth.token) {
auth.setLoading(false);
await goto("/login");
return;
}
let currentUser = $auth.user;
if (!currentUser) {
auth.setLoading(true);
try {
const user = await api.fetchApi("/auth/me");
auth.setUser(user);
currentUser = user;
} catch (error) {
console.error("[ProtectedRoute][COHERENCE:FAILED] Failed to verify session:", error);
auth.logout();
await goto("/login");
return;
} finally {
auth.setLoading(false);
}
}
if (!currentUser) {
auth.logout();
await goto("/login");
return;
}
if (requiredPermission && !hasPermission(currentUser, requiredPermission, "READ")) {
console.warn(
`[ProtectedRoute][REFLECT] Permission denied for ${requiredPermission}, redirecting to ${fallbackPath}`,
);
hasRouteAccess = false;
await goto(fallbackPath);
return;
}
hasRouteAccess = true;
} finally {
isCheckingAccess = false;
}
}
// [/DEF:verifySessionAndAccess:Function]
onMount(() => {
void verifySessionAndAccess();
});
</script>
{#if $auth.loading || isCheckingAccess}
<div class="flex items-center justify-center min-h-screen">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
{:else if $auth.isAuthenticated && hasRouteAccess}
<slot />
{/if}
<!-- [/DEF:ProtectedRoute:Component] -->