import { jwtDecode } from 'jwt-decode' import { getApiUrlPrefix } from './apiEndpointHelpers' /** JWT claims extracted from the access token */ export type UserProfile = Record & { /** Subject identifier */ sub?: string /** Expiration time (seconds since epoch) */ exp?: number /** Issued at time (seconds since epoch) */ iat?: number /** Email address */ email?: string /** User's name */ name?: string } export type TokenExchangeResponse = { success: boolean /** User profile with all JWT claims from the access token */ userProfile?: UserProfile /** The id token used for the exchange (for storing in localStorage) */ idToken?: string /** Token expiration time in milliseconds since epoch */ expiresAt?: number error?: | 'invalid_token' | 'unauthorized' | 'exchange_failed' | 'exchange_error' status?: number } export type TokenExchangeOptions = { /** Optional API headers to include in the request */ headers?: Record /** Optional API base URL. Defaults to adminczar API prefix */ apiBaseUrl?: string } function isJwtToken(token: string): boolean { try { jwtDecode(token) return true } catch { return false } } function decodeJwtPayload(jwt: string): UserProfile | undefined { try { return jwtDecode(jwt) } catch { return undefined } } /** * Exchanges an Auth0 id token with the adminczar global token exchange endpoint. * This helper function allows all frontend applications to reuse the same global token exchange logic * without redirecting users to the Admin frontend application. * * @param idToken - The Auth0 id token to exchange * @param options - Optional configuration for the exchange request * @param options.headers - Optional additional headers to include in the request * @param options.apiBaseUrl - Optional API base URL (defaults to adminczar API prefix) * @returns Promise with success boolean, user profile with JWT claims, and error information if failed * * @example * ```typescript * // Basic usage * const result = await exchangeGlobalToken(idToken) * if (result.success) { * // Global token exchange successful * console.log(result.userProfile) // All JWT claims from access token * console.log(result.idToken) // The id token for storing in localStorage * console.log(result.expiresAt) // Expiration time in milliseconds * } else { * // Handle error: result.error can be 'invalid_token', 'unauthorized', 'exchange_failed', or 'exchange_error' * } * * // With custom headers * const result = await exchangeGlobalToken(idToken, { * headers: { 'X-Custom-Header': 'value' } * }) * * // With custom API base URL * const result = await exchangeGlobalToken(idToken, { * apiBaseUrl: 'https://custom-api.example.com' * }) * ``` */ export async function exchangeGlobalToken( idToken: string, options?: TokenExchangeOptions, ): Promise { if (!isJwtToken(idToken)) { return { success: false, error: 'invalid_token', status: 400, } } const apiBaseUrl = options?.apiBaseUrl ?? getApiUrlPrefix('adminczar') try { const resp = await fetch(`${apiBaseUrl}/api/v1/exchange/token`, { method: 'POST', headers: { Authorization: `Bearer ${idToken}`, 'Content-Type': 'application/json', ...options?.headers, }, }) if (!resp.ok) { return { success: false, error: resp.status === 401 ? 'unauthorized' : 'exchange_failed', status: resp.status, } } // Parse the response to get the access_token const data = await resp.json() const accessToken = data?.access_token as string | undefined // Validate that access_token is present in the response if (!accessToken) { return { success: false, error: 'exchange_failed', status: resp.status, } } // Decode the access token to extract user profile claims const userProfile = decodeJwtPayload(accessToken) // Validate that access_token could be decoded if (!userProfile) { return { success: false, error: 'exchange_failed', status: resp.status, } } const expiresAt = userProfile.exp ? userProfile.exp * 1000 : undefined return { success: true, userProfile, idToken: accessToken, expiresAt, } } catch { return { success: false, error: 'exchange_error', } } }