authscape 1.0.760 → 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 +855 -211
- package/package.json +3 -3
- package/src/components/AuthScapeApp.js +462 -56
- package/src/services/apiService.js +50 -150
- package/src/services/authService.js +13 -2
- package/src/services/signInValidator.js +33 -5
|
@@ -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
|
+
}
|
|
70
|
+
|
|
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
|
+
}
|
|
98
|
+
|
|
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]);
|
|
12
133
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
};
|
|
16
141
|
|
|
17
|
-
|
|
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,15 +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
|
-
// Check if we're on the signin-oidc page
|
|
85
|
-
const isOnSignInPage = pathname === "/signin-oidc";
|
|
86
|
-
|
|
87
|
-
// ----- PKCE Sign-in (browser-only) -----
|
|
88
492
|
const signInValidator = async (codeFromQuery) => {
|
|
89
493
|
if (queryCodeUsed.current === codeFromQuery) return;
|
|
90
494
|
queryCodeUsed.current = codeFromQuery;
|
|
@@ -95,7 +499,6 @@ export function AuthScapeApp({
|
|
|
95
499
|
|
|
96
500
|
const codeVerifier = window.localStorage.getItem("verifier");
|
|
97
501
|
if (!codeFromQuery || !codeVerifier) {
|
|
98
|
-
// No code or verifier - redirect to login
|
|
99
502
|
window.localStorage.clear();
|
|
100
503
|
module.exports.authService().login();
|
|
101
504
|
return;
|
|
@@ -123,7 +526,6 @@ export function AuthScapeApp({
|
|
|
123
526
|
|
|
124
527
|
window.localStorage.removeItem("verifier");
|
|
125
528
|
|
|
126
|
-
// NOTE: replace setCookie below with your implementation if different
|
|
127
529
|
await setCookie("access_token", response.data.access_token, {
|
|
128
530
|
maxAge: 60 * 60 * 24 * 365,
|
|
129
531
|
path: "/",
|
|
@@ -146,31 +548,15 @@ export function AuthScapeApp({
|
|
|
146
548
|
const redirectUri = window.localStorage.getItem("redirectUri") || "/";
|
|
147
549
|
window.localStorage.clear();
|
|
148
550
|
|
|
149
|
-
|
|
150
|
-
window.history.replaceState({}, "", redirectUri);
|
|
151
|
-
|
|
152
|
-
// Now load the current user and update state
|
|
153
|
-
try {
|
|
154
|
-
const usr = await module.exports.apiService().GetCurrentUser();
|
|
155
|
-
signedInUser.current = ensureUserHelpers(usr);
|
|
156
|
-
setSignedInUserState(signedInUser.current);
|
|
157
|
-
setFrontEndLoadedState(true);
|
|
158
|
-
|
|
159
|
-
// Trigger a soft navigation using Next.js router
|
|
160
|
-
Router.replace(redirectUri, undefined, { shallow: false });
|
|
161
|
-
} catch (userErr) {
|
|
162
|
-
// If we can't get user, still navigate
|
|
163
|
-
Router.replace(redirectUri);
|
|
164
|
-
}
|
|
551
|
+
window.location.href = redirectUri;
|
|
165
552
|
} catch (exp) {
|
|
166
553
|
console.error("PKCE sign-in failed", exp);
|
|
167
|
-
// Invalid code - clear storage and redirect to login
|
|
168
554
|
window.localStorage.clear();
|
|
555
|
+
setIsSigningIn(false);
|
|
169
556
|
module.exports.authService().login();
|
|
170
557
|
}
|
|
171
558
|
};
|
|
172
559
|
|
|
173
|
-
// ----- GA + Clarity -----
|
|
174
560
|
async function initGA(G) {
|
|
175
561
|
if (typeof window !== "undefined" && !GA4React.isInitialized() && G) {
|
|
176
562
|
ga4React.current = new GA4React(G, { debug_mode: !process.env.production });
|
|
@@ -184,7 +570,6 @@ export function AuthScapeApp({
|
|
|
184
570
|
|
|
185
571
|
const logEvent = (category, action, label) => {
|
|
186
572
|
if (ga4React.current) ga4React.current.event(action, label, category);
|
|
187
|
-
// your DB analytics can go here if desired
|
|
188
573
|
};
|
|
189
574
|
|
|
190
575
|
const databaseDrivenPageView = (pathName) => {
|
|
@@ -194,7 +579,6 @@ export function AuthScapeApp({
|
|
|
194
579
|
|
|
195
580
|
const host = window.location.protocol + "//" + window.location.host;
|
|
196
581
|
|
|
197
|
-
// Use module.exports to access sibling exports in babel bundle
|
|
198
582
|
module.exports.apiService().post("/Analytics/PageView", {
|
|
199
583
|
userId: signedInUser.current?.id,
|
|
200
584
|
locationId: signedInUser.current?.locationId,
|
|
@@ -204,7 +588,6 @@ export function AuthScapeApp({
|
|
|
204
588
|
});
|
|
205
589
|
};
|
|
206
590
|
|
|
207
|
-
// ----- Auth init (runs once) -----
|
|
208
591
|
useEffect(() => {
|
|
209
592
|
if (queryCode) {
|
|
210
593
|
signInValidator(queryCode);
|
|
@@ -215,13 +598,21 @@ export function AuthScapeApp({
|
|
|
215
598
|
loadingAuth.current = true;
|
|
216
599
|
|
|
217
600
|
if (enableAuth) {
|
|
218
|
-
// Use module.exports to access sibling exports in babel bundle
|
|
219
601
|
module.exports.apiService().GetCurrentUser().then((usr) => {
|
|
220
602
|
signedInUser.current = ensureUserHelpers(usr);
|
|
221
603
|
setSignedInUserState(signedInUser.current);
|
|
222
604
|
setFrontEndLoadedState(true);
|
|
223
|
-
|
|
224
|
-
//
|
|
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) => {
|
|
225
616
|
signedInUser.current = null;
|
|
226
617
|
setSignedInUserState(null);
|
|
227
618
|
setFrontEndLoadedState(true);
|
|
@@ -230,9 +621,8 @@ export function AuthScapeApp({
|
|
|
230
621
|
setFrontEndLoadedState(true);
|
|
231
622
|
}
|
|
232
623
|
}
|
|
233
|
-
}, [queryCode, enableAuth]);
|
|
624
|
+
}, [queryCode, enableAuth, enableErrorTracking]);
|
|
234
625
|
|
|
235
|
-
// ----- Analytics init -----
|
|
236
626
|
useEffect(() => {
|
|
237
627
|
if (!frontEndLoadedState || typeof window === "undefined") return;
|
|
238
628
|
|
|
@@ -258,7 +648,6 @@ export function AuthScapeApp({
|
|
|
258
648
|
return () => Router.events.off("routeChangeComplete", handler);
|
|
259
649
|
}, [frontEndLoadedState, pageProps.googleAnalytics4Code, pageProps.microsoftClarityCode]);
|
|
260
650
|
|
|
261
|
-
// ----- Enforce login (client) -----
|
|
262
651
|
useEffect(() => {
|
|
263
652
|
if (
|
|
264
653
|
enforceLoggedIn &&
|
|
@@ -266,19 +655,14 @@ export function AuthScapeApp({
|
|
|
266
655
|
frontEndLoadedState &&
|
|
267
656
|
!signedInUserState
|
|
268
657
|
) {
|
|
269
|
-
// Use module.exports to access sibling exports in babel bundle
|
|
270
658
|
module.exports.authService().login();
|
|
271
659
|
}
|
|
272
660
|
}, [signedInUserState, enforceLoggedIn, frontEndLoadedState, pathname]);
|
|
273
661
|
|
|
274
|
-
// Stable getter for current user (with helpers)
|
|
275
662
|
const currentUser = useMemo(() => ensureUserHelpers(signedInUser.current), [signedInUserState]);
|
|
276
663
|
|
|
277
664
|
const useStore = create(() => store);
|
|
278
665
|
|
|
279
|
-
// ----- Render (SSR-safe; always output page so <title> is visible) -----
|
|
280
|
-
|
|
281
|
-
// Default sign-in loading component if none provided
|
|
282
666
|
const defaultSignInLoading = (
|
|
283
667
|
<div style={{
|
|
284
668
|
display: 'flex',
|
|
@@ -307,8 +691,7 @@ export function AuthScapeApp({
|
|
|
307
691
|
</div>
|
|
308
692
|
);
|
|
309
693
|
|
|
310
|
-
|
|
311
|
-
if (isOnSignInPage || isSigningIn) {
|
|
694
|
+
if (isSigningIn) {
|
|
312
695
|
return (
|
|
313
696
|
<>
|
|
314
697
|
<Head>
|
|
@@ -317,13 +700,27 @@ export function AuthScapeApp({
|
|
|
317
700
|
content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86"
|
|
318
701
|
/>
|
|
319
702
|
</Head>
|
|
320
|
-
<
|
|
703
|
+
<AppThemeProvider customTheme={muiTheme}>
|
|
321
704
|
{signInLoadingComponent || defaultSignInLoading}
|
|
322
|
-
</
|
|
705
|
+
</AppThemeProvider>
|
|
323
706
|
</>
|
|
324
707
|
);
|
|
325
708
|
}
|
|
326
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
|
+
|
|
327
724
|
const pageContent = layout
|
|
328
725
|
? layout({
|
|
329
726
|
children: (
|
|
@@ -356,6 +753,15 @@ export function AuthScapeApp({
|
|
|
356
753
|
/>
|
|
357
754
|
);
|
|
358
755
|
|
|
756
|
+
const wrappedContent = enableNotifications && currentUser ? (
|
|
757
|
+
<NotificationProvider
|
|
758
|
+
currentUser={currentUser}
|
|
759
|
+
apiService={module.exports.apiService}
|
|
760
|
+
>
|
|
761
|
+
{pageContent}
|
|
762
|
+
</NotificationProvider>
|
|
763
|
+
) : pageContent;
|
|
764
|
+
|
|
359
765
|
return (
|
|
360
766
|
<>
|
|
361
767
|
<Head>
|
|
@@ -365,11 +771,11 @@ export function AuthScapeApp({
|
|
|
365
771
|
/>
|
|
366
772
|
</Head>
|
|
367
773
|
|
|
368
|
-
<
|
|
369
|
-
{
|
|
370
|
-
|
|
371
|
-
</ThemeProvider>
|
|
774
|
+
<AppThemeProvider customTheme={muiTheme}>
|
|
775
|
+
{wrappedContent}
|
|
776
|
+
</AppThemeProvider>
|
|
372
777
|
|
|
778
|
+
<ToastContainer {...defaultToastConfig} />
|
|
373
779
|
{loadingLayout && loadingLayout(isLoadingShow)}
|
|
374
780
|
</>
|
|
375
781
|
);
|