fix: handle token expiry (#3847)
This commit is contained in:
@ -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());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
66
packages/netlify-cms-core/src/actions/status.ts
Normal file
66
packages/netlify-cms-core/src/actions/status.ts
Normal 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));
|
||||
}
|
||||
};
|
||||
}
|
@ -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();
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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;
|
||||
|
33
packages/netlify-cms-core/src/reducers/status.ts
Normal file
33
packages/netlify-cms-core/src/reducers/status.ts
Normal 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;
|
@ -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> {
|
||||
|
Reference in New Issue
Block a user