All files / src/authentication session.js

100% Statements 30/30
100% Branches 10/10
100% Functions 7/7
100% Lines 26/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84                                  12x 82x 82x   1x       12x 22x 22x           12x 54x 54x                       12x 12x 49x   13x           12x 11x 11x 6x 6x 6x   5x                   12x 11x 11x       6x 6x    
/**
 * BFF Session Module — ADR-001 Phase 2 (httpOnly-cookie auth).
 *
 * After Phase 2 the access token lives in an HttpOnly cookie the SPA cannot read,
 * so identity comes from the server. `fetchSession()` probes `GET /v1/auth/me`
 * (the cookie rides along via `withCredentials`); a 200 means authenticated.
 *
 * The user's ROLE is not a secret — every backend action is authorized
 * server-side independently — so we cache the server-verified role as a
 * non-sensitive UI hint in `localStorage.role` for synchronous route gating.
 * The access token itself is NEVER stored client-side anymore (closes H1).
 *
 * Developer: Allan Ninal
 */
import axios from "axios";
import { API_ENDPOINTS } from "@/api/endpoints";
 
export const getStoredRole = () => {
    try {
        return localStorage.getItem("role");
    } catch {
        return null;
    }
};
 
export const setStoredRole = (role) => {
    try {
        if (role) localStorage.setItem("role", role);
    } catch {
        /* ignore */
    }
};
 
export const clearStoredRole = () => {
    try {
        localStorage.removeItem("role");
    } catch {
        /* ignore */
    }
};
 
// CSRF token store (in-memory). The csrf_token cookie is host-only on the API
// host, which the SPA cannot read via document.cookie when the SPA and API run
// on different hosts (frontend www / api apex; the staff app is fully
// cross-domain). So the backend returns the token in the login / refresh / me
// response BODIES and we keep it here; the axios interceptor echoes it as the
// X-CSRF-Token header. The backend still double-submits it against the cookie.
let _csrfToken = null;
export const setCsrfToken = (token) => {
    _csrfToken = token || null;
};
export const getCsrfToken = () => _csrfToken;
 
/**
 * Server-backed session probe. Resolves to { isAuthenticated, role, user }.
 * Never throws — a failed/401 probe resolves to an unauthenticated session.
 */
export const fetchSession = async () => {
    try {
        const { data } = await axios.get(API_ENDPOINTS.auth.me, { withCredentials: true });
        if (data?.role) setStoredRole(data.role);
        if (data?.csrf_token) setCsrfToken(data.csrf_token);
        return { isAuthenticated: true, role: data?.role ?? null, user: data };
    } catch {
        return { isAuthenticated: false, role: null, user: null };
    }
};
 
/**
 * BFF logout — ADR-001 Phase 4. Calls POST /v1/auth/logout so the backend
 * revokes the refresh token at Auth0 and clears all session cookies (fixes C3).
 * Best-effort: a revoke/network failure never blocks the local sign-out, which
 * the caller still performs (Auth0 SDK local logout + Redux + navigation).
 */
export const logoutSession = async () => {
    try {
        await axios.post(API_ENDPOINTS.auth.logout, {}, { withCredentials: true });
    } catch {
        /* best-effort — proceed with local logout regardless */
    }
    clearStoredRole();
    setCsrfToken(null);
};