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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 10x 10x 10x 10x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | import { type GetTokenSilentlyOptions } from '@auth0/auth0-react';
import axios, { type AxiosError, type AxiosPromise, type AxiosRequestConfig } from 'axios';
import qs from 'qs';
import { generatePath } from 'react-router-dom';
import { routes } from '@amalia/core/routes';
import { assert } from '@amalia/ext/typescript';
import { config } from '@amalia/kernel/config/client';
import { log } from '@amalia/kernel/logger/client';
axios.defaults.baseURL = config.api.url;
axios.defaults.withCredentials = true;
// ================ AUTH ================
const allowlistedUnauthorizedErrors = ['/companies', '/users/me'] as const;
let tokenInterceptor: number | null = null;
let globalJwt: string | null = null;
/**
* Properly cleanup the token interceptor.
*/
const clearTokenInterceptor = () => {
if (tokenInterceptor) {
axios.interceptors.request.eject(tokenInterceptor);
tokenInterceptor = null;
log.info('Token interceptor cleared');
}
};
/**
* Set up an axios interceptor that fetch the access token before each call.
*
* It's not clearly written in the Auth0 documentation, but the function they
* provide in their SDK to get an access token should be called before each
* call to our API. It will either return the access token it has in cache
* or call auth0 to perform a refresh token, then use the new access token.
*
* @param getAccessTokenSilently
*/
const setTokenInterceptor = (getAccessTokenSilently: (options: GetTokenSilentlyOptions) => Promise<string>) => {
// Clear the existing interceptor if present.
clearTokenInterceptor();
tokenInterceptor = axios.interceptors.request.use(async (requestConfig) => {
let jwt = '';
try {
// Use the Auth0 callback to get an access token.
jwt = await getAccessTokenSilently({
authorizationParams: {
audience: config.auth0.audience,
scope: config.auth0.scope,
},
});
globalJwt = jwt;
} catch (e) {
// If the token is expired and could not be refreshed (for instance,
// the user logged out), log the error and return an empty jwt.
// The API will then return a 401 on the next call, and the
// user will be redirected to the login page.
log.error(e);
}
if (jwt) {
requestConfig.headers.Authorization = `Bearer ${jwt}`;
}
return requestConfig;
});
};
export const qsStringify: typeof qs.stringify = (params, options) =>
qs.stringify(params, { arrayFormat: 'repeat', indices: false, ...options });
export class HttpError<T = unknown> extends Error {
public statusCode: number;
public payload: T;
public constructor(message: string, statusCode: number, payload: T) {
super(message);
this.statusCode = statusCode;
this.payload = payload;
}
}
// FIXME: maybe validate shape of error payload with a yup or zod schema?
export const isHttpError = <TPayload = unknown>(error: unknown): error is HttpError<TPayload> =>
error instanceof HttpError;
axios.interceptors.response.use(
(response) => response,
(error: AxiosError<{ message: string } | undefined> | Error) => {
// We try to intercept unauthorized errors.
// If we find one, then the user token is not valid anymore, so refresh on /
// Allowlisted unauthorized URLs are managed by the authorization protector and must not refresh the page
assert('response' in error, error);
const { response, config } = error;
assert(response, error);
if (response.status === 401 && !allowlistedUnauthorizedErrors.includes(config?.url ?? '')) {
globalThis.location.href = generatePath(routes.ROOT);
}
const { status, data } = response;
// If we can find a human-readable message in the error, display it, or else throw the error as-is.
if (data?.message) {
const { message, ...payload } = data;
throw new HttpError(message, status, payload);
}
throw error;
},
);
function setJwt(jwt: string | null) {
globalJwt = jwt;
axios.defaults.headers.common.Authorization = jwt ? `Bearer ${jwt}` : '';
}
function getJwt() {
return globalJwt as string;
}
// ================ AXIOS OVERRIDES ================
// Axios DELETE normally doesn't accept a body, so we're overriding the method everywhere.
const axiosDelete = <T = unknown>(url: string, axiosConfig?: AxiosRequestConfig) =>
axios({
method: 'DELETE',
url,
...axiosConfig,
}) as AxiosPromise<T>;
export const http = {
get: axios.get,
post: axios.post,
put: axios.put,
patch: axios.patch,
delete: axiosDelete,
getJwt,
setJwt,
setTokenInterceptor,
clearTokenInterceptor,
};
|