Skip to content
This repository was archived by the owner on Dec 28, 2022. It is now read-only.

Commit 2c4e490

Browse files
committed
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
1 parent 448a06b commit 2c4e490

9 files changed

Lines changed: 199 additions & 23 deletions

File tree

packages/apps/admin/src/components/Admin/Admin.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import EntitiesRoute from '../../routes/entities'
1414
import Settings from '../../routes/settings'
1515
import ErrorView from '../ErrorView'
1616
import Header from '../Header'
17+
import InvalidSession from '../InvalidSession'
1718
import Navigation from '../Navigation'
1819
import navigationStrategy from './../../routes/entities/utils/navigationStrategy'
1920
import {burgerMenuStyles, StyledContent, StyledMenu, StyledWrapper} from './StyledComponents'
@@ -105,16 +106,21 @@ const Admin = ({
105106
)
106107

107108
return (
108-
<LoadMask required={[history !== null]}>
109-
<Router history={history || {}}>
110-
<GlobalStyles />
111-
<notification.Notifications navigationStrategy={navigationStrategy()} />
112-
<StyledWrapper width={useWindowWidth()}>
113-
<Header />
114-
{adminAllowedContent || adminForbiddenContent}
115-
</StyledWrapper>
116-
</Router>
117-
</LoadMask>
109+
<>
110+
<errorLogging.ErrorBoundary>
111+
<InvalidSession />
112+
</errorLogging.ErrorBoundary>
113+
<LoadMask required={[history !== null]}>
114+
<Router history={history || {}}>
115+
<GlobalStyles />
116+
<notification.Notifications navigationStrategy={navigationStrategy()} />
117+
<StyledWrapper width={useWindowWidth()}>
118+
<Header />
119+
{adminAllowedContent || adminForbiddenContent}
120+
</StyledWrapper>
121+
</Router>
122+
</LoadMask>
123+
</>
118124
)
119125
}
120126

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import PropTypes from 'prop-types'
2+
import ReactDOM from 'react-dom'
3+
import {FormattedMessage} from 'react-intl'
4+
import styled from 'styled-components'
5+
import {notification} from 'tocco-app-extensions'
6+
import {Typography} from 'tocco-ui'
7+
8+
import Login from '../Login'
9+
10+
const StyledLoginHolder = styled(notification.StyledModalHolder)`
11+
// higher than StyledModalHolder
12+
// higher than StyledToasterBox
13+
z-index: 100000;
14+
`
15+
16+
const InvalidSession = ({invalidSession, intl}) => {
17+
const msg = id => intl.formatMessage({id})
18+
19+
const Content = () => (
20+
<>
21+
<Typography.P>
22+
<div dangerouslySetInnerHTML={{__html: msg('client.admin.invalidSession.description')}} />
23+
</Typography.P>
24+
<Login />
25+
</>
26+
)
27+
28+
if (!invalidSession) {
29+
return null
30+
}
31+
32+
return ReactDOM.createPortal(
33+
<StyledLoginHolder>
34+
<notification.ModalContent
35+
id="invalid-session"
36+
title={<FormattedMessage id="client.admin.invalidSession.title" />}
37+
component={() => <Content />}
38+
onClose={() => {}}
39+
onCancel={() => {}}
40+
/>
41+
<notification.StyledPageOverlay />
42+
</StyledLoginHolder>,
43+
document.body
44+
)
45+
}
46+
47+
InvalidSession.propTypes = {
48+
intl: PropTypes.object.isRequired,
49+
invalidSession: PropTypes.bool
50+
}
51+
52+
export default InvalidSession
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {injectIntl} from 'react-intl'
2+
import {connect} from 'react-redux'
3+
4+
import InvalidSession from './InvalidSession'
5+
6+
const mapActionCreators = {}
7+
8+
const mapStateToProps = (state, props) => ({
9+
invalidSession: state.session.invalidSession
10+
})
11+
12+
export default connect(mapStateToProps, mapActionCreators)(injectIntl(InvalidSession))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {default} from './InvalidSessionContainer'

packages/apps/admin/src/modules/session/actions.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const SET_BUSINESS_UNITS = 'session/SET_BUSINESS_UNITS'
99
export const CHANGE_BUSINESS_UNIT = 'session/CHANGE_BUSINESS_UNIT'
1010
export const CHECK_SSO_AVAILABLE = 'session/CHECK_SSO_AVAILABLE'
1111
export const SET_SSO_AVAILABLE = 'session/SET_SSO_AVAILABLE'
12+
export const SET_INVALID_SESSION = 'session/SET_INVALID_SESSION'
1213

1314
export const sessionHeartbeat = () => ({
1415
type: SESSION_HEARTBEAT
@@ -68,3 +69,10 @@ export const setSsoAvailable = ssoAvailable => ({
6869
ssoAvailable
6970
}
7071
})
72+
73+
export const setInvalidSession = invalidSession => ({
74+
type: SET_INVALID_SESSION,
75+
payload: {
76+
invalidSession
77+
}
78+
})

packages/apps/admin/src/modules/session/reducer.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ const ACTION_HANDLERS = {
66
[actions.SET_USERNAME]: reducerUtil.singleTransferReducer('username'),
77
[actions.SET_CURRENT_BUSINESS_UNIT]: reducerUtil.singleTransferReducer('currentBusinessUnit'),
88
[actions.SET_BUSINESS_UNITS]: reducerUtil.singleTransferReducer('businessUnits'),
9-
[actions.SET_SSO_AVAILABLE]: reducerUtil.singleTransferReducer('ssoAvailable')
9+
[actions.SET_SSO_AVAILABLE]: reducerUtil.singleTransferReducer('ssoAvailable'),
10+
[actions.SET_INVALID_SESSION]: reducerUtil.singleTransferReducer('invalidSession')
1011
}
1112

1213
const initialState = {
@@ -16,7 +17,8 @@ const initialState = {
1617
id: ''
1718
},
1819
businessUnits: [],
19-
ssoAvailable: false
20+
ssoAvailable: false,
21+
invalidSession: false
2022
}
2123

2224
export default function reducer(state = initialState, action) {

packages/apps/admin/src/modules/session/sagas.js

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,28 @@ import * as actions from './actions'
88
export const HEARTBEAT_INTERVAL_IN_MS = 30 * 1000
99

1010
export const sessionSelector = state => state.session
11+
export const loginSelector = state => state.login
1112

1213
export function* sessionHeartbeat() {
1314
yield call(doSessionRequest)
1415
yield call(delayByTimeout, HEARTBEAT_INTERVAL_IN_MS)
1516
yield call(sessionHeartbeat)
1617
}
1718

18-
function* doSessionRequest() {
19+
export function* doSessionRequest() {
20+
const {loggedIn} = yield select(loginSelector)
1921
const {success, adminAllowed} = yield call(login.doSessionRequest)
20-
yield put(login.setLoggedIn(success))
21-
yield put(login.setAdminAllowed(adminAllowed))
22+
if (!success && loggedIn) {
23+
/**
24+
* setLoggedIn and setAdminAllowed should not be called if the session is invalid.
25+
* Otherwise unsaved changes will be lost.
26+
*/
27+
yield put(actions.setInvalidSession(true))
28+
} else {
29+
yield put(login.setLoggedIn(success))
30+
yield put(login.setAdminAllowed(adminAllowed))
31+
yield put(actions.setInvalidSession(false))
32+
}
2233
}
2334

2435
/**
@@ -35,13 +46,20 @@ export function* doLogoutRequest() {
3546
}
3647

3748
export function* loginSuccessful() {
49+
const {invalidSession} = yield select(sessionSelector)
3850
/**
39-
* `adminAllowed` will be set explicitly to true/false inside the doSessionRequest.
40-
* Nevertheless it has to be reset toghether with `loggedIn=true`.
41-
* With `adminAllowed=undefined` an empty page is shown instead
42-
* "no roles" error message while fetching the session.
51+
* During relogin via the session invalid modal `adminAllowed=undefined` should not be set.
52+
* Otherwise unsaved changes will be lost.
4353
*/
44-
yield put(login.setAdminAllowed(undefined))
54+
if (!invalidSession) {
55+
/**
56+
* `adminAllowed` will be set explicitly to true/false inside the doSessionRequest.
57+
* Nevertheless it has to be reset toghether with `loggedIn=true`.
58+
* With `adminAllowed=undefined` an empty page is shown instead
59+
* "no roles" error message while fetching the session.
60+
*/
61+
yield put(login.setAdminAllowed(undefined))
62+
}
4563
yield put(login.setLoggedIn(true))
4664
yield put(notification.connectSocket())
4765
yield call(doSessionRequest)

packages/apps/admin/src/modules/session/sagas.spec.js

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {expectSaga} from 'redux-saga-test-plan'
22
import * as matchers from 'redux-saga-test-plan/matchers'
3-
import {login, rest} from 'tocco-app-extensions'
3+
import {select} from 'redux-saga/effects'
4+
import {login, notification, rest} from 'tocco-app-extensions'
45

6+
import * as actions from './actions'
57
import * as sagas from './sagas'
68

79
describe('admin', () => {
@@ -59,24 +61,94 @@ describe('admin', () => {
5961
})
6062
})
6163
describe('sessionHeartbeat', () => {
62-
test('should set flags and call itself', () => {
64+
test('should set flags if user is logged in successfully', () => {
6365
const sessionResponse = {
6466
success: true,
6567
adminAllowed: true
6668
}
6769

6870
return expectSaga(sagas.sessionHeartbeat)
6971
.provide([
72+
[select(sagas.loginSelector), {loggedIn: false}],
7073
[matchers.call.fn(login.doSessionRequest), sessionResponse],
7174
[matchers.call.fn(sagas.sessionHeartbeat)],
7275
[matchers.call.fn(sagas.delayByTimeout)]
7376
])
7477
.put(login.setLoggedIn(true))
7578
.put(login.setAdminAllowed(true))
79+
.put(actions.setInvalidSession(false))
7680
.call(sagas.sessionHeartbeat)
7781
.call(sagas.delayByTimeout, sagas.HEARTBEAT_INTERVAL_IN_MS)
7882
.run()
7983
})
84+
85+
test('should set flags if user is already logged in', () => {
86+
const sessionResponse = {
87+
success: true,
88+
adminAllowed: true
89+
}
90+
91+
return expectSaga(sagas.sessionHeartbeat)
92+
.provide([
93+
[select(sagas.loginSelector), {loggedIn: true}],
94+
[matchers.call.fn(login.doSessionRequest), sessionResponse],
95+
[matchers.call.fn(sagas.sessionHeartbeat)],
96+
[matchers.call.fn(sagas.delayByTimeout)]
97+
])
98+
.put(login.setLoggedIn(true))
99+
.put(login.setAdminAllowed(true))
100+
.put(actions.setInvalidSession(false))
101+
.call(sagas.sessionHeartbeat)
102+
.call(sagas.delayByTimeout, sagas.HEARTBEAT_INTERVAL_IN_MS)
103+
.run()
104+
})
105+
106+
test('should set invalid session if user is no longer logged in', () => {
107+
const sessionResponse = {
108+
success: false
109+
}
110+
111+
return expectSaga(sagas.sessionHeartbeat)
112+
.provide([
113+
[select(sagas.loginSelector), {loggedIn: true}],
114+
[matchers.call.fn(login.doSessionRequest), sessionResponse],
115+
[matchers.call.fn(sagas.sessionHeartbeat)],
116+
[matchers.call.fn(sagas.delayByTimeout)]
117+
])
118+
.not.put(login.setLoggedIn(false))
119+
.put(actions.setInvalidSession(true))
120+
.call(sagas.sessionHeartbeat)
121+
.call(sagas.delayByTimeout, sagas.HEARTBEAT_INTERVAL_IN_MS)
122+
.run()
123+
})
124+
})
125+
126+
describe('loginSuccessful', () => {
127+
test('login and set admin allowed to undefined', () => {
128+
return expectSaga(sagas.loginSuccessful)
129+
.provide([
130+
[select(sagas.sessionSelector), {invalidSession: false}],
131+
[matchers.call.fn(sagas.doSessionRequest)]
132+
])
133+
.put(login.setAdminAllowed(undefined))
134+
.put(login.setLoggedIn(true))
135+
.put(notification.connectSocket())
136+
.call(sagas.doSessionRequest)
137+
.run()
138+
})
139+
140+
test('relogin after invalid session', () => {
141+
return expectSaga(sagas.loginSuccessful)
142+
.provide([
143+
[select(sagas.sessionSelector), {invalidSession: true}],
144+
[matchers.call.fn(sagas.doSessionRequest)]
145+
])
146+
.not.put(login.setAdminAllowed(undefined))
147+
.put(login.setLoggedIn(true))
148+
.put(notification.connectSocket())
149+
.call(sagas.doSessionRequest)
150+
.run()
151+
})
80152
})
81153
})
82154
})

packages/core/app-extensions/src/notification/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {blockingInfo, removeBlockingInfo} from './modules/blocking/actions'
33
import NotificationCenter from './modules/center/NotificationCenter'
44
import {yesNoQuestion, confirm} from './modules/interactive/actions'
55
import {modal, removeModal} from './modules/modal/actions'
6+
import ModalContent from './modules/modal/ModalDisplay/ModalContent'
7+
import {StyledModalHolder, StyledPageOverlay} from './modules/modal/ModalDisplay/StyledComponents'
68
import {connectSocket, closeSocket} from './modules/socket/actions'
79
import {toaster, removeToaster} from './modules/toaster/actions'
810
import {addToStore} from './notification'
@@ -20,5 +22,8 @@ export default {
2022
toaster,
2123
removeToaster,
2224
connectSocket,
23-
closeSocket
25+
closeSocket,
26+
ModalContent,
27+
StyledModalHolder,
28+
StyledPageOverlay
2429
}

0 commit comments

Comments
 (0)