@selfcommunity/react-core
Version:
React Core Components useful for integrating UI Community components (react-ui).
269 lines (262 loc) • 11.7 kB
JavaScript
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;
;