diff --git a/src/actions.js b/src/actions.js index 45a34eaf..ac0af447 100644 --- a/src/actions.js +++ b/src/actions.js @@ -278,8 +278,86 @@ export function fetch(config) { }, }, }); + + const endpoint = config.endpoint; + const payload = action?.payload || {}; + const response = payload?.response; + const status = response?.status; + const statusText = response?.statusText; + const gqlErrors = payload?.errors || response?.errors || []; + const message = payload?.message || action?.error?.message; + + if (action.error) { + let errorMessage = ""; + + if (!response && !message) { + errorMessage = "Server not responding"; + } else if (status) { + errorMessage = `HTTP ${status}: ${statusText || "Unknown status"}`; + } else if (gqlErrors?.length > 0) { + errorMessage = `GraphQL Error: ${gqlErrors.map(e => e.message).join("; ")}`; + } else if (message) { + errorMessage = `Network or API Error: ${message}`; + } else { + errorMessage = "Unknown error during API call"; + } + + Sentry.captureException(new Error(errorMessage), { + level: "error", + tags: { + endpoint, + status: status || "no-status", + type: config.method || "unknown-method", + }, + extra: { + endpoint, + status, + statusText, + body: config.body, + response: action.payload, + }, + }); + } + + if (!action.error && gqlErrors?.length > 0) { + const gqlMessage = gqlErrors.map(e => e.message).join("; "); + + Sentry.captureException(new Error(`GraphQL Error: ${gqlMessage}`), { + level: "error", + tags: { + endpoint, + type: config.method || "unknown-method", + }, + extra: { + endpoint, + errors: gqlErrors, + query: config.body, + }, + }); + } + + const norm = (m) => String(m || "").toLowerCase().replace(/['"]/g, "").trim(); + const csrfError = gqlErrors.some((e) => { + const msg = norm(e?.message); + return msg === "csrftoken" + || msg === "user not authorized for this operation" + || msg === "unauthorized"; + }); + + if (csrfError) { + dispatch( + coreConfirm( + "Session Expired", + "Your session has expired, You will be redirected to the login page.", + "csrf_logout" + ) + ); + return action; + } + } catch (err) { const errorMessage = "Server not responding"; + Sentry.captureException(new Error(errorMessage), { level: "error", tags: { @@ -292,64 +370,8 @@ export function fetch(config) { originalError: err, }, }); - return { - error: true, - payload: { message: errorMessage }, - }; - } - const endpoint = config.endpoint; - const response = action?.payload?.response; - const status = response?.status; - const statusText = response?.statusText; - const gqlErrors = response?.errors; - const message = action?.payload?.message || action?.error?.message; - - if (action.error) { - let errorMessage = ""; - if (!response && !message) { - errorMessage = "Server not responding"; - } - if (status) { - errorMessage = `HTTP ${status}: ${statusText || "Unknown status"}`; - } else if (gqlErrors?.length > 0) { - errorMessage = `GraphQL Error: ${gqlErrors.map((e) => e.message).join("; ")}`; - } else if (message) { - errorMessage = `Network or API Error: ${message}`; - } else { - errorMessage = "Unknown error during API call"; - } - - Sentry.captureException(new Error(errorMessage), { - level: "error", - tags: { - endpoint, - status: status || "no-status", - type: config.method || "unknown-method", - }, - extra: { - endpoint, - status, - statusText, - body: config.body, - response: action.payload, - }, - }); - } - - if (!action.error && gqlErrors && gqlErrors.length > 0) { - Sentry.captureException(new Error(`GraphQL Error: ${gqlErrors.map((e) => e.message).join("; ")}`), { - level: "error", - tags: { - endpoint, - type: config.method || "unknown-method", - }, - extra: { - endpoint, - errors: gqlErrors, - query: config.body, - }, - }); + throw err; } return action; @@ -550,9 +572,9 @@ export function clearAlert() { }; } -export function coreConfirm(title, message) { +export function coreConfirm(title, message, intent = null) { return (dispatch) => { - dispatch({ type: "CORE_CONFIRM", payload: { title, message } }); + dispatch({ type: "CORE_CONFIRM", payload: { title, message, intent } }); }; } diff --git a/src/components/App.jsx b/src/components/App.jsx index a1e137c0..54c7bd3a 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -1,5 +1,5 @@ import React, { useMemo, useEffect, useState } from "react"; -import { connect } from "react-redux"; +import { connect, useDispatch } from "react-redux"; import { IntlProvider } from "react-intl"; import { Route, BrowserRouter, Switch } from "react-router-dom"; import { CssBaseline } from "@mui/material"; @@ -49,6 +49,7 @@ const App = (props) => { history, error, confirm, + confirmed, user, messages, clearConfirm, @@ -59,11 +60,13 @@ const App = (props) => { rights, ...others } = props; + const dispatch = useDispatch(); const economicUnitConfig = modulesManager.getConf("fe-core", "App.economicUnitConfig", false); const [economicUnitDialogOpen, setEconomicUnitDialogOpen] = useState(false); const [isSecondaryCalendar, setSecondaryCalendar] = useBoolean(true); + const [lastConfirmIntent, setLastConfirmIntent] = useState(null); const auth = useAuthentication(); const routes = useMemo(() => { @@ -106,6 +109,19 @@ const App = (props) => { } }, []); + useEffect(() => { + setLastConfirmIntent(confirm?.intent ?? null); + }, [confirm]); + + useEffect(() => { + const handleConfirm = async () => { + if (confirmed === true && lastConfirmIntent === "csrf_logout") { + await onLogout(dispatch); + } + }; + handleConfirm(); + }, [confirmed, lastConfirmIntent, dispatch]); + useEffect(() => { const userHasModalRight = user?.rights ? user.rights.includes(RIGHT_VIEW_EU_MODAL) : false; if ( @@ -220,6 +236,7 @@ const mapStateToProps = (state) => ({ user: state.core.user?.i_user, error: state.core.error, confirm: state.core.confirm, + confirmed: state.core.confirmed, }); const mapDispatchToProps = (dispatch) => bindActionCreators({ clearConfirm, toggleCurrentCalendarType }, dispatch); diff --git a/src/pages/Role.jsx b/src/pages/Role.jsx index ed5dd1cf..1a4c5791 100644 --- a/src/pages/Role.jsx +++ b/src/pages/Role.jsx @@ -84,7 +84,11 @@ class Role extends Component { this.props.fetchRole([`clientMutationId: "${this.props.mutation.clientMutationId}"`]), ); } - } else if (prevProps.confirmed !== this.props.confirmed && !!this.props.confirmed && !!this.state.confirmedAction) { + } else if ( + prevProps.confirmed !== this.props.confirmed && + !!this.props.confirmed && + typeof this.state.confirmedAction === "function" + ) { this.state.confirmedAction(); } } diff --git a/src/pages/Roles.jsx b/src/pages/Roles.jsx index 7acc20ba..22feeee9 100644 --- a/src/pages/Roles.jsx +++ b/src/pages/Roles.jsx @@ -170,7 +170,11 @@ class Roles extends Component { if (prevProps.submittingMutation && !this.props.submittingMutation) { this.props.journalize(this.props.mutation); this.setState((state) => ({ deleted: state.deleted.concat(state.toDelete) })); - } else if (prevProps.confirmed !== this.props.confirmed && !!this.props.confirmed && !!this.state.confirmedAction) { + } else if ( + prevProps.confirmed !== this.props.confirmed && + !!this.props.confirmed && + typeof this.state.confirmedAction === "function" + ) { this.state.confirmedAction(); } }