diff --git a/package.json b/package.json index acd564e..618b46a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "global-shell-app", - "version": "1.9.1", + "version": "1.11.0", "description": "", "license": "BSD-3-Clause", "private": true, @@ -51,9 +51,13 @@ "typescript": "^5.7.3" }, "dependencies": { - "@dhis2/app-runtime": "^3.14.0", + "@dhis2/app-runtime": "^3.16.0", "@dhis2/pwa": "^12.3.0", "@dhis2/ui": "^10.1.13", + "@types/js-cookie": "^3.0.6", + "@types/post-robot": "^10.0.6", + "js-cookie": "^3.0.5", + "post-robot": "^10.0.46", "react-router": "^7.2.0", "styled-jsx": "^4.0.1" }, diff --git a/src/App.jsx b/src/App.jsx index 15a9261..3781236 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,6 +7,7 @@ import styles from './App.module.css' import { ConnectedHeaderBar } from './components/ConnectedHeaderbar.jsx' import { PluginLoader } from './components/PluginLoader.jsx' import { RedirectHandler } from './components/RedirectHandler.tsx' +import { SessionHandler } from './components/session-handler/index.ts' import { ClientPWAProvider } from './lib/clientPWAUpdateState.jsx' const APPS_INFO_QUERY = { @@ -27,8 +28,12 @@ const APPS_INFO_QUERY = { } const Layout = ({ appsInfoQuery }) => { + const config = useConfig() + const sessionTimeout = config?.systemInfo?.sessionTimeout + return (
+ {/* Skip the routes in dev; they don't make the same sense */} {process.env.NODE_ENV !== 'development' ? : null} diff --git a/src/components/session-handler/index.ts b/src/components/session-handler/index.ts new file mode 100644 index 0000000..bb37be9 --- /dev/null +++ b/src/components/session-handler/index.ts @@ -0,0 +1 @@ +export { SessionHandler } from './session-handler' diff --git a/src/components/session-handler/modal-countdown.tsx b/src/components/session-handler/modal-countdown.tsx new file mode 100644 index 0000000..834a110 --- /dev/null +++ b/src/components/session-handler/modal-countdown.tsx @@ -0,0 +1,28 @@ +import i18n from '@dhis2/d2-i18n' +import { + Button, + ButtonStrip, + Modal, + ModalActions, + ModalContent, + ModalTitle, +} from '@dhis2/ui' +import * as React from 'react' + +export const ExpirationCountdownModal = ({ countDown }) => { + return ( + + {i18n.t('Your session is about to expire')} + + Your session will expire in {countDown} seconds. + + + + {/* can this get into undesired state when offline - i.e. can't extend but want to dismiss */} + + {/* */} + + + + ) +} diff --git a/src/components/session-handler/modal-expired.tsx b/src/components/session-handler/modal-expired.tsx new file mode 100644 index 0000000..acee019 --- /dev/null +++ b/src/components/session-handler/modal-expired.tsx @@ -0,0 +1,47 @@ +import { useConfig } from '@dhis2/app-runtime' +import i18n from '@dhis2/d2-i18n' +import { + Button, + ButtonStrip, + Modal, + ModalActions, + ModalContent, + ModalTitle, +} from '@dhis2/ui' +import * as React from 'react' + +export const ExpiredModal = ({ dismissModal }) => { + const { baseUrl } = useConfig() + + const goToLogin = () => { + window.open(baseUrl) + } + const dismiss = () => { + dismissModal() + } + // console.log(config) + return ( + + {i18n.t('Your session has expired')} + + {/* {i18n.t('Your session has expired.')}{' '} */} + {/* {i18n.t( + 'You can go to the login page or dismiss the modal if the app supports working offline.' + )} */} +
+ Eos omnis cumque quia quaerat aut. Neque consequuntur sed non a + quibusdam eligendi. Fugit eveniet expedita nihil ab maxime sequi + nihil quidem. Et aut nobis assumenda in iure. +
+ + + + + + + +
+ ) +} diff --git a/src/components/session-handler/session-handler.tsx b/src/components/session-handler/session-handler.tsx new file mode 100644 index 0000000..cfac844 --- /dev/null +++ b/src/components/session-handler/session-handler.tsx @@ -0,0 +1,53 @@ +import postRobot from 'post-robot' +import * as React from 'react' +import { ExpirationCountdownModal } from './modal-countdown' +import { ExpiredModal } from './modal-expired' +import { useCheckCookie } from './use-check-cookie' + +const globalShellBroadcast = new BroadcastChannel('global-shell') +const broadCastMessage = 'BROADCAST_SESSION_EXPIRED' + +// ToDO: there is some re-rendering hell going on with the component - still needs some tightening up +export const SessionHandler = ({ sessionTimeout }) => { + const { showWarning, time, expired, reset } = useCheckCookie({ + sessionTimeout, + warningThreshold: Number(sessionTimeout) / 10, + }) + const [modalHidden, hideModal] = React.useState(false) + const [received401, setReceived401] = React.useState(false) + + globalShellBroadcast.addEventListener('message', (ev) => { + if (ev.data === broadCastMessage) { + console.log( + `[Session] Received broadcast message from another window: "${ev.data}" on channel "${globalShellBroadcast.name}"` + ) + hideModal(false) + setReceived401(true) + } + }) + + React.useEffect(() => { + postRobot.on('notifyShell', (event) => { + console.log( + ` [Session] [API monitor]: ${event?.data?.resource} (${event?.data.status})` + ) + // todo: check API call are to DHIS2 server specifically? + if (event?.data?.status == 401) { + hideModal(false) + setReceived401(true) + // todo: should broadcast 2xx to extend session across windows + globalShellBroadcast.postMessage(broadCastMessage) + } else { + reset() + } + }) + }, []) + + if (!expired && showWarning) { + return + } + if ((expired || received401) && !modalHidden) { + return hideModal(true)} /> + } + return null +} diff --git a/src/components/session-handler/use-check-cookie.ts b/src/components/session-handler/use-check-cookie.ts new file mode 100644 index 0000000..d3d55b1 --- /dev/null +++ b/src/components/session-handler/use-check-cookie.ts @@ -0,0 +1,42 @@ +import { useEffect, useRef, useState } from 'react' + +// const WARNING_THRESHOLD_IN_SECONDS = 20 +const CHECK_INTERVAL = 1000 // ms countdown every second +export const useCheckCookie = ({ sessionTimeout, warningThreshold }) => { + const timeout = useRef(sessionTimeout) + + // console.log('??? warningthreshold', warningThresholdSeconds) + const stime = useRef(timeout.current) + const [time, setTime] = useState(timeout.current) + + const [expired, setExpired] = useState(false) + const [warningShown, setShowWarning] = useState(false) + + const reset = () => { + stime.current = Math.round(sessionTimeout) + setExpired(false) + setShowWarning(false) + setTime(stime.current) + } + useEffect(() => { + const interval = setInterval(() => { + console.log(`[Session] Remaining seconds: ${stime.current}`) + setShowWarning(stime.current < warningThreshold) + if (stime.current === 1) { + setExpired(true) + clearInterval(interval) + } + stime.current = stime.current - 1 + setTime(stime.current) + }, CHECK_INTERVAL) + + return () => clearInterval(interval) + }, []) + + return { + reset, + time, + expired, + showWarning: warningShown, + } +} diff --git a/yarn.lock b/yarn.lock index 9afe7ed..ff498c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1845,47 +1845,63 @@ "@dhis2/pwa" "12.3.0" moment "^2.24.0" -"@dhis2/app-runtime@^3.12.0", "@dhis2/app-runtime@^3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@dhis2/app-runtime/-/app-runtime-3.14.0.tgz#85a6f337036ba31868fd7193360208ef174d4345" - integrity sha512-0qmP2QxhoK8dWCAOOomD+JDer44AC5rxOHuK5qINNt1K4BMZgG5axmkvk6/86SXTRtLj2+Iio7BLe9DBnh+Bxg== - dependencies: - "@dhis2/app-service-alerts" "3.14.0" - "@dhis2/app-service-config" "3.14.0" - "@dhis2/app-service-data" "3.14.0" - "@dhis2/app-service-offline" "3.14.0" - "@dhis2/app-service-plugin" "3.14.0" - -"@dhis2/app-service-alerts@3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@dhis2/app-service-alerts/-/app-service-alerts-3.14.0.tgz#93eab29b3e42115e9de509bec7daa172cc50a0f4" - integrity sha512-VhXc0w5fX+2Rd4J7MZmklnl7Fq7PLnCrSYDSWRZPYXbNo428QjVIiN/1g003gCatVPon3bl19LisZbn5vmNoRg== - -"@dhis2/app-service-config@3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@dhis2/app-service-config/-/app-service-config-3.14.0.tgz#823ce5e96c6deb8beec92bd65180ca611f0551a0" - integrity sha512-v/gnPMG7IZ6pEutYnHJff4S93w1DAQbeDiGtRqMffEiNxa9Bx5wc5MPTGMyeUGeEw8on3qaeP/2rQa+PGaQvcg== - -"@dhis2/app-service-data@3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@dhis2/app-service-data/-/app-service-data-3.14.0.tgz#7109852154af0b02240e077df9ae5e65a727a593" - integrity sha512-7DQVlVSCdJauduw+9LXrb13IsNagGzPNN2Iy9e2jVeE+eNqP6j0PeI6enHTIjaYA6OOSx/Ob7/HXkBVH8wyLlg== +"@dhis2/app-runtime@^3.12.0", "@dhis2/app-runtime@^3.16.0": + version "3.16.0" + resolved "https://registry.yarnpkg.com/@dhis2/app-runtime/-/app-runtime-3.16.0.tgz#50bf19b1d8f648940d5c5022cc4cd2f9a23cd5b5" + integrity sha512-4PbXIpjoclQwVPkZrZwKUUPqN4UWbTE6nuC4h70q2fY6rHEE+QPSBIEDno+rxpQIMy+HtZm0hrDEUK387EMKjA== + dependencies: + "@dhis2/app-service-alerts" "3.16.0" + "@dhis2/app-service-config" "3.16.0" + "@dhis2/app-service-data" "3.16.0" + "@dhis2/app-service-offline" "3.16.0" + "@dhis2/app-service-plugin" "3.16.0" + "@dhis2/app-service-user" "3.16.0" + prop-types "^15.7.2" + +"@dhis2/app-service-alerts@3.16.0": + version "3.16.0" + resolved "https://registry.yarnpkg.com/@dhis2/app-service-alerts/-/app-service-alerts-3.16.0.tgz#373d9ac1aee462cc2f12a9fa7ccfe2d4c986022b" + integrity sha512-aTC47gbYebuf0yJfSX7POpmaC+ejG7DCBmAm+qBL3NkBqlsmNI3A0sO9EjztEvkuxnLA/WRuKhu1an+0jsmpTw== + dependencies: + prop-types "^15.7.2" + +"@dhis2/app-service-config@3.16.0": + version "3.16.0" + resolved "https://registry.yarnpkg.com/@dhis2/app-service-config/-/app-service-config-3.16.0.tgz#5b2bd81f638dd4f02e811679d60b56bbd1147d7a" + integrity sha512-kU9aavV8GBjRP8NU/ZsmOuQSSs8p5kcX3bTaCvlw7dgRA4eHscdG24JaHYqAfPFQI0ntPT88Qf8m86Obz375cA== + dependencies: + prop-types "^15.7.2" + +"@dhis2/app-service-data@3.16.0": + version "3.16.0" + resolved "https://registry.yarnpkg.com/@dhis2/app-service-data/-/app-service-data-3.16.0.tgz#8f47a9de9004288f0c21c34d22a4cabf93313671" + integrity sha512-RFF4wg2m/6dIEl1KmiaX2bw3TPMgB6R9gu27HyOCFNYT808wtFkvyrpa8TAWjF4m+y+yLZyKtHO0yEXp7otfHQ== dependencies: "@tanstack/react-query" "^4.36.1" + prop-types "^15.7.2" -"@dhis2/app-service-offline@3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@dhis2/app-service-offline/-/app-service-offline-3.14.0.tgz#9164f4ef1ffef18d52a77f36bfd3683b5ccbdb81" - integrity sha512-sgp7AtaPIvNt5TeXCrVR2/ndtgx9p+TstQWiBzRq75R894P34nqBX9jWMVUoNDZU35OfcGqtxHfOrpoLrdZ0XA== +"@dhis2/app-service-offline@3.16.0": + version "3.16.0" + resolved "https://registry.yarnpkg.com/@dhis2/app-service-offline/-/app-service-offline-3.16.0.tgz#73848e8d3e29c7eca0b29b6d2504e65be93463aa" + integrity sha512-I67msCV3miwh4La4Dpb0HI2dab5Hs20mNmJGPbaN7WLizxbgVMvyrqtTXBGOT7iZulTgc0fB4zUSAaeE3r7pvA== dependencies: lodash "^4.17.21" + prop-types "^15.7.2" -"@dhis2/app-service-plugin@3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@dhis2/app-service-plugin/-/app-service-plugin-3.14.0.tgz#ea6ed4afc805072352e861e5412223b056e2edcf" - integrity sha512-3D9iPG6/HFT1dY7G6Wwwunv2bQ3hMzW2gsw8rFAcOphFHMujzQYUYuaZQmxg7rs/Hh/DPzLVOzsNAqOi1cS7wA== +"@dhis2/app-service-plugin@3.16.0": + version "3.16.0" + resolved "https://registry.yarnpkg.com/@dhis2/app-service-plugin/-/app-service-plugin-3.16.0.tgz#eb191e39eb0f183936073b998505be3035fdc29c" + integrity sha512-pjjlHLEdLFFr+fuDKjBBjeYM8nyZuBOl1Pi/jCzsAq1HdmxkPI4hIhsCbRkoRWpIs7s9EW0ONI/vf2bkxnct1Q== dependencies: post-robot "^10.0.46" + prop-types "^15.7.2" + +"@dhis2/app-service-user@3.16.0": + version "3.16.0" + resolved "https://registry.yarnpkg.com/@dhis2/app-service-user/-/app-service-user-3.16.0.tgz#9033187955ad9b2f62793da7134d41c707595a7d" + integrity sha512-3bFeiy4NDtwvzo+mF9q70B6GslNKf0lbr9Ujb6PXsp9IqJpahzqFvzMDruTjP/eAdOCNE6o/UL8ZHgSux7aVTg== + dependencies: + prop-types "^15.7.2" "@dhis2/app-shell@12.3.0": version "12.3.0" @@ -3030,6 +3046,11 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/js-cookie@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.6.tgz#a04ca19e877687bd449f5ad37d33b104b71fdf95" + integrity sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ== + "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -3079,6 +3100,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== +"@types/post-robot@^10.0.6": + version "10.0.6" + resolved "https://registry.yarnpkg.com/@types/post-robot/-/post-robot-10.0.6.tgz#9c8b1db55dd109cc36aa5a0d8edbd46827fd572c" + integrity sha512-x+LaZPQ4TYW8aBbKpFZsJn7TGipka7tC2ZokE8gpCAlh4E4XFgxxcvt19LD41BeExmlzAtjHh700r3r8SdfX9A== + "@types/prettier@^2.1.5": version "2.7.3" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" @@ -7743,6 +7769,11 @@ jiti@^1.20.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"