import axios, {isAxiosError} from 'axios'; import { useCallback, useEffect, useState } from 'react'; import { useApp } from './App'; export function useApi(tokenEndpoint: string, client_id: string, client_secret: string, allowAnonymous: boolean = false) { const { id, endpoint } = useApp(); const [ api ] = useState(() => apiProvider(id, tokenEndpoint, endpoint, client_id, client_secret, allowAnonymous)); const [ isLoggedIn, setIsLoggedIn ] = useState(() => api.is_logged_in()); const onUpdateCallback = useCallback(function() { setIsLoggedIn(api.is_logged_in()); }, [api]); useEffect(() => { api.onLoggedIn(onUpdateCallback); return function() { api.onLoggedIn(null); }; }, [api]); return [ api, isLoggedIn ]; } function apiProvider(id: string, tokenEndpoint: string, endpoint: string, client_id: string, client_secret: string, allowAnonymous: boolean = false) { let access_token = typeof window !== 'undefined' && window.localStorage.getItem(id + '_access_token'), refresh_token = typeof window !== 'undefined' && window.localStorage.getItem(id + '_refresh_token'); const free = axios.create({ baseURL: endpoint }); free.interceptors.response.use(a => a, a => !isAxiosError(a) || a.response?.status === 402 ? Promise.reject(a) : Promise.reject(a.response && a.response.data && (new Error(a.response.data.message || a.response.data.code || 'Unknown error')))); function is_logged_in() { return !!refresh_token; } let _onLoggedIn: Function|null; function onLoggedIn(callback: Function|null) { _onLoggedIn = callback; } function loggedIn() { _onLoggedIn && _onLoggedIn(true); } function logout() { access_token = null; refresh_token = null; localStorage.removeItem(id + '_access_token'); localStorage.removeItem(id + '_refresh_token'); _onLoggedIn && _onLoggedIn(false); } async function log_in(username: string, password: string) { const form = new FormData(); form.append('grant_type', 'password'); form.append('client_id', client_id); form.append('client_secret', client_secret); form.append('username', username); form.append('password', password); const data = await free.post(tokenEndpoint, form); access_token = data.data.access_token; refresh_token = data.data.refresh_token; localStorage.setItem(id + '_access_token', access_token || ''); localStorage.setItem(id + '_refresh_token', refresh_token || ''); loggedIn(); } async function log_in_by_code(code:string, redirect_uri:string) { const form = new FormData(); form.append('grant_type', 'authorization_code'); form.append('client_id', client_id); form.append('client_secret', client_secret); form.append('code', code); form.append('redirect_uri', redirect_uri); const data = await free.post(tokenEndpoint, form); access_token = data.data.access_token; refresh_token = data.data.refresh_token; localStorage.setItem(id + '_access_token', access_token || ''); localStorage.setItem(id + '_refresh_token', refresh_token || ''); loggedIn(); } let cached_token; function renew_token() { if(!cached_token) { cached_token = _renew_token() .finally(function() { cached_token = null; }); } return cached_token; async function _renew_token() { const form = new FormData(); form.append('grant_type', 'refresh_token'); form.append('client_id', client_id); form.append('client_secret', client_secret); form.append('refresh_token', refresh_token || ''); try { const data = await free.post(tokenEndpoint, form); access_token = data.data.access_token; localStorage.setItem(id + '_access_token', access_token || ''); } catch(e) { if ((e as any)?.message === "invalid_grant") { logout(); } throw e; } } } const tokenized = axios.create({ baseURL: endpoint }); function set_hook(start: Function, stop: Function): Function { let count = 0; function check() { if(count === 1) { start(); } else if(count === 0) { stop(); } } const request = tokenized.interceptors.request.use((a) => { ++count; check(); return a; }); const response = tokenized.interceptors.response.use((a) => { --count; check(); return a; }, (a) => { --count; check(); return Promise.reject(a); }); const request2 = free.interceptors.request.use((a) => { ++count; check(); return a; }); const response2 = free.interceptors.response.use((a) => { --count; check(); return a; }, (a) => { --count; check(); return Promise.reject(a); }); return function() { tokenized.interceptors.response.eject(response); tokenized.interceptors.request.eject(request); free.interceptors.response.eject(response2); free.interceptors.request.eject(request2); }; } tokenized.interceptors.request.use(function(config) { if(access_token == null) { if (allowAnonymous) return config; return Promise.reject(new Error('You must be logged in to use this action.')); } (config as any).headers = { ...(config.headers || {}), 'Authorization': 'Bearer ' + access_token }; return config; }); tokenized.interceptors.response.use(undefined, async function(error) { if(error.response && error.response.status === 401) { await renew_token(); try { return await tokenized(error.config); } catch(e) { console.error(e); } } throw error; }); return { client_id, free, tokenized, log_in, is_logged_in, log_in_by_code, logout, onLoggedIn, endpoint, tokenEndpoint, set_hook } }