periodicjs.ext.reactadmin
Version:
An authentication extension for periodicjs that uses passport to authenticate user sessions.
671 lines (663 loc) • 27.8 kB
JavaScript
import constants from '../constants';
import { push, /* replace, go, goForward, goBack */ } from 'react-router-redux';
import { AsyncStorage, } from 'react-native';
import pageActions from './pages';
import uiActions from './ui';
import qs from 'querystring';
import utilities from '../util';
import manifest from './manifest';
import notification from './notification';
let __global__returnURL = false;
// import { Platform, } from 'react-web';
// import Immutable from 'immutable';
const checkStatus = function (response) {
if (response.status >= 200 && response.status < 300) {
return response;
} else {
let error = new Error(response.statusText);
error.response = response;
throw error;
}
};
var initializationThrottle;
var initializationTimeout;
const user = {
getUserStatus() {
return {
type: constants.user.CURRENT_USER_STATUS,
payload: {},
};
},
/**
* @param {string} url restful resource
*/
loginRequest(url) {
return {
type: constants.user.LOGIN_DATA_REQUEST,
payload: {
url,
},
};
},
/**
* @param {string} location name of extension to load
* @param {object} options what-wg fetch options
*/
recievedLoginUser(url, response, json) {
return {
type: constants.user.LOGIN_DATA_SUCCESS,
payload: {
url,
response,
json,
updatedAt: new Date(),
timestamp: Date.now(),
},
};
},
/**
* @param {string} location name of extension to load
* @param {object} options what-wg fetch options
*/
saveUserProfile(url, response, json) {
return {
type: constants.user.SAVE_DATA_SUCCESS,
payload: {
url,
response,
json,
updatedAt: new Date(),
timestamp: Date.now(),
},
};
},
updateUserProfileSuccess(profile) {
return {
type: constants.user.UPDATE_PROFILE_SUCCESS,
payload: {
profile,
updatedAt: new Date(),
timestamp: Date.now(),
},
};
},
updateUserProfile(profile) {
console.debug('updatedUserProfile', { profile, });
return (dispatch/*, getState*/) => {
AsyncStorage.setItem(constants.jwt_token.PROFILE_JSON, JSON.stringify(profile.userdata))
.then((status) => {
console.debug({ status, });
dispatch(this.updateUserProfileSuccess(profile));
})
.catch(e => {
console.error(e);
dispatch(notification.errorNotification(e));
});
};
},
/**
* @param {string} location name of extension to load
*/
failedUserRequest(url, error) {
return {
type: constants.user.USER_DATA_FAILURE,
payload: {
url,
error,
updatedAt: new Date(),
timestamp: Date.now(),
},
};
},
logoutUserSuccess() {
return {
type: constants.user.LOGOUT_SUCCESS,
payload: {
updatedAt: new Date(),
timestamp: Date.now(),
},
};
},
failedLogoutRequest(error) {
return {
type: constants.user.LOGOUT_FAILURE,
payload: {
error,
updatedAt: new Date(),
timestamp: Date.now(),
},
};
},
preferenceSuccessResponse (response) {
return {
type: constants.user.PREFERENCE_LOAD_SUCCESS,
payload: {
preferences: response.data.settings,
updatedAt: new Date(),
timestamp: Date.now(),
},
};
},
preferenceErrorResponse (error) {
return {
type: constants.user.PREFERENCE_LOAD_ERROR,
payload: {
error,
updatedAt: new Date(),
timestamp: Date.now(),
},
};
},
preferenceRequest () {
return {
type: constants.user.PREFERENCE_REQUEST,
payload: {},
};
},
navigationSuccessResponse (response) {
return {
type: constants.user.NAVIGATION_LOAD_SUCCESS,
payload: {
navigation: response.data.settings,
updatedAt: new Date(),
timestamp: Date.now(),
},
};
},
navigationErrorResponse (error) {
return {
type: constants.user.NAVIGATION_LOAD_ERROR,
payload: {
error,
updatedAt: new Date(),
timestamp: Date.now(),
},
};
},
navigationRequest () {
return {
type: constants.user.NAVIGATION_REQUEST,
payload: {},
};
},
authenticatedMFA (isAuthenticated = true) {
return {
type: constants.user.MFA_AUTHENTICATED,
payload: {
updatedAt: new Date(),
timestamp: Date.now(),
isMFAAuthenticated: isAuthenticated,
},
};
},
logoutUser() {
return (dispatch, getState) => {
let state = getState();
// console.debug({ state });
dispatch(pageActions.resetAppLoadedState());
Promise.all([
AsyncStorage.removeItem(constants.jwt_token.TOKEN_NAME),
AsyncStorage.removeItem(constants.jwt_token.TOKEN_DATA),
AsyncStorage.removeItem(constants.jwt_token.PROFILE_JSON),
AsyncStorage.removeItem(constants.user.MFA_AUTHENTICATED),
utilities.flushCacheConfiguration(['manifest.authenticated', 'user.navigation', 'user.preferences', ]),
// AsyncStorage.removeItem(constants.pages.ASYNCSTORAGE_KEY),
])
.then((/*results*/) => {
dispatch(this.logoutUserSuccess());
dispatch(pageActions.initialAppLoaded());
dispatch(uiActions.closeUISidebar());
dispatch(this.authenticatedMFA(false));
dispatch(push(state.settings.auth.logged_out_path || '/'));
let t = setImmediate(() => {
clearImmediate(t);
window.location.reload();
});
})
.catch(error => {
dispatch(notification.errorNotification(error));
dispatch(this.failedLogoutRequest(error));
dispatch(pageActions.initialAppLoaded());
dispatch(uiActions.closeUISidebar());
dispatch(push(state.settings.auth.logged_out_path || '/'));
let t = setImmediate(() => {
clearImmediate(t);
window.location.reload();
});
});
};
},
fetchPreferences (options = {}) {
let preferencesAction = (dispatch, getState) => {
dispatch(this.preferenceRequest());
let state = getState();
let hasCached;
let basename = (typeof state.settings.adminPath ==='string' && state.settings.adminPath !=='/') ? state.settings.basename+state.settings.adminPath : state.settings.basename;
let headers = state.settings.userprofile.options.headers;
delete headers.clientid_default;
options.headers = Object.assign({}, options.headers, headers);
return utilities.loadCacheConfigurations()
.then(result => {
hasCached = (result.user && result.user.preferences);
if (hasCached) {
dispatch(this.preferenceSuccessResponse({
data: {
settings: result.user.preferences,
},
}));
}
return utilities.fetchComponent(`${ basename }/load/preferences`, options)();
})
.then(response => {
dispatch(this.preferenceSuccessResponse(response));
return response;
}, e => {
if (!hasCached) dispatch(this.preferenceErrorResponse(e));
});
};
return utilities.setCacheConfiguration(preferencesAction, 'user.preferences');
},
fetchNavigation (options = {}) {
let navigationAction = (dispatch, getState) => {
dispatch(this.navigationRequest());
let state = getState();
let hasCached;
let basename = (typeof state.settings.adminPath ==='string' && state.settings.adminPath !=='/') ? state.settings.basename+state.settings.adminPath : state.settings.basename;
let headers = state.settings.userprofile.options.headers;
delete headers.clientid_default;
options.headers = Object.assign({}, options.headers, headers);
//add ?refresh=true to fetch route below to reload navigtion configuration
return utilities.loadCacheConfigurations()
.then(result => {
hasCached = (result.user && result.user.navigation);
if (hasCached) {
dispatch(this.navigationSuccessResponse({
data: {
settings: result.user.navigation,
},
}));
}
return utilities.fetchComponent(`${ basename }/load/navigation${(state.settings.ui.initialization.refresh_navigation)?'?refresh=true':''}`, options)();
})
.then(response => {
dispatch(this.navigationSuccessResponse(response));
return response;
}, e => {
if (!hasCached) dispatch(this.navigationErrorResponse(e));
});
};
return utilities.setCacheConfiguration(navigationAction, 'user.navigation');
},
getUserProfile(jwt_token, responseFormatter) {
return (dispatch, getState) => {
let fetchResponse;
let url = getState().settings.userprofile.url;
dispatch(this.loginRequest(url));
fetch(url, {
method: getState().settings.userprofile.method || 'POST',
headers: Object.assign({
'Accept': 'application/json',
'Content-Type': 'application/json',
}, getState().settings.userprofile.options.headers, {
'x-access-token': jwt_token,
}),
})
.then(checkStatus)
.then((response) => {
fetchResponse = response;
if (responseFormatter) {
let formatterPromise = responseFormatter(response);
if (formatterPromise instanceof Promise) {
return formatterPromise;
} else {
throw new Error('responseFormatter must return a Promise');
}
} else {
return response.json();
}
})
.then((responseData) => {
dispatch(this.saveUserProfile(url, fetchResponse, responseData));
})
.catch((error) => {
dispatch(notification.errorNotification(error));
dispatch(this.failedUserRequest(url, error));
});
};
},
enforceMFA(noRedirect, __returnURL) {
// console.debug('enforceMFA', { noRedirect, __returnURL, __global__returnURL, });
return (dispatch, getState) => {
let state = getState();
let extensionattributes = (state.user.userdata) ? state.user.userdata.extensionattributes : false;
let queryparams = qs.parse((window.location.search.charAt(0) === '?') ? window.location.search.substr(1, window.location.search.length) : window.location.search);
let formReturnURL = (!__global__returnURL && state && state.routing && state.routing.locationBeforeTransitions && state.routing.locationBeforeTransitions.pathname && state.dynamic && state.dynamic.formdata && (state.dynamic.formdata.__loginReturnURL || state.dynamic.formdata.__loginLastURL))
? state.dynamic.formdata.__loginReturnURL || state.routing.locationBeforeTransitions.pathname
: false;
let returnUrl = (!__global__returnURL && queryparams.return_url)
? queryparams.return_url
: __returnURL || __global__returnURL || formReturnURL || false;
if (state.routing && state.routing.locationBeforeTransitions && state.routing.locationBeforeTransitions.pathname && state.routing.locationBeforeTransitions.pathname === returnUrl) {
returnUrl = false;
}
console.debug({ formReturnURL, returnUrl, });
// console.log('state.settings.auth', state.settings.auth);
// console.log('state.user.isMFAAuthenticated', state.user.isMFAAuthenticated);
// console.log('state.manifest.containers[/mfa]', state.manifest.containers[ '/mfa' ]);
// console.log('state.manifest.containers[${state.settings.adminPath}/mfa]', state.manifest.containers[ `${state.settings.adminPath}/mfa` ]);
if (state.settings.auth.enforce_mfa || (extensionattributes && extensionattributes.login_mfa)) {
if (state.user.isMFAAuthenticated) {
if (!noRedirect) {
if (state.user.isLoggedIn && returnUrl) dispatch(push(returnUrl));
else dispatch(push(state.settings.auth.logged_in_homepage));
}
return true;
} else {
if (!state.manifest.containers || (state.manifest.containers && !state.manifest.containers['/mfa'] && !state.manifest.containers[`${state.settings.adminPath}/mfa`])) {
dispatch(notification.errorNotification(new Error('Multi-Factor Authentication not Properly Configured')));
this.logoutUser()(dispatch, getState);
} else {
// console.log('utilities.getMFAPath(state)')
let mfapath = utilities.getMFAPath(state);
if (!returnUrl && window.location.href && window.location.href.indexOf(mfapath) === -1) {
// console.debug({ returnUrl, mfapath, }, ' window.location.href', window.location.href);
returnUrl = window.location.href;
}
__global__returnURL = returnUrl;
// console.debug({ mfapath,returnUrl }, 'window.location.href', window.location.href);
dispatch(push(`${mfapath}${(returnUrl) ? '?return_url=' + returnUrl : ''}`));
}
return false;
}
} else {
if (!noRedirect) {
if (state.user.isLoggedIn && returnUrl) dispatch(push(returnUrl));
else dispatch(push(state.settings.auth.logged_in_homepage));
}
if (state.user.isLoggedIn && returnUrl) dispatch(push(returnUrl));
if (state.notification.modals && state.notification.modals.length && state.notification.modals[0]&& state.notification.modals[0].pathname && (state.notification.modals[0].pathname==='/signin' || state.notification.modals[0].pathname==='/login')) {
dispatch(notification.hideModal('last'));
}
__global__returnURL = false;
return true;
}
};
},
validateMFA (formdata = {}) {
let requestOptions = {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'x-access-token': undefined,
},
body: JSON.stringify(formdata),
};
return (dispatch, getState) => {
let state = getState();
let basename = (typeof state.settings.adminPath === 'string' && state.settings.adminPath !== '/') ? state.settings.basename + state.settings.adminPath : state.settings.basename;
let headers = state.settings.userprofile.options.headers;
delete headers.clientid_default;
let options = Object.assign({}, requestOptions);
options.headers = Object.assign({}, options.headers, { 'x-access-token': state.user.jwt_token, });
return utilities.fetchComponent(`${ basename }/load/mfa`, options)()
.then(response => {
if (response && response.data && response.data.authenticated) {
dispatch(this.authenticatedMFA());
return AsyncStorage.setItem(constants.user.MFA_AUTHENTICATED, true)
.then(() => response, e => Promise.reject(e));
}
return response;
})
.then(response => {
if (response.result === 'error') dispatch(notification.errorNotification(new Error(response.data.error)));
return this.enforceMFA()(dispatch, getState);
})
.catch(e => {
dispatch(notification.errorNotification(e));
});
};
},
fetchConfigurations (options = {}) {
return (dispatch, getState) => {
let state = getState();
// console.log({ state, });
if ((state.manifest && state.manifest.authenticated && state.manifest.authenticated.hasLoaded) || (state.settings.user && state.settings.user.navigation && state.settings.user.navigation.hasLoaded) || (state.settings.user && state.settings.user.preferences && state.settings.user.preferences.hasLoaded)) {
let operations = [];
if (!state.manifest || (state.manifest && state.manifest.authenticated && !state.manifest.authenticated.hasLoaded)) operations.push(manifest.fetchManifest(options)(dispatch, getState));
if (!state.settings || (state.settings && !state.settings.user)) {
operations.push(this.fetchNavigation(options)(dispatch, getState));
operations.push(this.fetchPreferences(options)(dispatch, getState));
} else {
if (!state.settings.user.navigation || (state.settings.user.navigation && !state.settings.user.navigation.hasLoaded)) operations.push(this.fetchNavigation(options)(dispatch, getState));
if (!state.settings.user.preferences || (state.settings.user.preferences && !state.settings.user.preferences.hasLoaded)) operations.push(this.fetchPreferences(options)(dispatch, getState));
}
return Promise.all(operations);
} else {
dispatch(this.navigationRequest());
dispatch(this.preferenceRequest());
dispatch(manifest.manifestRequest());
let basename = (typeof state.settings.adminPath === 'string' && state.settings.adminPath !== '/') ? state.settings.basename + state.settings.adminPath : state.settings.basename;
let headers = state.settings.userprofile.options.headers;
delete headers.clientid_default;
options.headers = Object.assign({}, options.headers, headers);
return utilities.loadCacheConfigurations()
.then(result => {
if (result.user) {
if (result.user.navigation) {
dispatch(this.navigationSuccessResponse({
data: {
settings: result.user.navigation,
},
}));
}
if (result.user.preferences) {
dispatch(this.preferenceSuccessResponse({
data: {
settings: result.user.preferences,
},
}));
}
}
if (result.manifest && result.manifest.authenticated) dispatch(manifest.receivedManifestData(result.manifest.authenticated));
return true;
})
.then(() => {
//add ?refresh=true to fetch route below to reload configurations
return utilities.setCacheConfiguration(() => {
let refreshComponents = state.settings.ui.initialization.refresh_components;
let params = (refreshComponents) ? '?refresh=true' : '';
let configurationRoute = `${ basename }/load/configurations${ params }`;
return utilities.fetchComponent(configurationRoute, options)()
.then(response => {
if (response.result === 'error') return Promise.reject(new Error(response.data.error));
let responses = Object.keys(response.data.settings).reduce((result, key) => {
let data = Object.assign({}, response.data);
data.settings = response.data.settings[key];
result[key] = { data, };
return result;
}, {});
dispatch(this.navigationSuccessResponse(responses.navigation));
dispatch(this.preferenceSuccessResponse(responses.preferences));
dispatch(manifest.receivedManifestData(responses.manifest.data.settings));
return {
data: {
versions: response.data.versions,
settings: responses,
},
};
})
.catch(e => {
dispatch(this.navigationErrorResponse(e));
dispatch(this.preferenceErrorResponse(e));
dispatch(manifest.failedManifestRetrival(e));
});
}, {
navigation: 'user.navigation',
preferences: 'user.preferences',
manifest: 'manifest.authenticated',
unauthenticated_manifest: 'manifest.unauthenticated',
}, { multi: true, })();
})
.catch(e => {
dispatch(this.navigationErrorResponse(e));
dispatch(this.preferenceErrorResponse(e));
dispatch(manifest.failedManifestRetrival(e));
});
}
};
},
initializeAuthenticatedUser(token, ensureMFA, __returnURL) {
// console.debug('initializeAuthenticatedUser', { token, ensureMFA, __returnURL, __global__returnURL, });
// console.debug({ __returnURL });
return (dispatch, getState) => {
let requestOptions = {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'x-access-token': token,
},
};
let state = getState();
if (state.manifest && state.manifest.authenticated && state.manifest.authenticated.hasLoaded && state.settings && state.settings.user && state.settings.user.navigation && state.settings.user.navigation.hasLoaded && state.settings.user.preferences && state.settings.user.preferences.hasLoaded) {
if (initializationTimeout) {
clearTimeout(initializationTimeout);
initializationThrottle.destroyInactiveThrottle();
}
// console.debug('enforceMFA clearTimeout __returnURL', __returnURL);
return (ensureMFA !== false) ? this.enforceMFA(undefined, __returnURL)(dispatch, getState) : undefined;
} else {
let assignThrottle = (resolve, reject) => {
let throttle = () => {
initializationTimeout = setTimeout(() => {
clearTimeout(initializationTimeout);
// console.debug('throttled enforceMFA clearTimeout __returnURL', __returnURL);
this.fetchConfigurations(requestOptions)(dispatch, getState)
.then(() => (ensureMFA !== false) ? this.enforceMFA(undefined, __returnURL)(dispatch, getState) : undefined)
.then(resolve)
.catch(reject);
}, 10);
};
throttle.destroyInactiveThrottle = resolve;
return throttle;
};
return new Promise((resolve, reject) => {
if (!initializationTimeout) {
initializationThrottle = assignThrottle(resolve, reject);
initializationThrottle();
} else {
clearTimeout(initializationTimeout);
initializationThrottle.destroyInactiveThrottle();
initializationThrottle = assignThrottle(resolve, reject);
initializationThrottle();
}
});
}
};
},
/**
* @param {string} url url for fetch request
* @param {object} options what-wg fetch options
* @param {function} responseFormatter custom reponse formatter, must be a function that returns a promise that resolves to json/javascript object
*/
loginUser(loginData, __returnURL) {
// console.debug('loginUser', { loginData, __returnURL, });
return (dispatch, getState) => {
let fetchResponse;
let cachedResponseData;
let loginSettings = getState().settings.login;
let notificationsSettings = getState().settings.ui.notifications;
let url = loginSettings.url;
loginData.username = (loginSettings.lowercaseUsername)
? loginData.username.toLowerCase()
: loginData.username;
dispatch(this.loginRequest(url));
fetch(url, {
method: loginSettings.method || 'POST',
headers: Object.assign({
'Accept': 'application/json',
// 'Content-Type': 'application/json',
}, loginSettings.options.headers, {
username: loginData.username,
password: loginData.password,
}),
body: JSON.stringify({
username: loginData.username,
password: loginData.password,
}),
})
.then(checkStatus)
.then((response) => response.json())
.then((responseData) => {
if (responseData.user && responseData.user.locked) {
dispatch(notification.errorNotification(notificationsSettings.locked_user_account_error || 'User account is locked. Please contact us.'));
throw new Error('Locked Account');
} else {
return responseData;
}
})
.then((responseData) => {
cachedResponseData = responseData;
// console.debug('loginData.__returnURL', loginData.__returnURL);
__global__returnURL = loginData.__returnURL || __returnURL;
return Promise.all([
AsyncStorage.setItem(constants.jwt_token.TOKEN_NAME, responseData.token),
AsyncStorage.setItem(constants.jwt_token.TOKEN_DATA, JSON.stringify({
expires: responseData.expires,
timeout: responseData.timeout,
token: responseData.token,
})),
AsyncStorage.setItem(constants.jwt_token.PROFILE_JSON, JSON.stringify(responseData.user)),
this.initializeAuthenticatedUser(responseData.token, false, __global__returnURL)(dispatch, getState),
]);
})
.then(() => {
if (loginSettings.GPS && loginSettings.GPS.login) {
return fetch(loginSettings.GPS.loginURL, {
method: loginSettings.method || 'POST',
headers: Object.assign({
'Accept': 'application/json',
}, loginSettings.options.headers, {
username: loginSettings.userename || loginData.username,
password: loginSettings.password || loginData.password,
}),
body: JSON.stringify({
username: loginSettings.userename || loginData.username,
password: loginSettings.password || loginData.password,
response: cachedResponseData,
}),
})
.then(response => {
return response.json();
});
} else {
return Promise.resolve();
}
})
.then((gpsLoginResponse) => {
console.debug({ gpsLoginResponse });
let __returnURL = (gpsLoginResponse && gpsLoginResponse.__returnURL) ? gpsLoginResponse.__returnURL : __returnURL;
__global__returnURL = __returnURL;
dispatch(this.recievedLoginUser(url, fetchResponse, cachedResponseData));
if(!notificationsSettings.hide_login_notification){
dispatch(notification.createNotification({ text: 'Welcome back', timeout:4000, type:'success', }));
}
return this.enforceMFA(null, __returnURL)(dispatch, getState);
})
.catch((error) => {
if (error && error.message && error.message === 'Locked Account') {
return 'Locked Account';
} else {
if(notificationsSettings.login_error_message){
dispatch(notification.errorNotification(notificationsSettings.login_error_message));
} else {
dispatch(notification.errorNotification(error));
}
dispatch(this.failedUserRequest(url, error));
}
});
};
},
};
export default user;