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
-
+
)
@@ -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()
+ })
+ })
+ })
+ })
+ })
+ })
+})