diff --git a/assets/css/error-modal.scss b/assets/css/error-modal.scss
new file mode 100644
index 000000000..aff314b0a
--- /dev/null
+++ b/assets/css/error-modal.scss
@@ -0,0 +1,40 @@
+.modal.error-modal {
+ color: $text-primary;
+ .modal-dialog {
+ margin-top: 362px;
+ }
+
+ .modal-content {
+ background-color: $cool-gray-20;
+
+ .modal-header {
+ border-bottom: none;
+
+ .btn-close:hover {
+ background-color: transparent;
+ }
+ }
+
+ .modal-footer {
+ background-color: $cool-gray-20;
+ border-top: none;
+ height: 62px;
+ padding: 0 12px 0 0;
+
+ .btn {
+ height: 38px;
+ }
+
+ .error-modal__refresh-button {
+ background-color: $button-primary;
+ color: $button-secondary;
+ }
+
+ .error-modal__cancel-button {
+ background-color: $cool-gray-20;
+ color: #c1e4ff;
+ border: transparent;
+ }
+ }
+ }
+}
diff --git a/assets/css/screenplay.scss b/assets/css/screenplay.scss
index 6ddeb6928..80fb957a8 100644
--- a/assets/css/screenplay.scss
+++ b/assets/css/screenplay.scss
@@ -43,6 +43,7 @@ $form-feedback-invalid-color: $text-error;
@import "sort-label.scss";
@import "search-bar.scss";
@import "dashboard/picker.scss";
+@import "error-modal.scss";
@import "dashboard/toast.scss";
html {
diff --git a/assets/css/workflow.scss b/assets/css/workflow.scss
index 54d792706..a5771ddab 100644
--- a/assets/css/workflow.scss
+++ b/assets/css/workflow.scss
@@ -56,46 +56,3 @@
padding-left: 8px;
}
}
-
-.modal {
- &.error-modal {
- color: $text-primary;
- .modal-dialog {
- margin-top: 362px;
- }
-
- .modal-content {
- background-color: $cool-gray-20;
-
- .modal-header {
- border-bottom: none;
-
- .btn-close:hover {
- background-color: transparent;
- }
- }
-
- .modal-footer {
- background-color: $cool-gray-20;
- border-top: none;
- height: 62px;
- padding: 0 12px 0 0;
-
- .btn {
- height: 38px;
- }
-
- .error-modal__refresh-button {
- background-color: $button-primary;
- color: $button-secondary;
- }
-
- .error-modal__cancel-button {
- background-color: $cool-gray-20;
- color: #c1e4ff;
- border: transparent;
- }
- }
- }
- }
-}
diff --git a/assets/js/components/Dashboard/Dashboard.tsx b/assets/js/components/Dashboard/Dashboard.tsx
index 83b214ea6..59f20281f 100644
--- a/assets/js/components/Dashboard/Dashboard.tsx
+++ b/assets/js/components/Dashboard/Dashboard.tsx
@@ -13,6 +13,7 @@ import AlertBanner from "Components/AlertBanner";
import LinkCopiedToast from "Components/LinkCopiedToast";
import ActionOutcomeToast from "Components/ActionOutcomeToast";
import { useLocation } from "react-router-dom";
+import ErrorModal from "Components/ErrorModal";
const Dashboard: ComponentType = () => {
const {
@@ -24,6 +25,8 @@ const Dashboard: ComponentType = () => {
} = useScreenplayContext();
const dispatch = useScreenplayDispatchContext();
const [bannerDone, setBannerDone] = useState(false);
+ const [isAlertsIntervalRunning, setIsAlertsIntervalRunning] = useState(true);
+ const [showModal, setShowModal] = useState(false);
useEffect(() => {
fetchAlerts().then(
@@ -52,23 +55,35 @@ const Dashboard: ComponentType = () => {
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Fetch alerts every 4 seconds.
- useInterval(() => {
- fetchAlerts().then(
- ({
- all_alert_ids: allAPIalertIds,
- alerts: newAlerts,
- screens_by_alert: screensByAlertMap,
- }) => {
- findAndSetBannerAlert(alerts, newAlerts);
- dispatch({
- type: "SET_ALERTS",
- alerts: newAlerts,
- allAPIAlertIds: allAPIalertIds,
- screensByAlertMap: screensByAlertMap,
+ useInterval(
+ () => {
+ fetchAlerts()
+ .then(
+ ({
+ all_alert_ids: allAPIalertIds,
+ alerts: newAlerts,
+ screens_by_alert: screensByAlertMap,
+ }) => {
+ findAndSetBannerAlert(alerts, newAlerts);
+ dispatch({
+ type: "SET_ALERTS",
+ alerts: newAlerts,
+ allAPIAlertIds: allAPIalertIds,
+ screensByAlertMap: screensByAlertMap,
+ });
+ },
+ )
+ .catch((response: Response) => {
+ if (response.status === 403) {
+ setIsAlertsIntervalRunning(false);
+ setShowModal(true);
+ } else {
+ throw response;
+ }
});
- },
- );
- }, 4000);
+ },
+ isAlertsIntervalRunning ? 4000 : null,
+ );
const findAndSetBannerAlert = (oldAlerts: Alert[], newAlerts: Alert[]) => {
const now = new Date();
@@ -187,6 +202,14 @@ const Dashboard: ComponentType = () => {
)}
+ setShowModal(false)}
+ errorMessage="Your session has expired, please refresh your browser."
+ confirmButtonLabel="Refresh now"
+ onConfirm={() => window.location.reload()}
+ />
);
};
diff --git a/assets/js/components/Dashboard/ErrorModal.tsx b/assets/js/components/Dashboard/ErrorModal.tsx
new file mode 100644
index 000000000..986dfdbc2
--- /dev/null
+++ b/assets/js/components/Dashboard/ErrorModal.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+import { Button, Modal } from "react-bootstrap";
+
+interface ErrorModalProps {
+ title: string;
+ showErrorModal: boolean;
+ onHide: () => void;
+ errorMessage: string;
+ confirmButtonLabel: string;
+ onConfirm: () => void;
+}
+
+const ErrorModal = ({
+ title,
+ showErrorModal,
+ onHide,
+ errorMessage,
+ confirmButtonLabel,
+ onConfirm,
+}: ErrorModalProps) => {
+ return (
+
+
+ {title && {title}}
+
+ {errorMessage}
+
+
+
+
+
+ );
+};
+
+export default ErrorModal;
diff --git a/assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/GlEinkWorkflow.tsx b/assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/GlEinkWorkflow.tsx
index 45ba071ff..54e566066 100644
--- a/assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/GlEinkWorkflow.tsx
+++ b/assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/GlEinkWorkflow.tsx
@@ -10,7 +10,7 @@ import ConfigureScreensWorkflowPage, {
import BottomActionBar from "Components/PermanentConfiguration/BottomActionBar";
import { useLocation, useNavigate } from "react-router-dom";
import StationSelectPage from "Components/PermanentConfiguration/Workflows/GlEink/StationSelectPage";
-import { Alert, Button, Modal } from "react-bootstrap";
+import { Alert } from "react-bootstrap";
import { ExclamationCircleFill } from "react-bootstrap-icons";
import {
useConfigValidationContext,
@@ -18,6 +18,7 @@ import {
} from "Hooks/useScreenplayContext";
import { putPendingScreens } from "Utils/api";
import { useScreenplayContext } from "Hooks/useScreenplayContext";
+import ErrorModal from "Components/ErrorModal";
interface EditNavigationState {
place_id: string;
@@ -275,35 +276,15 @@ const GlEinkWorkflow: ComponentType = () => {
};
layout = (
<>
- setShowErrorModal(false)}
- >
-
-
- Someone else is configuring these screens
-
-
-
- In order not to overwrite each others work, please refresh your
- browser and fill-out the form again.
-
-
-
-
-
-
+ errorMessage="In order not to overwrite each others work, please refresh your
+ browser and fill-out the form again."
+ confirmButtonLabel="Refresh now"
+ onConfirm={() => window.location.reload()}
+ />
void, delay: number) {
+export function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef<() => void>(noop);
// Remember the latest callback.
diff --git a/assets/js/utils/api.ts b/assets/js/utils/api.ts
index 950eef3a8..0ccb292ae 100644
--- a/assets/js/utils/api.ts
+++ b/assets/js/utils/api.ts
@@ -19,7 +19,11 @@ interface AlertsResponse {
export const fetchAlerts = async (): Promise => {
const response = await fetch("/api/alerts");
- return await response.json();
+ if (response.status === 200) {
+ return await response.json();
+ } else {
+ throw response;
+ }
};
export const fetchActiveAndFutureAlerts = async (): Promise => {
diff --git a/assets/tests/setup.ts b/assets/tests/setup.ts
index f3e9381f1..13097e3f4 100644
--- a/assets/tests/setup.ts
+++ b/assets/tests/setup.ts
@@ -33,6 +33,7 @@ beforeEach(() => {
.fn()
.mockReturnValueOnce(
Promise.resolve({
+ status: 200,
json: () =>
Promise.resolve({
all_alert_ids: allAPIAlertIds,
diff --git a/lib/screenplay_web/auth_manager/error_handler.ex b/lib/screenplay_web/auth_manager/error_handler.ex
index 266b0b59e..e3e545f57 100644
--- a/lib/screenplay_web/auth_manager/error_handler.ex
+++ b/lib/screenplay_web/auth_manager/error_handler.ex
@@ -9,8 +9,12 @@ defmodule ScreenplayWeb.AuthManager.ErrorHandler do
@impl Guardian.Plug.ErrorHandler
def auth_error(conn, error, _opts) do
- auth_params = auth_params_for_error(error)
- Phoenix.Controller.redirect(conn, to: ~p"/auth/keycloak?#{auth_params}")
+ if conn.request_path =~ "api" or Plug.Conn.get_session(conn, :previous_path) =~ "api" do
+ Plug.Conn.send_resp(conn, 403, "Session expired")
+ else
+ auth_params = auth_params_for_error(error)
+ Phoenix.Controller.redirect(conn, to: ~p"/auth/keycloak?#{auth_params}")
+ end
end
def auth_params_for_error({:invalid_token, {:auth_expired, sub}}) do
diff --git a/test/screenplay_web/auth_manager/error_handler_test.exs b/test/screenplay_web/auth_manager/error_handler_test.exs
index 518e16adc..7b407653e 100644
--- a/test/screenplay_web/auth_manager/error_handler_test.exs
+++ b/test/screenplay_web/auth_manager/error_handler_test.exs
@@ -4,10 +4,19 @@ defmodule ScreenplayWeb.AuthManager.ErrorHandlerTest do
alias ScreenplayWeb.AuthManager.ErrorHandler
describe "auth_error/3" do
+ test "returns 403 for API endpoints if there's no refresh key", %{conn: conn} do
+ conn =
+ conn
+ |> init_test_session(%{previous_path: "api/test"})
+ |> ErrorHandler.auth_error({:some_type, :reason}, [])
+
+ assert %{status: 403} = conn
+ end
+
test "redirects to Keycloak login if there's no refresh key", %{conn: conn} do
conn =
conn
- |> init_test_session(%{})
+ |> init_test_session(%{previous_path: "test"})
|> ErrorHandler.auth_error({:some_type, :reason}, [])
assert html_response(conn, 302) =~ "\"/auth/keycloak\?prompt%3Dlogin"