rokojori-auth-connector/source/browser/apiFetch.ts

55 lines
1.9 KiB
TypeScript
Raw Permalink Normal View History

2026-07-17 07:38:05 +00:00
// Browser SPA auth helper.
//
// The server's jwtMiddleware handles transparent token refresh for cookie-based clients,
// so the browser rarely needs to act on a 401. When it does arrive it means both tokens
// are dead (refresh token expired or revoked) and the user must log in again.
//
// Usage:
// import { apiFetch, setAuthRedirectUrl } from 'rokojori-auth-connector/browser/apiFetch';
//
// // Optional — override the login URL if your app is on a different subdomain
// setAuthRedirectUrl('https://account.rokojori.com/login.html');
//
// const res = await apiFetch('/api/projects');
// const data = await res.json();
const AUTH_HOST = typeof window !== 'undefined'
? ( ( window as unknown as Record<string, unknown> ).AUTH_HOST as string | undefined )
: undefined;
let loginUrl = ( AUTH_HOST ?? 'https://account.rokojori.com' ) + '/login.html';
export function setAuthRedirectUrl( url: string ): void
{
loginUrl = url;
}
export async function apiFetch( input: RequestInfo | URL, init?: RequestInit ): Promise<Response>
{
const res = await fetch( input, init );
if ( res.status === 401 )
{
const redirect = encodeURIComponent( window.location.href );
window.location.href = `${ loginUrl }?redirect=${ redirect }`;
}
return res;
}
// Decode the JWT access token payload without verifying the signature.
// Use this to read userId, roles, and products from the token the server set.
// The token is in the accessToken cookie — readable only if it is NOT HttpOnly,
// which it is by default. In that case call GET /api/auth/me instead.
export function decodeTokenPayload( token: string ): Record<string, unknown> | null
{
try
{
const parts = token.split( '.' );
if ( parts.length !== 3 ) return null;
const json = atob( parts[1].replace( /-/g, '+' ).replace( /_/g, '/' ) );
return JSON.parse( json ) as Record<string, unknown>;
}
catch { return null; }
}