diff --git a/packages/apps/admin/src/components/Admin/Admin.js b/packages/apps/admin/src/components/Admin/Admin.js index 16f7dc3afa..5a0274adaf 100644 --- a/packages/apps/admin/src/components/Admin/Admin.js +++ b/packages/apps/admin/src/components/Admin/Admin.js @@ -14,6 +14,7 @@ import EntitiesRoute from '../../routes/entities' import Settings from '../../routes/settings' import ErrorView from '../ErrorView' import Header from '../Header' +import InvalidSession from '../InvalidSession' import Navigation from '../Navigation' import navigationStrategy from './../../routes/entities/utils/navigationStrategy' import {burgerMenuStyles, StyledContent, StyledMenu, StyledWrapper} from './StyledComponents' @@ -105,16 +106,21 @@ const Admin = ({ ) return ( - - - - - -
- {adminAllowedContent || adminForbiddenContent} - - - + <> + + + + + + + + +
+ {adminAllowedContent || adminForbiddenContent} + + + + ) } @@ -128,7 +134,7 @@ Admin.propTypes = { initializeNavigation: PropTypes.func.isRequired, loadSettingsAndPreferences: PropTypes.func.isRequired, theme: PropTypes.object.isRequired, - adminAllowed: PropTypes.bool.isRequired + adminAllowed: PropTypes.bool } export default withTheme(Admin) diff --git a/packages/apps/admin/src/components/InvalidSession/InvalidSession.js b/packages/apps/admin/src/components/InvalidSession/InvalidSession.js new file mode 100644 index 0000000000..f7cec696a2 --- /dev/null +++ b/packages/apps/admin/src/components/InvalidSession/InvalidSession.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types' +import ReactDOM from 'react-dom' +import {FormattedMessage} from 'react-intl' +import styled from 'styled-components' +import {notification} from 'tocco-app-extensions' +import {Typography} from 'tocco-ui' + +import Login from '../Login' + +const StyledLoginHolder = styled(notification.StyledModalHolder)` + // higher than StyledModalHolder + // higher than StyledToasterBox + z-index: 100000; +` + +const InvalidSession = ({invalidSession, intl}) => { + const msg = id => intl.formatMessage({id}) + + const Content = () => ( + <> + +
+ + + + ) + + if (!invalidSession) { + return null + } + + return ReactDOM.createPortal( + + } + component={() => } + onClose={() => {}} + onCancel={() => {}} + /> + + , + document.body + ) +} + +InvalidSession.propTypes = { + intl: PropTypes.object.isRequired, + invalidSession: PropTypes.bool +} + +export default InvalidSession diff --git a/packages/apps/admin/src/components/InvalidSession/InvalidSessionContainer.js b/packages/apps/admin/src/components/InvalidSession/InvalidSessionContainer.js new file mode 100644 index 0000000000..799c9ba9f7 --- /dev/null +++ b/packages/apps/admin/src/components/InvalidSession/InvalidSessionContainer.js @@ -0,0 +1,12 @@ +import {injectIntl} from 'react-intl' +import {connect} from 'react-redux' + +import InvalidSession from './InvalidSession' + +const mapActionCreators = {} + +const mapStateToProps = (state, props) => ({ + invalidSession: state.session.invalidSession +}) + +export default connect(mapStateToProps, mapActionCreators)(injectIntl(InvalidSession)) diff --git a/packages/apps/admin/src/components/InvalidSession/index.js b/packages/apps/admin/src/components/InvalidSession/index.js new file mode 100644 index 0000000000..b1d290d56f --- /dev/null +++ b/packages/apps/admin/src/components/InvalidSession/index.js @@ -0,0 +1 @@ +export {default} from './InvalidSessionContainer' diff --git a/packages/apps/admin/src/components/Login/Login.js b/packages/apps/admin/src/components/Login/Login.js index 1fba28ca48..58a457feb1 100644 --- a/packages/apps/admin/src/components/Login/Login.js +++ b/packages/apps/admin/src/components/Login/Login.js @@ -5,18 +5,7 @@ import {FormattedMessage} from 'react-intl' import ToccoLogin from 'tocco-login/src/main' import SsoLogin from 'tocco-sso-login/src/main' -import ToccoSlogan from '../../assets/tocco_white.svg' -import { - StyledSsoMsg, - StyledSsoError, - StyledSpanLogin, - StyledLogin, - StyledMobileSloganImg, - StyledSloganImg, - StyledLoginWrapper, - StyledHeadingLogin, - GlobalBodyStyle -} from './StyledComponents' +import {StyledSsoMsg, StyledSsoError, StyledSpanLogin} from './StyledComponents' const Login = ({ssoAvailable, loginSuccessful, checkSsoAvailable}) => { const [showError, setShowError] = useState(false) @@ -27,14 +16,10 @@ const Login = ({ssoAvailable, loginSuccessful, checkSsoAvailable}) => { checkSsoAvailable() }, [checkSsoAvailable]) - const loginSuccess = ({timeout}) => { - loginSuccessful(timeout) - } - const ssoLoginCompleted = ({successful, provider, registration}) => { if (successful) { Cookies.set('sso-autologin', provider, {expires: 365}) - loginSuccessful(30) + loginSuccessful() } else if (!successful && registration) { setShowRegistrationText(true) } else { @@ -43,7 +28,7 @@ const Login = ({ssoAvailable, loginSuccessful, checkSsoAvailable}) => { } const SsoLoginPart = () => ( - <> +
{showRegistrationText && ( @@ -58,23 +43,15 @@ const Login = ({ssoAvailable, loginSuccessful, checkSsoAvailable}) => { - +
) return ( <> - - - - - - - - - {ssoAvailable && } - - - + {ssoAvailable && } +
+ +
) } diff --git a/packages/apps/admin/src/components/Login/StyledComponents.js b/packages/apps/admin/src/components/Login/StyledComponents.js index 0829ccbf99..1f1ea086b9 100644 --- a/packages/apps/admin/src/components/Login/StyledComponents.js +++ b/packages/apps/admin/src/components/Login/StyledComponents.js @@ -1,47 +1,6 @@ -import styled, {createGlobalStyle} from 'styled-components' -import {scale, StyledH1, StyledSpan, theme} from 'tocco-ui' +import styled from 'styled-components' +import {scale, StyledSpan, theme} from 'tocco-ui' -import ToccoLogo from '../../assets/tocco-circle.svg' - -// Overwrite index.html overflow which is hidden -export const GlobalBodyStyle = createGlobalStyle` - @media (max-width: 1024px) { - body { - overflow: auto !important; - } - } -` - -export const StyledLogin = styled.div` - height: 100vh; - width: 100vw; - background-image: url(${ToccoLogo}); - background-repeat: no-repeat; - background-size: 61vw; - background-position-y: -25vw; - background-position-x: -41vw; - - .tocco-sso-login { - display: flex; - justify-content: space-between; - margin-bottom: 1.8rem; - } - - @media (max-width: 1024px) { - background-size: 2000px; - background-position: 50% -1850px; - } - - @media (max-width: 425px) { - background-position: 50% -1890px; - } -` - -export const StyledHeadingLogin = styled(StyledH1)` - && { - font-size: ${scale.font(11)}; - } -` export const StyledSpanLogin = styled(StyledSpan)` && { text-align: center; @@ -52,57 +11,6 @@ export const StyledSpanLogin = styled(StyledSpan)` } ` -export const StyledLoginWrapper = styled.div` - max-width: 410px; - margin: 6% 5% 0 28%; - - && { - .tocco-login * { - font-size: ${scale.font(1.3)}; - } - - @media (max-width: 1024px) { - margin: 14rem auto 0; - padding-left: 2rem; - padding-right: 2rem; - } - - @media (max-width: 425px) { - margin-top: 12rem; - } - } -` - -export const StyledSloganImg = styled.img` - transform: rotate(270deg); - position: relative; - top: 12.5vw; - left: -8%; - width: 25vw; - - @media (max-width: 1024px) { - display: none; - } -` - -export const StyledMobileSloganImg = styled.img` - display: none; - max-width: 400px; - width: 95%; - height: auto; - position: relative; - margin: auto; - top: 45px; - - @media (max-width: 1024px) { - display: block; - } - - @media (max-width: 425px) { - max-width: 280px; - } -` - export const StyledSsoMsg = styled(StyledSpan)` && { display: inline-block; diff --git a/packages/apps/admin/src/components/LoginGuard/LoginGuard.js b/packages/apps/admin/src/components/LoginGuard/LoginGuard.js index ccfe1551d7..3a7410aac8 100644 --- a/packages/apps/admin/src/components/LoginGuard/LoginGuard.js +++ b/packages/apps/admin/src/components/LoginGuard/LoginGuard.js @@ -3,13 +3,14 @@ import {useEffect} from 'react' import {Helmet} from 'react-helmet' import {LoadMask} from 'tocco-ui' -import Login from '../../components/Login' +import LoginScreen from '../../components/LoginScreen' import Admin from '../Admin' -const LoginGuard = ({doSessionCheck, loggedIn}) => { +const LoginGuard = ({connectSocket, sessionHeartbeat, loggedIn}) => { useEffect(() => { - doSessionCheck() - }, [doSessionCheck]) + connectSocket() + sessionHeartbeat() + }, [connectSocket, sessionHeartbeat]) return (
@@ -17,7 +18,7 @@ const LoginGuard = ({doSessionCheck, loggedIn}) => { Tocco -
{!loggedIn ? : }
+
{!loggedIn ? : }
) @@ -25,7 +26,8 @@ const LoginGuard = ({doSessionCheck, loggedIn}) => { LoginGuard.propTypes = { loggedIn: PropTypes.bool, - doSessionCheck: PropTypes.func.isRequired + connectSocket: PropTypes.func.isRequired, + sessionHeartbeat: PropTypes.func.isRequired } export default LoginGuard diff --git a/packages/apps/admin/src/components/LoginGuard/LoginGuardContainer.js b/packages/apps/admin/src/components/LoginGuard/LoginGuardContainer.js index 49dd4100dd..aa2e8444cb 100644 --- a/packages/apps/admin/src/components/LoginGuard/LoginGuardContainer.js +++ b/packages/apps/admin/src/components/LoginGuard/LoginGuardContainer.js @@ -1,11 +1,13 @@ import {injectIntl} from 'react-intl' import {connect} from 'react-redux' -import {notification, login} from 'tocco-app-extensions' +import {notification} from 'tocco-app-extensions' +import {sessionHeartbeat} from '../../modules/session/actions' import LoginGuard from './LoginGuard' const mapActionCreators = { - doSessionCheck: login.doSessionCheck, + sessionHeartbeat, + connectSocket: notification.connectSocket, confirm: notification.confirm } diff --git a/packages/apps/admin/src/components/LoginScreen/LoginScreen.js b/packages/apps/admin/src/components/LoginScreen/LoginScreen.js new file mode 100644 index 0000000000..7848818d99 --- /dev/null +++ b/packages/apps/admin/src/components/LoginScreen/LoginScreen.js @@ -0,0 +1,30 @@ +import {FormattedMessage} from 'react-intl' + +import ToccoSlogan from '../../assets/tocco_white.svg' +import LoginForm from '../Login' +import { + StyledLogin, + StyledMobileSloganImg, + StyledSloganImg, + StyledLoginWrapper, + StyledHeadingLogin, + GlobalBodyStyle +} from './StyledComponents' + +const LoginScreen = () => ( + <> + + + + + + + + + + + + +) + +export default LoginScreen diff --git a/packages/apps/admin/src/components/LoginScreen/StyledComponents.js b/packages/apps/admin/src/components/LoginScreen/StyledComponents.js new file mode 100644 index 0000000000..3066a97aa2 --- /dev/null +++ b/packages/apps/admin/src/components/LoginScreen/StyledComponents.js @@ -0,0 +1,95 @@ +import styled, {createGlobalStyle} from 'styled-components' +import {scale, StyledH1} from 'tocco-ui' + +import ToccoLogo from '../../assets/tocco-circle.svg' + +// Overwrite index.html overflow which is hidden +export const GlobalBodyStyle = createGlobalStyle` + @media (max-width: 1024px) { + body { + overflow: auto !important; + } + } +` + +export const StyledLogin = styled.div` + height: 100vh; + width: 100vw; + background-image: url(${ToccoLogo}); + background-repeat: no-repeat; + background-size: 61vw; + background-position-y: -25vw; + background-position-x: -41vw; + + .tocco-sso-login { + display: flex; + justify-content: space-between; + margin-bottom: 1.8rem; + } + + @media (max-width: 1024px) { + background-size: 2000px; + background-position: 50% -1850px; + } + + @media (max-width: 425px) { + background-position: 50% -1890px; + } +` + +export const StyledHeadingLogin = styled(StyledH1)` + && { + font-size: ${scale.font(11)}; + } +` + +export const StyledLoginWrapper = styled.div` + max-width: 410px; + margin: 6% 5% 0 28%; + + && { + .tocco-login * { + font-size: ${scale.font(1.3)}; + } + + @media (max-width: 1024px) { + margin: 14rem auto 0; + padding-left: 2rem; + padding-right: 2rem; + } + + @media (max-width: 425px) { + margin-top: 12rem; + } + } +` + +export const StyledSloganImg = styled.img` + transform: rotate(270deg); + position: relative; + top: 12.5vw; + left: -8%; + width: 25vw; + + @media (max-width: 1024px) { + display: none; + } +` + +export const StyledMobileSloganImg = styled.img` + display: none; + max-width: 400px; + width: 95%; + height: auto; + position: relative; + margin: auto; + top: 45px; + + @media (max-width: 1024px) { + display: block; + } + + @media (max-width: 425px) { + max-width: 280px; + } +` diff --git a/packages/apps/admin/src/components/LoginScreen/index.js b/packages/apps/admin/src/components/LoginScreen/index.js new file mode 100644 index 0000000000..d1ea2f4827 --- /dev/null +++ b/packages/apps/admin/src/components/LoginScreen/index.js @@ -0,0 +1 @@ +export {default} from './LoginScreen' diff --git a/packages/apps/admin/src/modules/session/actions.js b/packages/apps/admin/src/modules/session/actions.js index d496d402a6..8d365595cd 100644 --- a/packages/apps/admin/src/modules/session/actions.js +++ b/packages/apps/admin/src/modules/session/actions.js @@ -1,3 +1,4 @@ +export const SESSION_HEARTBEAT = 'session/SESSION_HEARTBEAT' export const DO_LOGOUT = 'session/DO_LOGOUT' export const LOGIN_SUCCESSFUL = 'session/LOGIN_SUCCESSFUL' export const LOAD_PRINCIPAL = 'session/LOAD_PRINCIPAL' @@ -8,16 +9,18 @@ export const SET_BUSINESS_UNITS = 'session/SET_BUSINESS_UNITS' export const CHANGE_BUSINESS_UNIT = 'session/CHANGE_BUSINESS_UNIT' export const CHECK_SSO_AVAILABLE = 'session/CHECK_SSO_AVAILABLE' export const SET_SSO_AVAILABLE = 'session/SET_SSO_AVAILABLE' +export const SET_INVALID_SESSION = 'session/SET_INVALID_SESSION' + +export const sessionHeartbeat = () => ({ + type: SESSION_HEARTBEAT +}) export const doLogout = () => ({ type: DO_LOGOUT }) -export const loginSuccessful = sessionTimeout => ({ - type: LOGIN_SUCCESSFUL, - payload: { - sessionTimeout - } +export const loginSuccessful = () => ({ + type: LOGIN_SUCCESSFUL }) export const loadPrincipal = () => ({ @@ -66,3 +69,10 @@ export const setSsoAvailable = ssoAvailable => ({ ssoAvailable } }) + +export const setInvalidSession = invalidSession => ({ + type: SET_INVALID_SESSION, + payload: { + invalidSession + } +}) diff --git a/packages/apps/admin/src/modules/session/reducer.js b/packages/apps/admin/src/modules/session/reducer.js index ac761f1fa7..35c94d8f39 100644 --- a/packages/apps/admin/src/modules/session/reducer.js +++ b/packages/apps/admin/src/modules/session/reducer.js @@ -6,7 +6,8 @@ const ACTION_HANDLERS = { [actions.SET_USERNAME]: reducerUtil.singleTransferReducer('username'), [actions.SET_CURRENT_BUSINESS_UNIT]: reducerUtil.singleTransferReducer('currentBusinessUnit'), [actions.SET_BUSINESS_UNITS]: reducerUtil.singleTransferReducer('businessUnits'), - [actions.SET_SSO_AVAILABLE]: reducerUtil.singleTransferReducer('ssoAvailable') + [actions.SET_SSO_AVAILABLE]: reducerUtil.singleTransferReducer('ssoAvailable'), + [actions.SET_INVALID_SESSION]: reducerUtil.singleTransferReducer('invalidSession') } const initialState = { @@ -16,7 +17,8 @@ const initialState = { id: '' }, businessUnits: [], - ssoAvailable: false + ssoAvailable: false, + invalidSession: false } export default function reducer(state = initialState, action) { diff --git a/packages/apps/admin/src/modules/session/sagas.js b/packages/apps/admin/src/modules/session/sagas.js index b876f21f6f..2bdee62c0c 100644 --- a/packages/apps/admin/src/modules/session/sagas.js +++ b/packages/apps/admin/src/modules/session/sagas.js @@ -5,15 +5,31 @@ import {cache} from 'tocco-util' import * as actions from './actions' +export const HEARTBEAT_INTERVAL_IN_MS = 30 * 1000 + export const sessionSelector = state => state.session +export const loginSelector = state => state.login + +export function* sessionHeartbeat() { + yield call(doSessionRequest) + yield call(delayByTimeout, HEARTBEAT_INTERVAL_IN_MS) + yield call(sessionHeartbeat) +} -export function* sessionHeartbeat(sessionTimeoutInMinutes) { - const sessionHeartbeatTimeoutInMs = (sessionTimeoutInMinutes / 2) * 60 * 1000 +export function* doSessionRequest() { + const {loggedIn} = yield select(loginSelector) const {success, adminAllowed} = yield call(login.doSessionRequest) - yield put(login.setLoggedIn(success)) - yield put(login.setAdminAllowed(adminAllowed)) - yield call(delayByTimeout, sessionHeartbeatTimeoutInMs) - yield call(sessionHeartbeat, sessionTimeoutInMinutes) + if (!success && loggedIn) { + /** + * setLoggedIn and setAdminAllowed should not be called if the session is invalid. + * Otherwise unsaved changes will be lost. + */ + yield put(actions.setInvalidSession(true)) + } else { + yield put(login.setLoggedIn(success)) + yield put(login.setAdminAllowed(adminAllowed)) + yield put(actions.setInvalidSession(false)) + } } /** @@ -29,21 +45,27 @@ export function* doLogoutRequest() { return yield call(login.doRequest, 'logout', {method: 'POST'}) } -export function* loginSuccessful({payload}) { - const {sessionTimeout} = payload +export function* loginSuccessful() { + const {invalidSession} = yield select(sessionSelector) /** - * `adminAllowed` will be set explicitly to true/false inside the sessionHeartbeat. - * Nevertheless it has to be reset toghether with `loggedIn=true`. - * With `adminAllowed=undefined` an empty page is shown instead - * "no roles" error message while fetching the session. + * During relogin via the session invalid modal `adminAllowed=undefined` should not be set. + * Otherwise unsaved changes will be lost. */ - yield put(login.setAdminAllowed(undefined)) + if (!invalidSession) { + /** + * `adminAllowed` will be set explicitly to true/false inside the doSessionRequest. + * Nevertheless it has to be reset toghether with `loggedIn=true`. + * With `adminAllowed=undefined` an empty page is shown instead + * "no roles" error message while fetching the session. + */ + yield put(login.setAdminAllowed(undefined)) + } yield put(login.setLoggedIn(true)) yield put(notification.connectSocket()) - yield call(sessionHeartbeat, sessionTimeout) + yield call(doSessionRequest) } -export function* logout({payload}) { +export function* logout() { yield call(Cookies.remove, 'sso-autologin') yield call(doLogoutRequest) yield put(login.setLoggedIn(false)) @@ -92,6 +114,7 @@ export function* isSsoAvailable() { export default function* mainSagas() { yield all([ + takeLatest(actions.SESSION_HEARTBEAT, sessionHeartbeat), takeLatest(actions.LOGIN_SUCCESSFUL, loginSuccessful), takeLatest(actions.DO_LOGOUT, logout), takeLatest(actions.LOAD_PRINCIPAL, loadPrincipal), diff --git a/packages/apps/admin/src/modules/session/sagas.spec.js b/packages/apps/admin/src/modules/session/sagas.spec.js index 6753899947..0b8a34a824 100644 --- a/packages/apps/admin/src/modules/session/sagas.spec.js +++ b/packages/apps/admin/src/modules/session/sagas.spec.js @@ -1,7 +1,9 @@ import {expectSaga} from 'redux-saga-test-plan' import * as matchers from 'redux-saga-test-plan/matchers' -import {login, rest} from 'tocco-app-extensions' +import {select} from 'redux-saga/effects' +import {login, notification, rest} from 'tocco-app-extensions' +import * as actions from './actions' import * as sagas from './sagas' describe('admin', () => { @@ -59,26 +61,92 @@ describe('admin', () => { }) }) describe('sessionHeartbeat', () => { - test('should set flags and call itself', () => { - const sessionTimeoutInMinutes = 30 + test('should set flags if user is logged in successfully', () => { const sessionResponse = { success: true, adminAllowed: true } - // after the half time of the session timeout - const expectedHeartbeatDelayInMilliseconds = 15 * 60 * 1000 + return expectSaga(sagas.sessionHeartbeat) + .provide([ + [select(sagas.loginSelector), {loggedIn: false}], + [matchers.call.fn(login.doSessionRequest), sessionResponse], + [matchers.call.fn(sagas.sessionHeartbeat)], + [matchers.call.fn(sagas.delayByTimeout)] + ]) + .put(login.setLoggedIn(true)) + .put(login.setAdminAllowed(true)) + .put(actions.setInvalidSession(false)) + .call(sagas.sessionHeartbeat) + .call(sagas.delayByTimeout, sagas.HEARTBEAT_INTERVAL_IN_MS) + .run() + }) - return expectSaga(sagas.sessionHeartbeat, sessionTimeoutInMinutes) + test('should set flags if user is already logged in', () => { + const sessionResponse = { + success: true, + adminAllowed: true + } + + return expectSaga(sagas.sessionHeartbeat) .provide([ + [select(sagas.loginSelector), {loggedIn: true}], [matchers.call.fn(login.doSessionRequest), sessionResponse], [matchers.call.fn(sagas.sessionHeartbeat)], [matchers.call.fn(sagas.delayByTimeout)] ]) .put(login.setLoggedIn(true)) .put(login.setAdminAllowed(true)) - .call(sagas.sessionHeartbeat, sessionTimeoutInMinutes) - .call(sagas.delayByTimeout, expectedHeartbeatDelayInMilliseconds) + .put(actions.setInvalidSession(false)) + .call(sagas.sessionHeartbeat) + .call(sagas.delayByTimeout, sagas.HEARTBEAT_INTERVAL_IN_MS) + .run() + }) + + test('should set invalid session if user is no longer logged in', () => { + const sessionResponse = { + success: false + } + + return expectSaga(sagas.sessionHeartbeat) + .provide([ + [select(sagas.loginSelector), {loggedIn: true}], + [matchers.call.fn(login.doSessionRequest), sessionResponse], + [matchers.call.fn(sagas.sessionHeartbeat)], + [matchers.call.fn(sagas.delayByTimeout)] + ]) + .not.put(login.setLoggedIn(false)) + .put(actions.setInvalidSession(true)) + .call(sagas.sessionHeartbeat) + .call(sagas.delayByTimeout, sagas.HEARTBEAT_INTERVAL_IN_MS) + .run() + }) + }) + + describe('loginSuccessful', () => { + test('login and set admin allowed to undefined', () => { + return expectSaga(sagas.loginSuccessful) + .provide([ + [select(sagas.sessionSelector), {invalidSession: false}], + [matchers.call.fn(sagas.doSessionRequest)] + ]) + .put(login.setAdminAllowed(undefined)) + .put(login.setLoggedIn(true)) + .put(notification.connectSocket()) + .call(sagas.doSessionRequest) + .run() + }) + + test('relogin after invalid session', () => { + return expectSaga(sagas.loginSuccessful) + .provide([ + [select(sagas.sessionSelector), {invalidSession: true}], + [matchers.call.fn(sagas.doSessionRequest)] + ]) + .not.put(login.setAdminAllowed(undefined)) + .put(login.setLoggedIn(true)) + .put(notification.connectSocket()) + .call(sagas.doSessionRequest) .run() }) }) diff --git a/packages/core/app-extensions/src/form/asyncValidation.js b/packages/core/app-extensions/src/form/asyncValidation.js index 16ac8aa8c7..1939c8bf33 100644 --- a/packages/core/app-extensions/src/form/asyncValidation.js +++ b/packages/core/app-extensions/src/form/asyncValidation.js @@ -23,7 +23,7 @@ const validateRequest = (formValues, initialValues, fieldDefinitions, formDefini method: mode === 'create' ? 'POST' : 'PATCH', headers: {'X-Client': 'rest'}, // client type REST does not use client questions, which would interrupt validation body: entity, - acceptedStatusCodes: [403], + acceptedStatusCodes: [400, 403], acceptedErrorCodes: [OUTDATED_ENTITY_ERROR_CODE] } @@ -31,7 +31,7 @@ const validateRequest = (formValues, initialValues, fieldDefinitions, formDefini const endpoint = customEndpoint || `entities/2.0/${entity.model}${entity.key ? `/${entity.key}` : ''}` return rest.simpleRequest(endpoint, options).then(resp => { const body = resp.body - if (resp.status === 403) { + if (resp.status === 400 || resp.status === 403) { return {} } if (body.valid) { diff --git a/packages/core/app-extensions/src/login/actions.js b/packages/core/app-extensions/src/login/actions.js index 66a69bf7c2..68f101c2d6 100644 --- a/packages/core/app-extensions/src/login/actions.js +++ b/packages/core/app-extensions/src/login/actions.js @@ -1,6 +1,5 @@ export const SET_LOGGED_IN = 'login/SET_LOGGED_IN' export const SET_ADMIN_ALLOWED = 'login/SET_ADMIN_ALLOWED' -export const DO_SESSION_CHECK = 'login/DO_SESSION_CHECK' export const setLoggedIn = loggedIn => ({ type: SET_LOGGED_IN, @@ -15,7 +14,3 @@ export const setAdminAllowed = adminAllowed => ({ adminAllowed } }) - -export const doSessionCheck = () => ({ - type: DO_SESSION_CHECK -}) diff --git a/packages/core/app-extensions/src/login/index.js b/packages/core/app-extensions/src/login/index.js index 283a90514a..f1001400ee 100644 --- a/packages/core/app-extensions/src/login/index.js +++ b/packages/core/app-extensions/src/login/index.js @@ -1,4 +1,4 @@ -import {doSessionCheck, setAdminAllowed, setLoggedIn} from './actions' +import {setAdminAllowed, setLoggedIn} from './actions' import {addToStore} from './login' import {doRequest, doSessionRequest} from './sagas' @@ -6,7 +6,6 @@ export default { addToStore, setLoggedIn, setAdminAllowed, - doSessionCheck, doSessionRequest, doRequest } diff --git a/packages/core/app-extensions/src/login/login.js b/packages/core/app-extensions/src/login/login.js index c0541e5ed3..066dc1a8c0 100644 --- a/packages/core/app-extensions/src/login/login.js +++ b/packages/core/app-extensions/src/login/login.js @@ -1,12 +1,9 @@ import {reducer as reducerUtil} from 'tocco-util' import reducer from './reducer' -import sagas from './sagas' export const addToStore = store => { reducerUtil.injectReducers(store, { login: reducer }) - - store.sagaMiddleware.run(sagas) } diff --git a/packages/core/app-extensions/src/login/sagas.js b/packages/core/app-extensions/src/login/sagas.js index e403fa073f..90feb4915c 100644 --- a/packages/core/app-extensions/src/login/sagas.js +++ b/packages/core/app-extensions/src/login/sagas.js @@ -1,8 +1,5 @@ -import {all, call, put, takeLatest} from 'redux-saga/effects' -import {consoleLogger, request, cache} from 'tocco-util' - -import notification from '../notification' -import * as actions from './actions' +import {call} from 'redux-saga/effects' +import {consoleLogger, request} from 'tocco-util' export function doRequest(url, options) { return request @@ -17,18 +14,3 @@ export function doRequest(url, options) { export function* doSessionRequest() { return yield call(doRequest, 'session', {method: 'POST'}) } - -export function* sessionCheck() { - const {success, businessUnit, adminAllowed} = yield call(doSessionRequest) - const cachedPrincipal = cache.getLongTerm('session', 'principal') - if (cachedPrincipal && cachedPrincipal.currentBusinessUnit.id !== businessUnit) { - yield call(cache.clearAll) - } - yield put(actions.setAdminAllowed(adminAllowed)) - yield put(actions.setLoggedIn(success)) - yield put(notification.connectSocket()) -} - -export default function* mainSagas() { - yield all([takeLatest(actions.DO_SESSION_CHECK, sessionCheck)]) -} diff --git a/packages/core/app-extensions/src/login/sagas.spec.js b/packages/core/app-extensions/src/login/sagas.spec.js deleted file mode 100644 index 1673f247a6..0000000000 --- a/packages/core/app-extensions/src/login/sagas.spec.js +++ /dev/null @@ -1,86 +0,0 @@ -import {expectSaga, testSaga} from 'redux-saga-test-plan' -import * as matchers from 'redux-saga-test-plan/matchers' -import {takeLatest} from 'redux-saga/effects' -import {cache} from 'tocco-util' - -import notification from '../notification' -import * as actions from './actions' -import rootSaga, * as sagas from './sagas' - -describe('app-extensions', () => { - describe('login', () => { - describe('sagas', () => { - describe('root saga', () => { - test('should fork sagas', () => { - const saga = testSaga(rootSaga) - saga.next().all([takeLatest(actions.DO_SESSION_CHECK, sagas.sessionCheck)]) - }) - }) - describe('sessionCheck', () => { - test('should clear cache on different bu', () => { - cache.clearAll() - - const sessionResponse = { - businessUnit: 'm2' - } - cache.addLongTerm('session', 'principal', { - currentBusinessUnit: { - id: 'm1' - } - }) - return expectSaga(sagas.sessionCheck) - .provide([[matchers.call(sagas.doSessionRequest), sessionResponse]]) - .call(cache.clearAll) - .run() - }) - test('should not clear cache on same bu', () => { - cache.clearAll() - - const sessionResponse = { - businessUnit: 'm1' - } - cache.addLongTerm('session', 'principal', { - currentBusinessUnit: { - id: 'm1' - } - }) - return expectSaga(sagas.sessionCheck) - .provide([[matchers.call(sagas.doSessionRequest), sessionResponse]]) - .not.call(cache.clearAll) - .run() - }) - test('should set logged in', () => { - const sessionResponse = { - success: false - } - return expectSaga(sagas.sessionCheck) - .provide([[matchers.call(sagas.doSessionRequest), sessionResponse]]) - .put(actions.setLoggedIn(false)) - .run() - }) - - const testSocketConnection = loginSuccess => { - const sessionResponse = { - success: loginSuccess - } - return expectSaga(sagas.sessionCheck) - .provide([[matchers.call(sagas.doSessionRequest), sessionResponse]]) - .put(notification.connectSocket()) - .run() - } - test('should connect socket on failure', () => testSocketConnection(false)) - test('should connect socket on success', () => testSocketConnection(true)) - - test('should set adminAllowed in', () => { - const sessionResponse = { - adminAllowed: true - } - return expectSaga(sagas.sessionCheck) - .provide([[matchers.call(sagas.doSessionRequest), sessionResponse]]) - .put(actions.setAdminAllowed(true)) - .run() - }) - }) - }) - }) -}) diff --git a/packages/core/app-extensions/src/notification/index.js b/packages/core/app-extensions/src/notification/index.js index 3781646d7d..6c8f699404 100644 --- a/packages/core/app-extensions/src/notification/index.js +++ b/packages/core/app-extensions/src/notification/index.js @@ -3,6 +3,8 @@ import {blockingInfo, removeBlockingInfo} from './modules/blocking/actions' import NotificationCenter from './modules/center/NotificationCenter' import {yesNoQuestion, confirm} from './modules/interactive/actions' import {modal, removeModal} from './modules/modal/actions' +import ModalContent from './modules/modal/ModalDisplay/ModalContent' +import {StyledModalHolder, StyledPageOverlay} from './modules/modal/ModalDisplay/StyledComponents' import {connectSocket, closeSocket} from './modules/socket/actions' import {toaster, removeToaster} from './modules/toaster/actions' import {addToStore} from './notification' @@ -20,5 +22,8 @@ export default { toaster, removeToaster, connectSocket, - closeSocket + closeSocket, + ModalContent, + StyledModalHolder, + StyledPageOverlay } diff --git a/packages/widgets/entity-browser/src/modules/entityBrowser/sagas.js b/packages/widgets/entity-browser/src/modules/entityBrowser/sagas.js index 5fecd493db..39c7f69925 100644 --- a/packages/widgets/entity-browser/src/modules/entityBrowser/sagas.js +++ b/packages/widgets/entity-browser/src/modules/entityBrowser/sagas.js @@ -1,11 +1,18 @@ import {all, call, put, take} from 'redux-saga/effects' -import {appFactory, login} from 'tocco-app-extensions' +import {appFactory, cache as cacheHelpers, notification} from 'tocco-app-extensions' +import {cache} from 'tocco-util' export default function* sagas() { - yield all([call(connectSocket)]) + yield all([call(initialize)]) } -export function* connectSocket() { +export function* initialize() { yield take(appFactory.INPUT_INITIALIZED) - yield put(login.doSessionCheck()) + + const needsCacheInvalidation = yield call(cacheHelpers.hasInvalidCache) + + if (needsCacheInvalidation) { + yield call(cache.clearAll) + } + yield put(notification.connectSocket()) } diff --git a/packages/widgets/entity-browser/src/modules/entityBrowser/sagas.spec.js b/packages/widgets/entity-browser/src/modules/entityBrowser/sagas.spec.js new file mode 100644 index 0000000000..450a847b88 --- /dev/null +++ b/packages/widgets/entity-browser/src/modules/entityBrowser/sagas.spec.js @@ -0,0 +1,38 @@ +import {expectSaga, testSaga} from 'redux-saga-test-plan' +import * as matchers from 'redux-saga-test-plan/matchers' +import {call} from 'redux-saga/effects' +import {appFactory, cache as cacheHelpers, notification} from 'tocco-app-extensions' +import {cache} from 'tocco-util' + +import rootSaga, * as sagas from './sagas' + +describe('widgets', () => { + describe('modules', () => { + describe('entity-browser', () => { + describe('entityBrowser', () => { + describe('sagas', () => { + describe('root saga', () => { + test('should fork sagas', () => { + const saga = testSaga(rootSaga) + saga.next().all([call(sagas.initialize)]) + }) + }) + + describe('initialize', () => { + test('initialize', () => { + return expectSaga(sagas.initialize) + .provide([ + [matchers.take(appFactory.INPUT_INITIALIZED), {}], + [matchers.call(cacheHelpers.hasInvalidCache), true] + ]) + .call(cacheHelpers.hasInvalidCache) + .call(cache.clearAll) + .put(notification.connectSocket()) + .run() + }) + }) + }) + }) + }) + }) +})