react-session.manager.sk 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/ErrorBoundary.js +128 -0
- package/src/index.js +202 -82
package/package.json
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { toast } from "react-toastify";
|
|
3
|
+
import { Error as ErrorIcon } from "@mui/icons-material";
|
|
4
|
+
|
|
5
|
+
class ErrorBoundary extends React.Component {
|
|
6
|
+
constructor(props) {
|
|
7
|
+
super(props);
|
|
8
|
+
this.state = { hasError: false, error: null, errorInfo: null };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static getDerivedStateFromError(error) {
|
|
12
|
+
// Update state so the next render will show the fallback UI
|
|
13
|
+
return { hasError: true };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
componentDidCatch(error, errorInfo) {
|
|
17
|
+
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
|
18
|
+
|
|
19
|
+
this.setState({
|
|
20
|
+
error: error,
|
|
21
|
+
errorInfo: errorInfo,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Show a toast notification for the error
|
|
25
|
+
toast.error(
|
|
26
|
+
"Something went wrong. Please refresh the page or try again later.",
|
|
27
|
+
{
|
|
28
|
+
toastId: "error-boundary",
|
|
29
|
+
icon: <ErrorIcon />,
|
|
30
|
+
autoClose: 10000,
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Log error to external service if needed
|
|
35
|
+
if (this.props.onError) {
|
|
36
|
+
this.props.onError(error, errorInfo);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
handleRetry = () => {
|
|
41
|
+
this.setState({ hasError: false, error: null, errorInfo: null });
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
render() {
|
|
45
|
+
if (this.state.hasError) {
|
|
46
|
+
// Custom error UI
|
|
47
|
+
if (this.props.fallback) {
|
|
48
|
+
return this.props.fallback;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
padding: "20px",
|
|
55
|
+
textAlign: "center",
|
|
56
|
+
backgroundColor: "#f8f9fa",
|
|
57
|
+
border: "1px solid #dee2e6",
|
|
58
|
+
borderRadius: "8px",
|
|
59
|
+
margin: "20px",
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<ErrorIcon
|
|
63
|
+
style={{ fontSize: "48px", color: "#dc3545", marginBottom: "16px" }}
|
|
64
|
+
/>
|
|
65
|
+
<h2 style={{ color: "#343a40", marginBottom: "16px" }}>
|
|
66
|
+
Something went wrong
|
|
67
|
+
</h2>
|
|
68
|
+
<p style={{ color: "#6c757d", marginBottom: "20px" }}>
|
|
69
|
+
An unexpected error occurred. Please try refreshing the page.
|
|
70
|
+
</p>
|
|
71
|
+
<button
|
|
72
|
+
onClick={this.handleRetry}
|
|
73
|
+
style={{
|
|
74
|
+
backgroundColor: "#007bff",
|
|
75
|
+
color: "white",
|
|
76
|
+
border: "none",
|
|
77
|
+
padding: "10px 20px",
|
|
78
|
+
borderRadius: "4px",
|
|
79
|
+
cursor: "pointer",
|
|
80
|
+
marginRight: "10px",
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
Try Again
|
|
84
|
+
</button>
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => window.location.reload()}
|
|
87
|
+
style={{
|
|
88
|
+
backgroundColor: "#28a745",
|
|
89
|
+
color: "white",
|
|
90
|
+
border: "none",
|
|
91
|
+
padding: "10px 20px",
|
|
92
|
+
borderRadius: "4px",
|
|
93
|
+
cursor: "pointer",
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
Refresh Page
|
|
97
|
+
</button>
|
|
98
|
+
|
|
99
|
+
{process.env.NODE_ENV === "development" && this.state.error && (
|
|
100
|
+
<details style={{ marginTop: "20px", textAlign: "left" }}>
|
|
101
|
+
<summary style={{ cursor: "pointer", fontWeight: "bold" }}>
|
|
102
|
+
Error Details (Development)
|
|
103
|
+
</summary>
|
|
104
|
+
<pre
|
|
105
|
+
style={{
|
|
106
|
+
backgroundColor: "#f8f9fa",
|
|
107
|
+
padding: "10px",
|
|
108
|
+
borderRadius: "4px",
|
|
109
|
+
overflow: "auto",
|
|
110
|
+
fontSize: "12px",
|
|
111
|
+
marginTop: "10px",
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{this.state.error && this.state.error.toString()}
|
|
115
|
+
<br />
|
|
116
|
+
{this.state.errorInfo?.componentStack}
|
|
117
|
+
</pre>
|
|
118
|
+
</details>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return this.props.children;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default ErrorBoundary;
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { createContext, useState, useEffect, useCallback } from "react";
|
|
2
2
|
import versionCompare from "./components/versionCompare";
|
|
3
3
|
import getDeviceFingerprint from "./components/FingerPrint";
|
|
4
|
+
import ErrorBoundary from "./components/ErrorBoundary";
|
|
4
5
|
// Notifications
|
|
5
6
|
import { ToastContainer, toast } from "react-toastify";
|
|
6
7
|
import "react-toastify/dist/ReactToastify.css";
|
|
@@ -40,9 +41,26 @@ const SessionManagerProvider = ({
|
|
|
40
41
|
|
|
41
42
|
useEffect(() => {
|
|
42
43
|
if (!deviceUID) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
try {
|
|
45
|
+
const uid = getDeviceFingerprint();
|
|
46
|
+
if (uid) {
|
|
47
|
+
setDeviceUID(uid);
|
|
48
|
+
localStorage.setItem("deviceUID", uid);
|
|
49
|
+
AuthenticatedAxiosObject.defaults.headers.common["deviceUID"] = uid;
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error("Failed to generate device fingerprint:", err);
|
|
53
|
+
// Generate a fallback UUID
|
|
54
|
+
const fallbackUID =
|
|
55
|
+
"fallback-" +
|
|
56
|
+
Date.now() +
|
|
57
|
+
"-" +
|
|
58
|
+
Math.random().toString(36).substr(2, 9);
|
|
59
|
+
setDeviceUID(fallbackUID);
|
|
60
|
+
localStorage.setItem("deviceUID", fallbackUID);
|
|
61
|
+
AuthenticatedAxiosObject.defaults.headers.common["deviceUID"] =
|
|
62
|
+
fallbackUID;
|
|
63
|
+
}
|
|
46
64
|
}
|
|
47
65
|
}, [deviceUID, AuthenticatedAxiosObject]);
|
|
48
66
|
|
|
@@ -60,7 +78,7 @@ const SessionManagerProvider = ({
|
|
|
60
78
|
setTimeout(() => {
|
|
61
79
|
refreshToken()
|
|
62
80
|
.then((data) => {
|
|
63
|
-
if (data
|
|
81
|
+
if (data?.refreshed) {
|
|
64
82
|
const token = `Bearer ${data.access_token}`;
|
|
65
83
|
setCurrent(token);
|
|
66
84
|
AuthenticatedAxiosObject.defaults.headers.common[
|
|
@@ -71,10 +89,23 @@ const SessionManagerProvider = ({
|
|
|
71
89
|
}
|
|
72
90
|
})
|
|
73
91
|
.catch((err) => {
|
|
74
|
-
console.
|
|
75
|
-
|
|
92
|
+
console.error("Token refresh failed:", err);
|
|
93
|
+
// Clear invalid tokens
|
|
76
94
|
localStorage.removeItem("Authorization");
|
|
77
95
|
sessionStorage.removeItem("Authorization");
|
|
96
|
+
setCurrent("");
|
|
97
|
+
setCurrentLoggedIn(false);
|
|
98
|
+
|
|
99
|
+
// Only show toast for actual network/server errors, not auth failures
|
|
100
|
+
if (err?.code === "ERR_NETWORK" || err?.response?.status >= 500) {
|
|
101
|
+
toast.error(
|
|
102
|
+
"Unable to refresh your session. Please login again.",
|
|
103
|
+
{
|
|
104
|
+
toastId: "token-refresh-error",
|
|
105
|
+
icon: <Logout />,
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
}
|
|
78
109
|
});
|
|
79
110
|
}, 100);
|
|
80
111
|
},
|
|
@@ -87,8 +118,8 @@ const SessionManagerProvider = ({
|
|
|
87
118
|
|
|
88
119
|
if (local_auth) fromPrevious(local_auth, true);
|
|
89
120
|
else if (sess_auth) fromPrevious(sess_auth, false);
|
|
90
|
-
|
|
91
|
-
}, [
|
|
121
|
+
// No cleanup needed here since fromPrevious doesn't create intervals
|
|
122
|
+
}, [fromPrevious]);
|
|
92
123
|
|
|
93
124
|
// Wrap setHeader to store new header names in localStorage
|
|
94
125
|
const setHeader = (header) => {
|
|
@@ -104,13 +135,31 @@ const SessionManagerProvider = ({
|
|
|
104
135
|
setTimeout(() => {
|
|
105
136
|
userLoader()
|
|
106
137
|
.then((res) => {
|
|
107
|
-
const data = res
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
138
|
+
const data = res?.data;
|
|
139
|
+
if (data) {
|
|
140
|
+
setCurrentLoggedIn(data.logged_in);
|
|
141
|
+
setIsAdmin(data.is_admin);
|
|
142
|
+
setUserInfo(data.Info || {});
|
|
143
|
+
}
|
|
111
144
|
})
|
|
112
145
|
.catch((err) => {
|
|
113
|
-
console.
|
|
146
|
+
console.error("User data loading failed:", err);
|
|
147
|
+
// Don't show toast for auth errors (401, 403)
|
|
148
|
+
if (err?.response?.status !== 401 && err?.response?.status !== 403) {
|
|
149
|
+
if (err?.code === "ERR_NETWORK" || err?.response?.status >= 500) {
|
|
150
|
+
toast.error(
|
|
151
|
+
"Unable to load user information. Some features may be limited.",
|
|
152
|
+
{
|
|
153
|
+
toastId: "user-load-error",
|
|
154
|
+
icon: <GppBad />,
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Set safe defaults
|
|
160
|
+
setCurrentLoggedIn(false);
|
|
161
|
+
setIsAdmin(false);
|
|
162
|
+
setUserInfo({});
|
|
114
163
|
})
|
|
115
164
|
.finally(() => {
|
|
116
165
|
setLoadingUser(false);
|
|
@@ -138,7 +187,7 @@ const SessionManagerProvider = ({
|
|
|
138
187
|
clearInterval(interval);
|
|
139
188
|
}
|
|
140
189
|
return () => clearInterval(interval);
|
|
141
|
-
}, [
|
|
190
|
+
}, [fromPrevious, currentLoggin, refreshTimer]);
|
|
142
191
|
|
|
143
192
|
useEffect(() => {
|
|
144
193
|
const customInterceptor = () => {
|
|
@@ -147,9 +196,12 @@ const SessionManagerProvider = ({
|
|
|
147
196
|
return response;
|
|
148
197
|
},
|
|
149
198
|
(error) => {
|
|
199
|
+
console.error("Axios interceptor caught error:", error);
|
|
200
|
+
|
|
201
|
+
// Handle session expiry (455)
|
|
150
202
|
if (error?.response?.status === 455) {
|
|
151
203
|
try {
|
|
152
|
-
const status = error.response.data
|
|
204
|
+
const status = error.response.data?.logged_in || false;
|
|
153
205
|
if (!status) {
|
|
154
206
|
console.log("Session ended");
|
|
155
207
|
setCurrentLoggedIn(false);
|
|
@@ -157,50 +209,83 @@ const SessionManagerProvider = ({
|
|
|
157
209
|
"Authorization"
|
|
158
210
|
] = ``;
|
|
159
211
|
toast.info(
|
|
160
|
-
"Your session
|
|
212
|
+
"Your session is no longer valid, please login again.",
|
|
161
213
|
{ toastId: "Forced_log_out", icon: <Logout /> }
|
|
162
214
|
);
|
|
163
215
|
}
|
|
164
216
|
} catch (err) {
|
|
165
|
-
|
|
217
|
+
console.error("Error handling session expiry:", err);
|
|
218
|
+
// Don't throw here - just log and continue
|
|
166
219
|
}
|
|
220
|
+
return Promise.reject(error);
|
|
167
221
|
}
|
|
222
|
+
|
|
223
|
+
// Handle version mismatch (426)
|
|
168
224
|
if (error?.response?.status === 426) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
setTimeout(() => {
|
|
178
|
-
window.location.reload();
|
|
179
|
-
}, 1000 * reloads);
|
|
180
|
-
} else {
|
|
181
|
-
toast.warning(
|
|
182
|
-
"The application needs to be updated please wait for some time then reload the page.",
|
|
183
|
-
{
|
|
184
|
-
toastId: "appReloadError",
|
|
185
|
-
icon: <Update />,
|
|
186
|
-
}
|
|
225
|
+
try {
|
|
226
|
+
sessionStorage.setItem("appVersionOld", true);
|
|
227
|
+
sessionStorage.setItem(
|
|
228
|
+
"requiredVersion",
|
|
229
|
+
error.response.data?.minVersion || "unknown"
|
|
230
|
+
);
|
|
231
|
+
let reloads = parseInt(
|
|
232
|
+
sessionStorage.getItem("appReloads") || "0"
|
|
187
233
|
);
|
|
234
|
+
if (reloads < 2) {
|
|
235
|
+
sessionStorage.setItem("appReloads", (reloads + 1).toString());
|
|
236
|
+
setTimeout(() => {
|
|
237
|
+
window.location.reload();
|
|
238
|
+
}, 1000 * reloads);
|
|
239
|
+
} else {
|
|
240
|
+
toast.warning(
|
|
241
|
+
"The application needs to be updated. Please wait for some time then reload the page.",
|
|
242
|
+
{
|
|
243
|
+
toastId: "appReloadError",
|
|
244
|
+
icon: <Update />,
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error("Error handling version mismatch:", err);
|
|
188
250
|
}
|
|
251
|
+
return Promise.reject(error);
|
|
189
252
|
}
|
|
190
253
|
|
|
254
|
+
// Handle network errors and connection issues
|
|
191
255
|
if (!error?.response?.status) {
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
256
|
+
if (
|
|
257
|
+
error?.code !== "ERR_CANCELED" &&
|
|
258
|
+
error?.message !== "canceled"
|
|
259
|
+
) {
|
|
260
|
+
const errorMessage =
|
|
261
|
+
error?.code === "ERR_NETWORK"
|
|
262
|
+
? "Unable to connect to the server. Please check your internet connection and try again."
|
|
263
|
+
: "The server is not responding, please reload or try again later.";
|
|
264
|
+
|
|
265
|
+
toast.error(errorMessage, {
|
|
266
|
+
toastId: `${error?.code || "ERR_CONNECTION_REFUSED"}`,
|
|
267
|
+
icon: <GppBad />,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return Promise.reject(error);
|
|
197
271
|
}
|
|
198
|
-
|
|
272
|
+
|
|
273
|
+
// For all other errors, just reject without throwing
|
|
274
|
+
return Promise.reject(error);
|
|
199
275
|
}
|
|
200
276
|
);
|
|
201
277
|
};
|
|
202
|
-
|
|
203
|
-
|
|
278
|
+
|
|
279
|
+
// Call the interceptor setup
|
|
280
|
+
customInterceptor();
|
|
281
|
+
|
|
282
|
+
// Cleanup function to remove interceptors
|
|
283
|
+
return () => {
|
|
284
|
+
if (AuthenticatedAxiosObject?.interceptors?.response) {
|
|
285
|
+
AuthenticatedAxiosObject.interceptors.response.clear();
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}, [AuthenticatedAxiosObject, currentLoggin, setCurrentLoggedIn]);
|
|
204
289
|
|
|
205
290
|
// We will use the below to refresh our data about the user when ever we flag refreshData as true
|
|
206
291
|
const [refreshData, setRefreshFlag] = useState(false);
|
|
@@ -217,11 +302,20 @@ const SessionManagerProvider = ({
|
|
|
217
302
|
|
|
218
303
|
useEffect(() => {
|
|
219
304
|
if (refreshData) {
|
|
220
|
-
userLoader()
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
305
|
+
userLoader()
|
|
306
|
+
.then((res) => {
|
|
307
|
+
const data = res?.data;
|
|
308
|
+
if (data) {
|
|
309
|
+
setUserInfo(data.Info || {});
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
.catch((err) => {
|
|
313
|
+
console.error("User data refresh failed:", err);
|
|
314
|
+
// Silently fail for data refresh - don't overwhelm user with toasts
|
|
315
|
+
})
|
|
316
|
+
.finally(() => {
|
|
317
|
+
setRefreshData(false);
|
|
318
|
+
});
|
|
225
319
|
}
|
|
226
320
|
}, [refreshData, userLoader]);
|
|
227
321
|
|
|
@@ -230,10 +324,21 @@ const SessionManagerProvider = ({
|
|
|
230
324
|
};
|
|
231
325
|
|
|
232
326
|
// Check if user has specific role
|
|
233
|
-
|
|
234
327
|
const hasRole = (roles) => {
|
|
235
|
-
|
|
236
|
-
|
|
328
|
+
if (
|
|
329
|
+
!roles ||
|
|
330
|
+
!Array.isArray(roles) ||
|
|
331
|
+
!userInfo?.roles ||
|
|
332
|
+
!Array.isArray(userInfo.roles)
|
|
333
|
+
) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
return roles.some((r) => userInfo.roles.indexOf(r) >= 0);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
console.error("Error checking user roles:", err);
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
237
342
|
};
|
|
238
343
|
|
|
239
344
|
const contextValue = {
|
|
@@ -251,43 +356,58 @@ const SessionManagerProvider = ({
|
|
|
251
356
|
};
|
|
252
357
|
|
|
253
358
|
return (
|
|
254
|
-
<
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
359
|
+
<ErrorBoundary>
|
|
360
|
+
<SessionManager.Provider value={contextValue}>
|
|
361
|
+
<ToastContainer
|
|
362
|
+
position="top-left"
|
|
363
|
+
autoClose={5000}
|
|
364
|
+
closeOnClick
|
|
365
|
+
pauseOnFocusLoss
|
|
366
|
+
pauseOnHover
|
|
367
|
+
newestOnTop={false}
|
|
368
|
+
toastClassName={"custToast materialToast"}
|
|
369
|
+
{...toastOptions}
|
|
370
|
+
/>
|
|
371
|
+
<VersionProtection appVersion={appVersion} />
|
|
372
|
+
{children}
|
|
373
|
+
</SessionManager.Provider>
|
|
374
|
+
</ErrorBoundary>
|
|
268
375
|
);
|
|
269
376
|
};
|
|
270
377
|
|
|
271
378
|
function VersionProtection({ appVersion }) {
|
|
272
|
-
const oldVersion =
|
|
379
|
+
const [oldVersion, setOldVersion] = useState(false);
|
|
380
|
+
|
|
273
381
|
useEffect(() => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
382
|
+
try {
|
|
383
|
+
const isOldVersion = sessionStorage.getItem("appVersionOld") === "true";
|
|
384
|
+
setOldVersion(isOldVersion);
|
|
385
|
+
|
|
386
|
+
if (
|
|
387
|
+
isOldVersion &&
|
|
388
|
+
sessionStorage.getItem("requiredVersion") &&
|
|
389
|
+
appVersion &&
|
|
390
|
+
versionCompare(appVersion, sessionStorage.getItem("requiredVersion"))
|
|
391
|
+
) {
|
|
392
|
+
console.log("Update Success Toast");
|
|
393
|
+
toast.success("Your application has been updated", {
|
|
394
|
+
toastId: "appReload",
|
|
395
|
+
icon: <BrowserUpdated />,
|
|
396
|
+
onClose: () => {
|
|
397
|
+
try {
|
|
398
|
+
sessionStorage.removeItem("appVersionOld");
|
|
399
|
+
sessionStorage.removeItem("requiredVersion");
|
|
400
|
+
sessionStorage.removeItem("appReloads");
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.error("Error cleaning up version storage:", err);
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
} catch (err) {
|
|
408
|
+
console.error("Error in version protection:", err);
|
|
289
409
|
}
|
|
290
|
-
}, [
|
|
410
|
+
}, [appVersion]);
|
|
291
411
|
|
|
292
412
|
return <></>;
|
|
293
413
|
}
|