fix: handle token expiry (#3847)

This commit is contained in:
Erez Rokah
2020-06-03 12:44:03 +03:00
committed by GitHub
parent 43ef28b5dc
commit 285c940562
20 changed files with 282 additions and 17 deletions

View File

@ -1,7 +1,7 @@
import { actions as notifActions } from 'redux-notifications';
import { currentBackend } from 'coreSrc/backend';
const { notifSend } = notifActions;
const { notifSend, notifClear } = notifActions;
export const AUTH_REQUEST = 'AUTH_REQUEST';
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
@ -111,6 +111,7 @@ export function logoutUser() {
const backend = currentBackend(state.config);
Promise.resolve(backend.logout()).then(() => {
dispatch(logout());
dispatch(notifClear());
});
};
}

View File

@ -0,0 +1,66 @@
import { State } from '../types/redux';
import { currentBackend } from '../backend';
import { ThunkDispatch } from 'redux-thunk';
import { AnyAction } from 'redux';
import { actions as notifActions } from 'redux-notifications';
const { notifSend } = notifActions;
export const STATUS_REQUEST = 'STATUS_REQUEST';
export const STATUS_SUCCESS = 'STATUS_SUCCESS';
export const STATUS_FAILURE = 'STATUS_FAILURE';
export function statusRequest() {
return {
type: STATUS_REQUEST,
};
}
export function statusSuccess(status: { auth: boolean }) {
return {
type: STATUS_SUCCESS,
payload: { status },
};
}
export function statusFailure(error: Error) {
return {
type: STATUS_FAILURE,
error,
};
}
export function checkBackendStatus() {
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
try {
const state = getState();
if (state.status.get('isFetching') === true) {
return;
}
dispatch(statusRequest());
const backend = currentBackend(state.config);
const status = await backend.status();
const authError = status.auth === false;
if (authError) {
const key = 'ui.toast.onLoggedOut';
const existingNotification = state.notifs.find(n => n.message?.key === key);
if (!existingNotification) {
dispatch(
notifSend({
message: {
key: 'ui.toast.onLoggedOut',
},
kind: 'danger',
}),
);
}
}
dispatch(statusSuccess(status));
} catch (error) {
dispatch(statusFailure(error));
}
};
}

View File

@ -179,6 +179,21 @@ export class Backend {
}
}
async status() {
const attempts = 3;
let status: { auth: boolean } = { auth: false };
for (let i = 1; i <= attempts; i++) {
status = await this.implementation!.status();
// return on first success
if (Object.values(status).every(s => s === true)) {
return status;
} else {
await new Promise(resolve => setTimeout(resolve, i * 1000));
}
}
return status;
}
currentUser() {
if (this.user) {
return this.user;
@ -222,13 +237,17 @@ export class Backend {
});
}
logout() {
return Promise.resolve(this.implementation.logout()).then(() => {
async logout() {
try {
await this.implementation.logout();
} catch (e) {
console.warn('Error during logout', e.message);
} finally {
this.user = null;
if (this.authStore) {
this.authStore.logout();
}
});
}
}
getToken = () => this.implementation.getToken();

View File

@ -17,6 +17,8 @@ import {
zIndex,
} from 'netlify-cms-ui-default';
import SettingsDropdown from 'UI/SettingsDropdown';
import { connect } from 'react-redux';
import { checkBackendStatus } from '../../actions/status';
const styles = {
buttonActive: css`
@ -120,8 +122,21 @@ class Header extends React.Component {
displayUrl: PropTypes.string,
isTestRepo: PropTypes.bool,
t: PropTypes.func.isRequired,
checkBackendStatus: PropTypes.func.isRequired,
};
intervalId;
componentDidMount() {
this.intervalId = setInterval(() => {
this.props.checkBackendStatus();
}, 5 * 60 * 1000);
}
componentWillUnmount() {
clearInterval(this.intervalId);
}
handleCreatePostClick = collectionName => {
const { onCreateEntryClick } = this.props;
if (onCreateEntryClick) {
@ -211,4 +226,8 @@ class Header extends React.Component {
}
}
export default translate()(Header);
const mapDispatchToProps = {
checkBackendStatus,
};
export default connect(null, mapDispatchToProps)(translate()(Header));

View File

@ -1,7 +1,12 @@
import { Map } from 'immutable';
import { USE_OPEN_AUTHORING } from 'Actions/auth';
const LOADING_IGNORE_LIST = ['DEPLOY_PREVIEW'];
const LOADING_IGNORE_LIST = [
'DEPLOY_PREVIEW',
'STATUS_REQUEST',
'STATUS_SUCCESS',
'STATUS_FAILURE',
];
const ignoreWhenLoading = action => LOADING_IGNORE_LIST.some(type => action.type.includes(type));

View File

@ -11,6 +11,7 @@ import medias from './medias';
import mediaLibrary from './mediaLibrary';
import deploys, * as fromDeploys from './deploys';
import globalUI from './globalUI';
import status from './status';
import { Status } from '../constants/publishModes';
import { State, Collection } from '../types/redux';
@ -28,6 +29,7 @@ const reducers = {
mediaLibrary,
deploys,
globalUI,
status,
};
export default reducers;

View File

@ -0,0 +1,33 @@
import { Map, fromJS } from 'immutable';
import { AnyAction } from 'redux';
import { STATUS_REQUEST, STATUS_SUCCESS, STATUS_FAILURE } from '../actions/status';
import { Status } from '../types/redux';
export interface EntriesAction extends AnyAction {
payload: { status: { auth: boolean }; error?: Error };
}
const status = (state = Map(), action: EntriesAction) => {
switch (action.type) {
case STATUS_REQUEST:
return state.set('isFetching', true);
case STATUS_SUCCESS:
return state.withMutations(map => {
map.set('isFetching', false);
map.set('status', fromJS(action.payload.status));
});
case STATUS_FAILURE:
return state.withMutations(map => {
map.set('isFetching', false);
map.set('error', action.payload.error);
});
default:
return state;
}
};
export const selectStatus = (status: Status) => {
return status.get('status')?.toJS() || {};
};
export default status;

View File

@ -241,6 +241,11 @@ export type Search = StaticallyTypedRecord<{
export type Cursors = StaticallyTypedRecord<{}>;
export type Status = StaticallyTypedRecord<{
isFetching: boolean;
status: StaticallyTypedRecord<{ auth: boolean }>;
}>;
export interface State {
config: Config;
cursors: Cursors;
@ -253,6 +258,8 @@ export interface State {
medias: Medias;
mediaLibrary: MediaLibrary;
search: Search;
notifs: { message: { key: string }; kind: string; id: number }[];
status: Status;
}
export interface MediasAction extends Action<string> {