UNPKG

@selfcommunity/react-core

Version:

React Core Components useful for integrating UI Community components (react-ui).

269 lines (262 loc) • 11.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.userActionTypes = void 0; const tslib_1 = require("tslib"); const api_services_1 = require("@selfcommunity/api-services"); const react_1 = require("react"); const Session = tslib_1.__importStar(require("../constants/Session")); const use_deep_compare_effect_1 = tslib_1.__importDefault(require("use-deep-compare-effect")); const utils_1 = require("@selfcommunity/utils"); const Errors_1 = require("../constants/Errors"); /** * @hidden * We have complex state logic that involves multiple sub-values, * so useReducer is preferable to useState. * Define all possible auth action types label * Use this to export actions and dispatch an action */ exports.userActionTypes = { LOGIN_LOADING: '_login_loading', LOGIN_SUCCESS: '_login_success', LOGIN_FAILURE: '_login_failure', LOGOUT: '_logout', REFRESH_TOKEN_SUCCESS: '_refresh_token_success', REFRESH_TOKEN_FAILURE: '_invalid_token_failure', REFRESH_SESSION: '_refresh_token', UPDATE_USER: '_change_user', }; /** * userReducer: * - manage the state of authentication * - update the state base on action type * @param state * @param action */ function userReducer(state, action) { switch (action.type) { case exports.userActionTypes.LOGIN_LOADING: return { session: Object.assign({}, state.session), error: null, loading: true }; case exports.userActionTypes.LOGIN_SUCCESS: return { user: action.payload.user, error: null, session: Object.assign({}, state.session), loading: false }; case exports.userActionTypes.LOGIN_FAILURE: return { user: null, session: Object.assign({}, state.session), error: action.payload && action.payload.error ? action.payload.error : null, loading: false, }; case exports.userActionTypes.REFRESH_TOKEN_SUCCESS: const newAuthToken = Object.assign({}, state.session.authToken, Object.assign(Object.assign(Object.assign(Object.assign({}, state.session.authToken), { accessToken: action.payload.token.accessToken }), (action.payload.token.refreshToken ? { refreshToken: action.payload.token.refreshToken } : {})), (action.payload.token.expiresIn ? { expiresIn: action.payload.token.expiresIn } : {}))); const newSession = Object.assign({}, state.session, { authToken: newAuthToken, }); // Update current client config api_services_1.http.setAuthorizeToken(newAuthToken.accessToken); return Object.assign(Object.assign({}, state), { session: newSession, error: null, loading: false }); case exports.userActionTypes.REFRESH_TOKEN_FAILURE: return { user: null, session: Object.assign({}, state.session), loading: null, error: action.payload.error }; case exports.userActionTypes.LOGOUT: return { user: undefined, session: {}, error: null, loading: null }; case exports.userActionTypes.UPDATE_USER: return Object.assign(Object.assign({}, state), { user: Object.assign(Object.assign({}, state.user), action.payload) }); case exports.userActionTypes.REFRESH_SESSION: return Object.assign(Object.assign({}, state), action.payload.conf); default: throw new Error(`Unhandled type: ${action.type}`); } } /** * Define initial context auth session * @param session */ function stateInitializer(session) { let _session = Object.assign({}, session); let _isLoading = true; /** * Set http authorization if session type is OAuth or JWT * Configure http object (Authorization, etc...) */ if ([Session.OAUTH_SESSION, Session.JWT_SESSION].includes(_session.type)) { if (_session.authToken && _session.authToken.accessToken) { api_services_1.http.setAuthorizeToken(_session.authToken.accessToken); } else { api_services_1.http.setAuthorizeToken(); } } else if (_session.type === Session.COOKIE_SESSION) { /** * if the session is of type Cookie -> reset header token * and keep the session on loading to recover the logged user */ api_services_1.http.setAuthorizeToken(); } api_services_1.http.setSupportWithCredentials(_session.type === Session.COOKIE_SESSION); return { user: _isLoading ? undefined : null, session: _session, error: null, loading: _isLoading, isSessionRefreshing: false, refreshSession: false, }; } /** :::info This component is used to navigate through the application. ::: #### Usage In order to use router you need to import this components first: ```jsx import {SCRoutingContextType, useSCRouting, Link, SCRoutes} from '@selfcommunity/react-core'; ```` :::tip Usage Example: ```jsx const scRoutingContext: SCRoutingContextType = useSCRouting(); <Button component={Link} to={scRoutingContext.url(SCRoutes.USER_PROFILE_ROUTE_NAME, {id: user.id})>Go to profile</Button> ```` or ```jsx const scRoutingContext: SCRoutingContextType = useSCRouting(); <Link to={scRoutingContext.url('profile', {id: user.id})}>Go to profile</Link> ```` ::: * @param initialSession */ function useAuth(initialSession) { const [state, dispatch] = (0, react_1.useReducer)(userReducer, {}, () => stateInitializer(initialSession)); let authInterceptor = (0, react_1.useRef)(null); let isSessionRefreshing = (0, react_1.useRef)(false); let failedQueue = (0, react_1.useRef)([]); // CONST const userId = state.user ? state.user.id : null; const accessToken = state.session.authToken && state.session.authToken.accessToken ? state.session.authToken.accessToken : null; /** * Reset session if initial conf changed */ (0, use_deep_compare_effect_1.default)(() => { dispatch({ type: exports.userActionTypes.REFRESH_SESSION, payload: { conf: stateInitializer(initialSession) } }); }, [initialSession]); /** * Refresh session */ const refreshSession = (0, react_1.useMemo)(() => () => { const session = state.session; if (!isSessionRefreshing.current && session.handleRefreshToken) { isSessionRefreshing.current = true; return session .handleRefreshToken(state.session) .then((res) => { isSessionRefreshing.current = false; dispatch({ type: exports.userActionTypes.REFRESH_TOKEN_SUCCESS, payload: { token: res } }); return Promise.resolve(res); }) .catch((error) => { utils_1.Logger.error(Errors_1.SCOPE_SC_CORE, 'Unable to refresh user session.'); if (error.response && error.response.data) { dispatch({ type: exports.userActionTypes.REFRESH_TOKEN_FAILURE, payload: { error: error.response.toString() } }); } return Promise.reject(error); }); } return Promise.reject(new Error('Unable to refresh session. Unauthenticated user.')); }, [accessToken]); /** * Logout session */ const logoutSession = (0, react_1.useMemo)(() => () => { dispatch({ type: exports.userActionTypes.LOGOUT }); const session = state.session; if (session.handleLogout) { session.handleLogout(); } }, []); /** * Manages multiple request during refresh session * Save concurrent requests and retry them again * at the end of refreshing session */ const processQueue = (0, react_1.useMemo)(() => (error, token = null) => { failedQueue.current.forEach((prom) => { if (error) { prom.reject(error); } else { prom.resolve(token); } }); failedQueue.current = []; }, [failedQueue.current]); /** * Add/remove an http request interceptor. * When the component unmounted the interceptor will be detached * The interceptor check if the token is expiring */ (0, react_1.useEffect)(() => { if (userId !== null) { authInterceptor.current = api_services_1.http.getClientInstance().interceptors.response.use((response) => { return response; }, (error) => tslib_1.__awaiter(this, void 0, void 0, function* () { let originalConfig = error.config; if (error.response) { if (error.response.status === 401) { /** * if other requests arrive at the same time * as the token refresh, we save them for later */ if (isSessionRefreshing.current) { return new Promise(function (resolve, reject) { failedQueue.current.push({ resolve, reject }); }) .then((token) => { originalConfig.headers['Authorization'] = 'Bearer ' + token; return api_services_1.http.request(originalConfig); }) .catch((err) => { utils_1.Logger.error(Errors_1.SCOPE_SC_CORE, 'Unable to resolve promises in failedQueue.'); return Promise.reject(err); }); } /** * we mark the request as retried, * we avoid doing it again */ const session = state.session; const authToken = session && 'authToken' in session ? session.authToken : null; if (session.type !== Session.COOKIE_SESSION && !isSessionRefreshing.current && state.user && session && session.handleRefreshToken && Boolean(authToken && authToken.refreshToken)) { /** * set refreshing mode, * save all concurrent request in the meantime */ try { const res = yield refreshSession(); originalConfig.headers.Authorization = `Bearer ${res['accessToken']}`; processQueue(null, res['accessToken']); return Promise.resolve(api_services_1.http.request(originalConfig)); } catch (_error) { if (_error.response && _error.response.data) { processQueue(_error, null); return Promise.reject(_error.response.data); } } } } return Promise.reject(error); } })); } return () => { if (authInterceptor.current !== null) { api_services_1.http.getClientInstance().interceptors.response.eject(authInterceptor.current); } }; }, [userId, accessToken]); return { state, dispatch, helpers: { refreshSession, logoutSession } }; } exports.default = useAuth;