authscape 1.0.762 → 1.0.763
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/index.js +849 -181
- package/package.json +3 -3
- package/src/components/AuthScapeApp.js +459 -38
- package/src/services/apiService.js +50 -150
- package/src/services/authService.js +13 -2
- package/src/services/signInValidator.js +33 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "authscape",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.763",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"js-file-download": "^0.4.12",
|
|
23
23
|
"react-data-table-component": "^7.5.2",
|
|
24
|
-
"react-dom": "^18.2.0"
|
|
24
|
+
"react-dom": "^18.2.0",
|
|
25
|
+
"react-toastify": "^9.1.3"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@babel/cli": "^7.27.0",
|
|
@@ -57,7 +58,6 @@
|
|
|
57
58
|
"react-device-detect": "^2.2.3",
|
|
58
59
|
"react-hook-form": "^7.50.1",
|
|
59
60
|
"react-microsoft-clarity": "^1.2.0",
|
|
60
|
-
"react-toastify": "^9.1.3",
|
|
61
61
|
"styled-components": "^5.3.6",
|
|
62
62
|
"use-places-autocomplete": "^4.0.0",
|
|
63
63
|
"zustand": "^4.5.2"
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect, useMemo } from "react";
|
|
2
|
-
import { ToastContainer, toast } from "react-toastify";
|
|
3
|
-
import { ThemeProvider } from "@mui/material/styles";
|
|
1
|
+
import React, { useState, useRef, useEffect, useMemo, createContext, useContext, useCallback } from "react";
|
|
2
|
+
import { ToastContainer, toast, Bounce, Slide, Zoom, Flip } from "react-toastify";
|
|
4
3
|
import Head from "next/head";
|
|
4
|
+
|
|
5
|
+
// Re-export toast and transitions so pages can import from authscape
|
|
6
|
+
export { toast, Bounce, Slide, Zoom, Flip };
|
|
5
7
|
import { useSearchParams, usePathname } from "next/navigation";
|
|
6
8
|
import axios from "axios";
|
|
7
9
|
import querystring from "query-string";
|
|
@@ -9,16 +11,415 @@ import Router from "next/router";
|
|
|
9
11
|
import GA4React from "ga-4-react";
|
|
10
12
|
import { create } from "zustand";
|
|
11
13
|
import { clarity } from "react-microsoft-clarity";
|
|
14
|
+
import { createTheme, ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
|
|
15
|
+
import CssBaseline from '@mui/material/CssBaseline';
|
|
16
|
+
import { HubConnectionBuilder, LogLevel, HttpTransportType } from '@microsoft/signalr';
|
|
17
|
+
import Cookies from 'js-cookie';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Cookie utility function
|
|
21
|
+
// ============================================================================
|
|
22
|
+
const setCookie = (name, value, options = {}) => {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
let cookieString = `${name}=${value};`;
|
|
25
|
+
if (options.maxAge) cookieString += `max-age=${options.maxAge};`;
|
|
26
|
+
if (options.path) cookieString += `path=${options.path};`;
|
|
27
|
+
if (options.domain) cookieString += `domain=${options.domain};`;
|
|
28
|
+
if (options.secure) cookieString += `secure;`;
|
|
29
|
+
document.cookie = cookieString;
|
|
30
|
+
resolve();
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Error Tracking Service
|
|
36
|
+
// ============================================================================
|
|
37
|
+
let errorTrackingSessionId = null;
|
|
38
|
+
let errorTrackingUserId = null;
|
|
39
|
+
let errorTrackingInitialized = false;
|
|
40
|
+
|
|
41
|
+
function generateGuid() {
|
|
42
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
43
|
+
const r = Math.random() * 16 | 0;
|
|
44
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
45
|
+
return v.toString(16);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getOrCreateSessionId() {
|
|
50
|
+
if (typeof window === 'undefined') return null;
|
|
51
|
+
|
|
52
|
+
let storedSessionId = sessionStorage.getItem('errorTrackingSessionId');
|
|
53
|
+
|
|
54
|
+
if (!storedSessionId) {
|
|
55
|
+
storedSessionId = sessionStorage.getItem('analyticsSessionId') || generateGuid();
|
|
56
|
+
sessionStorage.setItem('errorTrackingSessionId', storedSessionId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return storedSessionId;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function initializeErrorTracking(currentUser = null) {
|
|
63
|
+
if (currentUser && currentUser.id) {
|
|
64
|
+
errorTrackingUserId = currentUser.id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
errorTrackingSessionId = getOrCreateSessionId();
|
|
68
|
+
errorTrackingInitialized = true;
|
|
69
|
+
}
|
|
12
70
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
71
|
+
export async function logError(errorData) {
|
|
72
|
+
if (!errorTrackingSessionId && typeof window !== 'undefined') {
|
|
73
|
+
errorTrackingSessionId = getOrCreateSessionId();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const error = {
|
|
77
|
+
message: errorData.message || 'Unknown error',
|
|
78
|
+
errorType: errorData.errorType || 'JavaScriptError',
|
|
79
|
+
stackTrace: errorData.stackTrace || '',
|
|
80
|
+
url: errorData.url || (typeof window !== 'undefined' ? window.location.href : ''),
|
|
81
|
+
componentName: errorData.componentName || null,
|
|
82
|
+
userId: errorTrackingUserId || null,
|
|
83
|
+
sessionId: errorTrackingSessionId || null,
|
|
84
|
+
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : null,
|
|
85
|
+
ipAddress: '',
|
|
86
|
+
metadata: errorData.metadata || null
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const response = await module.exports.apiService().post('/ErrorTracking/LogError', error);
|
|
91
|
+
if (response && response.status !== 200) {
|
|
92
|
+
console.error('Error tracking API returned:', response.status);
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error('Failed to send error to tracking system:', err.message);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
16
98
|
|
|
17
|
-
|
|
99
|
+
export function setErrorTrackingUserId(newUserId) {
|
|
100
|
+
errorTrackingUserId = newUserId;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// AppThemeProvider
|
|
105
|
+
// ============================================================================
|
|
106
|
+
const ThemeContext = createContext();
|
|
107
|
+
|
|
108
|
+
export const useAppTheme = () => {
|
|
109
|
+
const context = useContext(ThemeContext);
|
|
110
|
+
if (!context) {
|
|
111
|
+
return { mode: 'light', toggleTheme: () => {} };
|
|
112
|
+
}
|
|
113
|
+
return context;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const AppThemeProvider = ({ children, customTheme }) => {
|
|
117
|
+
const [mode, setMode] = useState('light');
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (typeof window !== 'undefined') {
|
|
121
|
+
const savedMode = localStorage.getItem('themeMode');
|
|
122
|
+
if (savedMode) {
|
|
123
|
+
setMode(savedMode);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (typeof document !== 'undefined') {
|
|
130
|
+
document.documentElement.setAttribute('data-theme', mode);
|
|
131
|
+
}
|
|
132
|
+
}, [mode]);
|
|
133
|
+
|
|
134
|
+
const toggleTheme = () => {
|
|
135
|
+
const newMode = mode === 'light' ? 'dark' : 'light';
|
|
136
|
+
setMode(newMode);
|
|
137
|
+
if (typeof window !== 'undefined') {
|
|
138
|
+
localStorage.setItem('themeMode', newMode);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const theme = customTheme || createTheme({
|
|
143
|
+
palette: {
|
|
144
|
+
mode,
|
|
145
|
+
...(mode === 'light'
|
|
146
|
+
? {
|
|
147
|
+
primary: { main: '#0098e5', light: '#4db8ff', dark: '#006ba6' },
|
|
148
|
+
secondary: { main: '#44596e', light: '#6b7f94', dark: '#2d3d4f' },
|
|
149
|
+
background: { default: '#f5f8fa', paper: '#ffffff' },
|
|
150
|
+
text: { primary: '#1a202c', secondary: '#4a5568' },
|
|
151
|
+
divider: 'rgba(0, 0, 0, 0.12)',
|
|
152
|
+
}
|
|
153
|
+
: {
|
|
154
|
+
primary: { main: '#2196f3', light: '#42a5f5', dark: '#1976d2' },
|
|
155
|
+
secondary: { main: '#90caf9', light: '#bbdefb', dark: '#42a5f5' },
|
|
156
|
+
background: { default: '#121212', paper: '#1e1e1e' },
|
|
157
|
+
text: { primary: '#ffffff', secondary: '#b0b0b0' },
|
|
158
|
+
divider: 'rgba(255, 255, 255, 0.12)',
|
|
159
|
+
action: { hover: 'rgba(255, 255, 255, 0.08)', selected: 'rgba(255, 255, 255, 0.16)' },
|
|
160
|
+
}),
|
|
161
|
+
},
|
|
162
|
+
typography: { fontFamily: 'Poppins, sans-serif' },
|
|
163
|
+
components: {
|
|
164
|
+
MuiPaper: {
|
|
165
|
+
styleOverrides: {
|
|
166
|
+
root: {
|
|
167
|
+
backgroundImage: 'none',
|
|
168
|
+
...(mode === 'dark' && { backgroundColor: '#1e1e1e', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.4)' }),
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
MuiTextField: {
|
|
173
|
+
styleOverrides: {
|
|
174
|
+
root: {
|
|
175
|
+
'& .MuiOutlinedInput-root': {
|
|
176
|
+
...(mode === 'dark' && {
|
|
177
|
+
'& fieldset': { borderColor: 'rgba(255, 255, 255, 0.23)' },
|
|
178
|
+
'&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.4)' },
|
|
179
|
+
'&.Mui-focused fieldset': { borderColor: '#2196f3' },
|
|
180
|
+
}),
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
MuiChip: {
|
|
186
|
+
styleOverrides: {
|
|
187
|
+
root: { ...(mode === 'dark' && { borderColor: 'rgba(255, 255, 255, 0.23)' }) },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
MuiDivider: {
|
|
191
|
+
styleOverrides: {
|
|
192
|
+
root: { ...(mode === 'dark' && { borderColor: 'rgba(255, 255, 255, 0.12)' }) },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return React.createElement(
|
|
199
|
+
ThemeContext.Provider,
|
|
200
|
+
{ value: { mode, toggleTheme } },
|
|
201
|
+
React.createElement(
|
|
202
|
+
MuiThemeProvider,
|
|
203
|
+
{ theme },
|
|
204
|
+
React.createElement(CssBaseline),
|
|
205
|
+
children
|
|
206
|
+
)
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// NotificationProvider
|
|
212
|
+
// ============================================================================
|
|
213
|
+
const NotificationContext = createContext();
|
|
214
|
+
|
|
215
|
+
let globalConnection = null;
|
|
216
|
+
let globalUserId = null;
|
|
217
|
+
let globalIsInitialized = false;
|
|
218
|
+
|
|
219
|
+
export function useNotifications() {
|
|
220
|
+
const context = useContext(NotificationContext);
|
|
221
|
+
if (!context) {
|
|
222
|
+
return {
|
|
223
|
+
notifications: [],
|
|
224
|
+
unreadCount: 0,
|
|
225
|
+
isConnected: false,
|
|
226
|
+
markAsRead: () => {},
|
|
227
|
+
markAllAsRead: () => {},
|
|
228
|
+
deleteNotification: () => {},
|
|
229
|
+
clearAll: () => {},
|
|
230
|
+
refresh: () => {}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return context;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function NotificationProvider({ children, currentUser, apiService }) {
|
|
237
|
+
const [notifications, setNotifications] = useState([]);
|
|
238
|
+
const [unreadCount, setUnreadCount] = useState(0);
|
|
239
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
240
|
+
|
|
241
|
+
const markAsRead = useCallback(async (notificationId) => {
|
|
242
|
+
try {
|
|
243
|
+
await apiService().post('/Notification/MarkAsRead', { notificationId });
|
|
244
|
+
setNotifications(prev =>
|
|
245
|
+
prev.map(n => n.id === notificationId ? { ...n, isRead: true, readAt: new Date() } : n)
|
|
246
|
+
);
|
|
247
|
+
setUnreadCount(prev => Math.max(0, prev - 1));
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error('Failed to mark as read:', err);
|
|
250
|
+
}
|
|
251
|
+
}, [apiService]);
|
|
252
|
+
|
|
253
|
+
const markAllAsRead = useCallback(async () => {
|
|
254
|
+
try {
|
|
255
|
+
await apiService().post('/Notification/MarkAllAsRead');
|
|
256
|
+
setNotifications(prev => prev.map(n => ({ ...n, isRead: true, readAt: new Date() })));
|
|
257
|
+
setUnreadCount(0);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.error('Failed to mark all as read:', err);
|
|
260
|
+
}
|
|
261
|
+
}, [apiService]);
|
|
262
|
+
|
|
263
|
+
const deleteNotification = useCallback(async (notificationId) => {
|
|
264
|
+
try {
|
|
265
|
+
await apiService().delete(`/Notification/DeleteNotification?id=${notificationId}`);
|
|
266
|
+
setNotifications(prev => {
|
|
267
|
+
const notification = prev.find(n => n.id === notificationId);
|
|
268
|
+
if (notification && !notification.isRead) {
|
|
269
|
+
setUnreadCount(count => Math.max(0, count - 1));
|
|
270
|
+
}
|
|
271
|
+
return prev.filter(n => n.id !== notificationId);
|
|
272
|
+
});
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.error('Failed to delete notification:', err);
|
|
275
|
+
}
|
|
276
|
+
}, [apiService]);
|
|
277
|
+
|
|
278
|
+
const clearAll = useCallback(async () => {
|
|
279
|
+
try {
|
|
280
|
+
await apiService().delete('/Notification/ClearAllNotifications');
|
|
281
|
+
setNotifications([]);
|
|
282
|
+
setUnreadCount(0);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.error('Failed to clear notifications:', err);
|
|
285
|
+
}
|
|
286
|
+
}, [apiService]);
|
|
287
|
+
|
|
288
|
+
const fetchNotifications = useCallback(async () => {
|
|
289
|
+
try {
|
|
290
|
+
const [notifResponse, countResponse] = await Promise.all([
|
|
291
|
+
apiService().get('/Notification/GetNotifications?unreadOnly=false&take=50'),
|
|
292
|
+
apiService().get('/Notification/GetUnreadCount')
|
|
293
|
+
]);
|
|
294
|
+
if (notifResponse.status === 200) {
|
|
295
|
+
setNotifications(notifResponse.data);
|
|
296
|
+
}
|
|
297
|
+
if (countResponse.status === 200) {
|
|
298
|
+
setUnreadCount(countResponse.data.count);
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error('Failed to fetch notifications:', err);
|
|
302
|
+
}
|
|
303
|
+
}, [apiService]);
|
|
304
|
+
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
const userId = currentUser?.id;
|
|
307
|
+
if (!userId) return;
|
|
308
|
+
|
|
309
|
+
const fetchData = async () => {
|
|
310
|
+
try {
|
|
311
|
+
const [notifResponse, countResponse] = await Promise.all([
|
|
312
|
+
apiService().get('/Notification/GetNotifications?unreadOnly=false&take=50'),
|
|
313
|
+
apiService().get('/Notification/GetUnreadCount')
|
|
314
|
+
]);
|
|
315
|
+
if (notifResponse.status === 200) {
|
|
316
|
+
setNotifications(notifResponse.data);
|
|
317
|
+
}
|
|
318
|
+
if (countResponse.status === 200) {
|
|
319
|
+
setUnreadCount(countResponse.data.count);
|
|
320
|
+
}
|
|
321
|
+
} catch (err) {
|
|
322
|
+
console.error('Failed to fetch notifications:', err);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
if (globalIsInitialized && globalUserId === userId && globalConnection) {
|
|
327
|
+
setIsConnected(globalConnection.state === 'Connected');
|
|
328
|
+
fetchData();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (globalConnection && globalUserId !== userId) {
|
|
333
|
+
globalConnection.stop();
|
|
334
|
+
globalConnection = null;
|
|
335
|
+
globalIsInitialized = false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
globalUserId = userId;
|
|
339
|
+
globalIsInitialized = true;
|
|
340
|
+
|
|
341
|
+
const apiBaseUrl = process.env.apiUri || 'http://localhost:54218';
|
|
342
|
+
const hubUrl = `${apiBaseUrl}/notifications`;
|
|
343
|
+
|
|
344
|
+
// Get access token for SignalR authentication
|
|
345
|
+
const accessToken = Cookies.get('access_token') || '';
|
|
346
|
+
|
|
347
|
+
const connection = new HubConnectionBuilder()
|
|
348
|
+
.withUrl(hubUrl, {
|
|
349
|
+
accessTokenFactory: () => accessToken,
|
|
350
|
+
transport: HttpTransportType.WebSockets | HttpTransportType.LongPolling
|
|
351
|
+
})
|
|
352
|
+
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
|
|
353
|
+
.configureLogging(LogLevel.Warning)
|
|
354
|
+
.build();
|
|
355
|
+
|
|
356
|
+
globalConnection = connection;
|
|
357
|
+
|
|
358
|
+
connection.on('OnNotificationReceived', (notification) => {
|
|
359
|
+
setNotifications(prev => [notification, ...prev]);
|
|
360
|
+
setUnreadCount(prev => prev + 1);
|
|
361
|
+
|
|
362
|
+
const description = notification.message || notification.categoryName || '';
|
|
363
|
+
toast.info(
|
|
364
|
+
React.createElement('div', null,
|
|
365
|
+
React.createElement('strong', null, notification.title),
|
|
366
|
+
description && React.createElement('div', { style: { fontSize: '0.9em', marginTop: '4px' } }, description)
|
|
367
|
+
),
|
|
368
|
+
{
|
|
369
|
+
onClick: () => {
|
|
370
|
+
if (notification.linkUrl) {
|
|
371
|
+
window.location.href = notification.linkUrl;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
connection.onreconnecting(() => setIsConnected(false));
|
|
379
|
+
connection.onreconnected(() => { setIsConnected(true); fetchData(); });
|
|
380
|
+
connection.onclose(() => setIsConnected(false));
|
|
381
|
+
|
|
382
|
+
const startConnection = async () => {
|
|
383
|
+
try {
|
|
384
|
+
await connection.start();
|
|
385
|
+
setIsConnected(true);
|
|
386
|
+
await connection.invoke('JoinUserNotifications', userId);
|
|
387
|
+
if (currentUser?.companyId) {
|
|
388
|
+
await connection.invoke('JoinCompanyNotifications', currentUser.companyId);
|
|
389
|
+
}
|
|
390
|
+
if (currentUser?.locationId) {
|
|
391
|
+
await connection.invoke('JoinLocationNotifications', currentUser.locationId);
|
|
392
|
+
}
|
|
393
|
+
await fetchData();
|
|
394
|
+
} catch (err) {
|
|
395
|
+
console.error('Failed to connect to NotificationHub:', err.message);
|
|
396
|
+
await fetchData();
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
startConnection();
|
|
401
|
+
}, [currentUser?.id, currentUser?.companyId, currentUser?.locationId, apiService]);
|
|
402
|
+
|
|
403
|
+
const value = {
|
|
404
|
+
notifications,
|
|
405
|
+
unreadCount,
|
|
406
|
+
isConnected,
|
|
407
|
+
markAsRead,
|
|
408
|
+
markAllAsRead,
|
|
409
|
+
deleteNotification,
|
|
410
|
+
clearAll,
|
|
411
|
+
refresh: fetchNotifications
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
return React.createElement(NotificationContext.Provider, { value }, children);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ============================================================================
|
|
418
|
+
// User Helpers
|
|
419
|
+
// ============================================================================
|
|
18
420
|
function ensureUserHelpers(u) {
|
|
19
421
|
if (!u || typeof u !== "object") return u;
|
|
20
422
|
|
|
21
|
-
// Avoid redefining on every call
|
|
22
423
|
if (typeof u.hasRole === "function" &&
|
|
23
424
|
typeof u.hasRoleId === "function" &&
|
|
24
425
|
typeof u.hasPermission === "function") {
|
|
@@ -28,7 +429,6 @@ function ensureUserHelpers(u) {
|
|
|
28
429
|
const rolesArr = Array.isArray(u.roles) ? u.roles : [];
|
|
29
430
|
const permsArr = Array.isArray(u.permissions) ? u.permissions : [];
|
|
30
431
|
|
|
31
|
-
// defineProperty keeps them non-enumerable
|
|
32
432
|
Object.defineProperty(u, "hasRole", {
|
|
33
433
|
value: function hasRole(name) {
|
|
34
434
|
if (!name) return false;
|
|
@@ -56,16 +456,23 @@ function ensureUserHelpers(u) {
|
|
|
56
456
|
return u;
|
|
57
457
|
}
|
|
58
458
|
|
|
459
|
+
// ============================================================================
|
|
460
|
+
// AuthScapeApp Component
|
|
461
|
+
// ============================================================================
|
|
59
462
|
export function AuthScapeApp({
|
|
60
463
|
Component,
|
|
61
464
|
layout,
|
|
62
465
|
loadingLayout,
|
|
63
466
|
signInLoadingComponent,
|
|
64
467
|
pageProps,
|
|
65
|
-
muiTheme =
|
|
468
|
+
muiTheme = null,
|
|
66
469
|
store = {},
|
|
67
470
|
enforceLoggedIn = false,
|
|
68
471
|
enableAuth = true,
|
|
472
|
+
enableNotifications = true,
|
|
473
|
+
enableErrorTracking = true,
|
|
474
|
+
toastConfig = {},
|
|
475
|
+
onUserLoaded = null,
|
|
69
476
|
}) {
|
|
70
477
|
const [frontEndLoadedState, setFrontEndLoadedState] = useState(false);
|
|
71
478
|
const [isLoadingShow, setIsLoadingShow] = useState(false);
|
|
@@ -76,12 +483,12 @@ export function AuthScapeApp({
|
|
|
76
483
|
const signedInUser = useRef(null);
|
|
77
484
|
const queryCodeUsed = useRef(null);
|
|
78
485
|
const ga4React = useRef(null);
|
|
486
|
+
const errorTrackingInitializedRef = useRef(false);
|
|
79
487
|
|
|
80
488
|
const searchParams = useSearchParams();
|
|
81
489
|
const queryCode = searchParams.get("code");
|
|
82
490
|
const pathname = usePathname();
|
|
83
491
|
|
|
84
|
-
// ----- PKCE Sign-in (browser-only) -----
|
|
85
492
|
const signInValidator = async (codeFromQuery) => {
|
|
86
493
|
if (queryCodeUsed.current === codeFromQuery) return;
|
|
87
494
|
queryCodeUsed.current = codeFromQuery;
|
|
@@ -92,7 +499,6 @@ export function AuthScapeApp({
|
|
|
92
499
|
|
|
93
500
|
const codeVerifier = window.localStorage.getItem("verifier");
|
|
94
501
|
if (!codeFromQuery || !codeVerifier) {
|
|
95
|
-
// No code or verifier - redirect to login
|
|
96
502
|
window.localStorage.clear();
|
|
97
503
|
module.exports.authService().login();
|
|
98
504
|
return;
|
|
@@ -120,7 +526,6 @@ export function AuthScapeApp({
|
|
|
120
526
|
|
|
121
527
|
window.localStorage.removeItem("verifier");
|
|
122
528
|
|
|
123
|
-
// NOTE: replace setCookie below with your implementation if different
|
|
124
529
|
await setCookie("access_token", response.data.access_token, {
|
|
125
530
|
maxAge: 60 * 60 * 24 * 365,
|
|
126
531
|
path: "/",
|
|
@@ -143,19 +548,15 @@ export function AuthScapeApp({
|
|
|
143
548
|
const redirectUri = window.localStorage.getItem("redirectUri") || "/";
|
|
144
549
|
window.localStorage.clear();
|
|
145
550
|
|
|
146
|
-
// Navigate to the redirect URI - use window.location for a clean page load
|
|
147
|
-
// This ensures all state is properly initialized on the target page
|
|
148
551
|
window.location.href = redirectUri;
|
|
149
552
|
} catch (exp) {
|
|
150
553
|
console.error("PKCE sign-in failed", exp);
|
|
151
|
-
// Invalid code - clear storage and redirect to login
|
|
152
554
|
window.localStorage.clear();
|
|
153
555
|
setIsSigningIn(false);
|
|
154
556
|
module.exports.authService().login();
|
|
155
557
|
}
|
|
156
558
|
};
|
|
157
559
|
|
|
158
|
-
// ----- GA + Clarity -----
|
|
159
560
|
async function initGA(G) {
|
|
160
561
|
if (typeof window !== "undefined" && !GA4React.isInitialized() && G) {
|
|
161
562
|
ga4React.current = new GA4React(G, { debug_mode: !process.env.production });
|
|
@@ -169,7 +570,6 @@ export function AuthScapeApp({
|
|
|
169
570
|
|
|
170
571
|
const logEvent = (category, action, label) => {
|
|
171
572
|
if (ga4React.current) ga4React.current.event(action, label, category);
|
|
172
|
-
// your DB analytics can go here if desired
|
|
173
573
|
};
|
|
174
574
|
|
|
175
575
|
const databaseDrivenPageView = (pathName) => {
|
|
@@ -179,7 +579,6 @@ export function AuthScapeApp({
|
|
|
179
579
|
|
|
180
580
|
const host = window.location.protocol + "//" + window.location.host;
|
|
181
581
|
|
|
182
|
-
// Use module.exports to access sibling exports in babel bundle
|
|
183
582
|
module.exports.apiService().post("/Analytics/PageView", {
|
|
184
583
|
userId: signedInUser.current?.id,
|
|
185
584
|
locationId: signedInUser.current?.locationId,
|
|
@@ -189,7 +588,6 @@ export function AuthScapeApp({
|
|
|
189
588
|
});
|
|
190
589
|
};
|
|
191
590
|
|
|
192
|
-
// ----- Auth init (runs once) -----
|
|
193
591
|
useEffect(() => {
|
|
194
592
|
if (queryCode) {
|
|
195
593
|
signInValidator(queryCode);
|
|
@@ -200,13 +598,21 @@ export function AuthScapeApp({
|
|
|
200
598
|
loadingAuth.current = true;
|
|
201
599
|
|
|
202
600
|
if (enableAuth) {
|
|
203
|
-
// Use module.exports to access sibling exports in babel bundle
|
|
204
601
|
module.exports.apiService().GetCurrentUser().then((usr) => {
|
|
205
602
|
signedInUser.current = ensureUserHelpers(usr);
|
|
206
603
|
setSignedInUserState(signedInUser.current);
|
|
207
604
|
setFrontEndLoadedState(true);
|
|
208
|
-
|
|
209
|
-
//
|
|
605
|
+
|
|
606
|
+
// Initialize error tracking with user info
|
|
607
|
+
if (enableErrorTracking && usr && !errorTrackingInitializedRef.current) {
|
|
608
|
+
initializeErrorTracking(usr);
|
|
609
|
+
errorTrackingInitializedRef.current = true;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (onUserLoaded && usr) {
|
|
613
|
+
onUserLoaded(usr);
|
|
614
|
+
}
|
|
615
|
+
}).catch((err) => {
|
|
210
616
|
signedInUser.current = null;
|
|
211
617
|
setSignedInUserState(null);
|
|
212
618
|
setFrontEndLoadedState(true);
|
|
@@ -215,9 +621,8 @@ export function AuthScapeApp({
|
|
|
215
621
|
setFrontEndLoadedState(true);
|
|
216
622
|
}
|
|
217
623
|
}
|
|
218
|
-
}, [queryCode, enableAuth]);
|
|
624
|
+
}, [queryCode, enableAuth, enableErrorTracking]);
|
|
219
625
|
|
|
220
|
-
// ----- Analytics init -----
|
|
221
626
|
useEffect(() => {
|
|
222
627
|
if (!frontEndLoadedState || typeof window === "undefined") return;
|
|
223
628
|
|
|
@@ -243,7 +648,6 @@ export function AuthScapeApp({
|
|
|
243
648
|
return () => Router.events.off("routeChangeComplete", handler);
|
|
244
649
|
}, [frontEndLoadedState, pageProps.googleAnalytics4Code, pageProps.microsoftClarityCode]);
|
|
245
650
|
|
|
246
|
-
// ----- Enforce login (client) -----
|
|
247
651
|
useEffect(() => {
|
|
248
652
|
if (
|
|
249
653
|
enforceLoggedIn &&
|
|
@@ -251,19 +655,14 @@ export function AuthScapeApp({
|
|
|
251
655
|
frontEndLoadedState &&
|
|
252
656
|
!signedInUserState
|
|
253
657
|
) {
|
|
254
|
-
// Use module.exports to access sibling exports in babel bundle
|
|
255
658
|
module.exports.authService().login();
|
|
256
659
|
}
|
|
257
660
|
}, [signedInUserState, enforceLoggedIn, frontEndLoadedState, pathname]);
|
|
258
661
|
|
|
259
|
-
// Stable getter for current user (with helpers)
|
|
260
662
|
const currentUser = useMemo(() => ensureUserHelpers(signedInUser.current), [signedInUserState]);
|
|
261
663
|
|
|
262
664
|
const useStore = create(() => store);
|
|
263
665
|
|
|
264
|
-
// ----- Render (SSR-safe; always output page so <title> is visible) -----
|
|
265
|
-
|
|
266
|
-
// Default sign-in loading component if none provided
|
|
267
666
|
const defaultSignInLoading = (
|
|
268
667
|
<div style={{
|
|
269
668
|
display: 'flex',
|
|
@@ -292,7 +691,6 @@ export function AuthScapeApp({
|
|
|
292
691
|
</div>
|
|
293
692
|
);
|
|
294
693
|
|
|
295
|
-
// Show loading screen when signing in
|
|
296
694
|
if (isSigningIn) {
|
|
297
695
|
return (
|
|
298
696
|
<>
|
|
@@ -302,13 +700,27 @@ export function AuthScapeApp({
|
|
|
302
700
|
content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86"
|
|
303
701
|
/>
|
|
304
702
|
</Head>
|
|
305
|
-
<
|
|
703
|
+
<AppThemeProvider customTheme={muiTheme}>
|
|
306
704
|
{signInLoadingComponent || defaultSignInLoading}
|
|
307
|
-
</
|
|
705
|
+
</AppThemeProvider>
|
|
308
706
|
</>
|
|
309
707
|
);
|
|
310
708
|
}
|
|
311
709
|
|
|
710
|
+
const defaultToastConfig = {
|
|
711
|
+
position: "top-right",
|
|
712
|
+
autoClose: 3000,
|
|
713
|
+
hideProgressBar: false,
|
|
714
|
+
newestOnTop: true,
|
|
715
|
+
closeOnClick: true,
|
|
716
|
+
rtl: false,
|
|
717
|
+
pauseOnFocusLoss: true,
|
|
718
|
+
draggable: true,
|
|
719
|
+
pauseOnHover: true,
|
|
720
|
+
theme: "colored",
|
|
721
|
+
...toastConfig
|
|
722
|
+
};
|
|
723
|
+
|
|
312
724
|
const pageContent = layout
|
|
313
725
|
? layout({
|
|
314
726
|
children: (
|
|
@@ -341,6 +753,15 @@ export function AuthScapeApp({
|
|
|
341
753
|
/>
|
|
342
754
|
);
|
|
343
755
|
|
|
756
|
+
const wrappedContent = enableNotifications && currentUser ? (
|
|
757
|
+
<NotificationProvider
|
|
758
|
+
currentUser={currentUser}
|
|
759
|
+
apiService={module.exports.apiService}
|
|
760
|
+
>
|
|
761
|
+
{pageContent}
|
|
762
|
+
</NotificationProvider>
|
|
763
|
+
) : pageContent;
|
|
764
|
+
|
|
344
765
|
return (
|
|
345
766
|
<>
|
|
346
767
|
<Head>
|
|
@@ -350,11 +771,11 @@ export function AuthScapeApp({
|
|
|
350
771
|
/>
|
|
351
772
|
</Head>
|
|
352
773
|
|
|
353
|
-
<
|
|
354
|
-
{
|
|
355
|
-
|
|
356
|
-
</ThemeProvider>
|
|
774
|
+
<AppThemeProvider customTheme={muiTheme}>
|
|
775
|
+
{wrappedContent}
|
|
776
|
+
</AppThemeProvider>
|
|
357
777
|
|
|
778
|
+
<ToastContainer {...defaultToastConfig} />
|
|
358
779
|
{loadingLayout && loadingLayout(isLoadingShow)}
|
|
359
780
|
</>
|
|
360
781
|
);
|