98 lines
3.4 KiB
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] --> |