From 8ae69e369513ff88963e99692cf21f23d0718150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=BCrsten?= Date: Fri, 4 Nov 2022 14:43:57 +0100 Subject: [PATCH 1/7] feat(admin): make heartbeat interval independent of session timeout Changelog: make heartbeat interval independent of session timeout Refs: TOCDEV-5313 --- .../apps/admin/src/components/Login/Login.js | 8 ++------ .../apps/admin/src/modules/session/actions.js | 7 ++----- packages/apps/admin/src/modules/session/sagas.js | 16 ++++++++-------- .../apps/admin/src/modules/session/sagas.spec.js | 10 +++------- 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/apps/admin/src/components/Login/Login.js b/packages/apps/admin/src/components/Login/Login.js index 1fba28ca48..8e7cd5e0d4 100644 --- a/packages/apps/admin/src/components/Login/Login.js +++ b/packages/apps/admin/src/components/Login/Login.js @@ -27,14 +27,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 { @@ -72,7 +68,7 @@ const Login = ({ssoAvailable, loginSuccessful, checkSsoAvailable}) => { {ssoAvailable && } - + diff --git a/packages/apps/admin/src/modules/session/actions.js b/packages/apps/admin/src/modules/session/actions.js index d496d402a6..f1f7afba80 100644 --- a/packages/apps/admin/src/modules/session/actions.js +++ b/packages/apps/admin/src/modules/session/actions.js @@ -13,11 +13,8 @@ export const doLogout = () => ({ type: DO_LOGOUT }) -export const loginSuccessful = sessionTimeout => ({ - type: LOGIN_SUCCESSFUL, - payload: { - sessionTimeout - } +export const loginSuccessful = () => ({ + type: LOGIN_SUCCESSFUL }) export const loadPrincipal = () => ({ diff --git a/packages/apps/admin/src/modules/session/sagas.js b/packages/apps/admin/src/modules/session/sagas.js index b876f21f6f..ce52c340d1 100644 --- a/packages/apps/admin/src/modules/session/sagas.js +++ b/packages/apps/admin/src/modules/session/sagas.js @@ -5,15 +5,16 @@ 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 function* sessionHeartbeat(sessionTimeoutInMinutes) { - const sessionHeartbeatTimeoutInMs = (sessionTimeoutInMinutes / 2) * 60 * 1000 +export function* sessionHeartbeat() { 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) + yield call(delayByTimeout, HEARTBEAT_INTERVAL_IN_MS) + yield call(sessionHeartbeat) } /** @@ -29,8 +30,7 @@ export function* doLogoutRequest() { return yield call(login.doRequest, 'logout', {method: 'POST'}) } -export function* loginSuccessful({payload}) { - const {sessionTimeout} = payload +export function* loginSuccessful() { /** * `adminAllowed` will be set explicitly to true/false inside the sessionHeartbeat. * Nevertheless it has to be reset toghether with `loggedIn=true`. @@ -40,10 +40,10 @@ export function* loginSuccessful({payload}) { yield put(login.setAdminAllowed(undefined)) yield put(login.setLoggedIn(true)) yield put(notification.connectSocket()) - yield call(sessionHeartbeat, sessionTimeout) + yield call(sessionHeartbeat) } -export function* logout({payload}) { +export function* logout() { yield call(Cookies.remove, 'sso-autologin') yield call(doLogoutRequest) yield put(login.setLoggedIn(false)) diff --git a/packages/apps/admin/src/modules/session/sagas.spec.js b/packages/apps/admin/src/modules/session/sagas.spec.js index 6753899947..6a6f6fac2d 100644 --- a/packages/apps/admin/src/modules/session/sagas.spec.js +++ b/packages/apps/admin/src/modules/session/sagas.spec.js @@ -60,16 +60,12 @@ describe('admin', () => { }) describe('sessionHeartbeat', () => { test('should set flags and call itself', () => { - const sessionTimeoutInMinutes = 30 const sessionResponse = { success: true, adminAllowed: true } - // after the half time of the session timeout - const expectedHeartbeatDelayInMilliseconds = 15 * 60 * 1000 - - return expectSaga(sagas.sessionHeartbeat, sessionTimeoutInMinutes) + return expectSaga(sagas.sessionHeartbeat) .provide([ [matchers.call.fn(login.doSessionRequest), sessionResponse], [matchers.call.fn(sagas.sessionHeartbeat)], @@ -77,8 +73,8 @@ describe('admin', () => { ]) .put(login.setLoggedIn(true)) .put(login.setAdminAllowed(true)) - .call(sagas.sessionHeartbeat, sessionTimeoutInMinutes) - .call(sagas.delayByTimeout, expectedHeartbeatDelayInMilliseconds) + .call(sagas.sessionHeartbeat) + .call(sagas.delayByTimeout, sagas.HEARTBEAT_INTERVAL_IN_MS) .run() }) }) From 4c480fd7c636442649046340894936f79cfa2066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=BCrsten?= Date: Mon, 7 Nov 2022 07:59:21 +0100 Subject: [PATCH 2/7] refactor(app-externsions): refactor heartbeat and session check - only start heartbeat once and not after each login (which could lead to multiple heartbeats) - move sessionCheck to entity-browser as in the admin setAdminAllowed and setLoggedIn are already handled and the notification socket is already opened. loggedIn is not reimplemented in the widgets as it is currently not used Refs: TOCDEV-5313 --- .../src/components/LoginGuard/LoginGuard.js | 10 ++- .../LoginGuard/LoginGuardContainer.js | 6 +- .../apps/admin/src/modules/session/actions.js | 5 ++ .../apps/admin/src/modules/session/sagas.js | 13 ++- .../core/app-extensions/src/login/actions.js | 5 -- .../core/app-extensions/src/login/index.js | 3 +- .../core/app-extensions/src/login/login.js | 3 - .../core/app-extensions/src/login/sagas.js | 22 +---- .../app-extensions/src/login/sagas.spec.js | 86 ------------------- .../src/modules/entityBrowser/sagas.js | 15 +++- .../src/modules/entityBrowser/sagas.spec.js | 38 ++++++++ 11 files changed, 76 insertions(+), 130 deletions(-) delete mode 100644 packages/core/app-extensions/src/login/sagas.spec.js create mode 100644 packages/widgets/entity-browser/src/modules/entityBrowser/sagas.spec.js diff --git a/packages/apps/admin/src/components/LoginGuard/LoginGuard.js b/packages/apps/admin/src/components/LoginGuard/LoginGuard.js index ccfe1551d7..05b24cd5af 100644 --- a/packages/apps/admin/src/components/LoginGuard/LoginGuard.js +++ b/packages/apps/admin/src/components/LoginGuard/LoginGuard.js @@ -6,10 +6,11 @@ import {LoadMask} from 'tocco-ui' import Login from '../../components/Login' import Admin from '../Admin' -const LoginGuard = ({doSessionCheck, loggedIn}) => { +const LoginGuard = ({connectSocket, sessionHeartbeat, loggedIn}) => { useEffect(() => { - doSessionCheck() - }, [doSessionCheck]) + connectSocket() + sessionHeartbeat() + }, [connectSocket, sessionHeartbeat]) return (
@@ -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/modules/session/actions.js b/packages/apps/admin/src/modules/session/actions.js index f1f7afba80..a1364d5f14 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' @@ -9,6 +10,10 @@ 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 sessionHeartbeat = () => ({ + type: SESSION_HEARTBEAT +}) + export const doLogout = () => ({ type: DO_LOGOUT }) diff --git a/packages/apps/admin/src/modules/session/sagas.js b/packages/apps/admin/src/modules/session/sagas.js index ce52c340d1..7c50a2230c 100644 --- a/packages/apps/admin/src/modules/session/sagas.js +++ b/packages/apps/admin/src/modules/session/sagas.js @@ -10,11 +10,15 @@ export const HEARTBEAT_INTERVAL_IN_MS = 30 * 1000 export const sessionSelector = state => state.session export function* sessionHeartbeat() { + yield call(doSessionRequest) + yield call(delayByTimeout, HEARTBEAT_INTERVAL_IN_MS) + yield call(sessionHeartbeat) +} + +function* doSessionRequest() { const {success, adminAllowed} = yield call(login.doSessionRequest) yield put(login.setLoggedIn(success)) yield put(login.setAdminAllowed(adminAllowed)) - yield call(delayByTimeout, HEARTBEAT_INTERVAL_IN_MS) - yield call(sessionHeartbeat) } /** @@ -32,7 +36,7 @@ export function* doLogoutRequest() { export function* loginSuccessful() { /** - * `adminAllowed` will be set explicitly to true/false inside the sessionHeartbeat. + * `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. @@ -40,7 +44,7 @@ export function* loginSuccessful() { yield put(login.setAdminAllowed(undefined)) yield put(login.setLoggedIn(true)) yield put(notification.connectSocket()) - yield call(sessionHeartbeat) + yield call(doSessionRequest) } export function* logout() { @@ -92,6 +96,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/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/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() + }) + }) + }) + }) + }) + }) +}) From dd89e66e3c2d18ef3399c859252899c712652bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=BCrsten?= Date: Mon, 7 Nov 2022 12:44:16 +0100 Subject: [PATCH 3/7] refactor(admin): split login in two components Refs: TOCDEV-5313 --- .../apps/admin/src/components/Login/Login.js | 27 +----- .../src/components/Login/StyledComponents.js | 96 +------------------ .../src/components/LoginGuard/LoginGuard.js | 4 +- .../src/components/LoginScreen/LoginScreen.js | 30 ++++++ .../LoginScreen/StyledComponents.js | 95 ++++++++++++++++++ .../admin/src/components/LoginScreen/index.js | 1 + 6 files changed, 133 insertions(+), 120 deletions(-) create mode 100644 packages/apps/admin/src/components/LoginScreen/LoginScreen.js create mode 100644 packages/apps/admin/src/components/LoginScreen/StyledComponents.js create mode 100644 packages/apps/admin/src/components/LoginScreen/index.js diff --git a/packages/apps/admin/src/components/Login/Login.js b/packages/apps/admin/src/components/Login/Login.js index 8e7cd5e0d4..6a9bc56fad 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) @@ -59,18 +48,8 @@ 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 05b24cd5af..3a7410aac8 100644 --- a/packages/apps/admin/src/components/LoginGuard/LoginGuard.js +++ b/packages/apps/admin/src/components/LoginGuard/LoginGuard.js @@ -3,7 +3,7 @@ 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 = ({connectSocket, sessionHeartbeat, loggedIn}) => { @@ -18,7 +18,7 @@ const LoginGuard = ({connectSocket, sessionHeartbeat, loggedIn}) => { Tocco -
{!loggedIn ? : }
+
{!loggedIn ? : }
) 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' From c947e612bcb467590d9d91a0bc4e2e7339346b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=BCrsten?= Date: Tue, 8 Nov 2022 08:22:06 +0100 Subject: [PATCH 4/7] feat(app-extensions): allow 400 status code in async validation if a field is focused and the invalid session modal is shown the focus is changed to the modal. This triggers the async validation. The server returns a 400 status code as the user is no longer logged in. This error should be ignored Changelog: allow 400 status code in async validation Refs: TOCDEV-5313 --- packages/core/app-extensions/src/form/asyncValidation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) { From 448a06be3329b0fbdcc5812686270e1df0040c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=BCrsten?= Date: Tue, 8 Nov 2022 09:13:09 +0100 Subject: [PATCH 5/7] fix(login): fix layout of login in modal the LoadMask (loading the login and sso-login app) has a height of 100%. Without the div both load masks have the height of the modal itself. Layout in normal login screen is not changed Changelog: fix layout of login in modal Refs: TOCDEV-5313 --- packages/apps/admin/src/components/Login/Login.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/apps/admin/src/components/Login/Login.js b/packages/apps/admin/src/components/Login/Login.js index 6a9bc56fad..58a457feb1 100644 --- a/packages/apps/admin/src/components/Login/Login.js +++ b/packages/apps/admin/src/components/Login/Login.js @@ -28,7 +28,7 @@ const Login = ({ssoAvailable, loginSuccessful, checkSsoAvailable}) => { } const SsoLoginPart = () => ( - <> +
{showRegistrationText && ( @@ -43,13 +43,15 @@ const Login = ({ssoAvailable, loginSuccessful, checkSsoAvailable}) => { - +
) return ( <> {ssoAvailable && } - +
+ +
) } From 2c4e49032cf89d93f3d122e7f9e45297019e5a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=BCrsten?= Date: Tue, 8 Nov 2022 09:30:34 +0100 Subject: [PATCH 6/7] feat(admin): add invalid session modal the session invalid modal should have a higher z-index than all other modals and the toasters. so we cannot just reuse the functionality Changelog: add invalid session modal Refs: TOCDEV-5313 --- .../apps/admin/src/components/Admin/Admin.js | 26 ++++--- .../InvalidSession/InvalidSession.js | 52 +++++++++++++ .../InvalidSession/InvalidSessionContainer.js | 12 +++ .../src/components/InvalidSession/index.js | 1 + .../apps/admin/src/modules/session/actions.js | 8 ++ .../apps/admin/src/modules/session/reducer.js | 6 +- .../apps/admin/src/modules/session/sagas.js | 34 +++++++-- .../admin/src/modules/session/sagas.spec.js | 76 ++++++++++++++++++- .../app-extensions/src/notification/index.js | 7 +- 9 files changed, 199 insertions(+), 23 deletions(-) create mode 100644 packages/apps/admin/src/components/InvalidSession/InvalidSession.js create mode 100644 packages/apps/admin/src/components/InvalidSession/InvalidSessionContainer.js create mode 100644 packages/apps/admin/src/components/InvalidSession/index.js diff --git a/packages/apps/admin/src/components/Admin/Admin.js b/packages/apps/admin/src/components/Admin/Admin.js index 16f7dc3afa..71ba2be4f3 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} + + + + ) } 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/modules/session/actions.js b/packages/apps/admin/src/modules/session/actions.js index a1364d5f14..8d365595cd 100644 --- a/packages/apps/admin/src/modules/session/actions.js +++ b/packages/apps/admin/src/modules/session/actions.js @@ -9,6 +9,7 @@ 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 @@ -68,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 7c50a2230c..2bdee62c0c 100644 --- a/packages/apps/admin/src/modules/session/sagas.js +++ b/packages/apps/admin/src/modules/session/sagas.js @@ -8,6 +8,7 @@ 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) @@ -15,10 +16,20 @@ export function* sessionHeartbeat() { yield call(sessionHeartbeat) } -function* doSessionRequest() { +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)) + 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)) + } } /** @@ -35,13 +46,20 @@ export function* doLogoutRequest() { } export function* loginSuccessful() { + const {invalidSession} = yield select(sessionSelector) /** - * `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. + * 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(doSessionRequest) diff --git a/packages/apps/admin/src/modules/session/sagas.spec.js b/packages/apps/admin/src/modules/session/sagas.spec.js index 6a6f6fac2d..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,7 +61,7 @@ describe('admin', () => { }) }) describe('sessionHeartbeat', () => { - test('should set flags and call itself', () => { + test('should set flags if user is logged in successfully', () => { const sessionResponse = { success: true, adminAllowed: true @@ -67,16 +69,86 @@ describe('admin', () => { 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() }) + + 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)) + .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/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 } From 01a82a654979c5da7b670d2f837537bf14520560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=BCrsten?= Date: Wed, 9 Nov 2022 14:32:34 +0100 Subject: [PATCH 7/7] fix(admin): adminAllowed is not a required prop during the login the adminAllowed prop is set to undefined Refs: TOCDEV-5313 --- packages/apps/admin/src/components/Admin/Admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/admin/src/components/Admin/Admin.js b/packages/apps/admin/src/components/Admin/Admin.js index 71ba2be4f3..5a0274adaf 100644 --- a/packages/apps/admin/src/components/Admin/Admin.js +++ b/packages/apps/admin/src/components/Admin/Admin.js @@ -134,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)