@visns-studio/visns-components 5.5.5 → 5.5.7

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.
@@ -14,10 +14,13 @@ import 'react-toastify/dist/ReactToastify.css';
14
14
 
15
15
  import CustomFetch from '../Fetch';
16
16
  import GenericMain from './GenericMain';
17
+ import GenericClientPortal from './GenericClientPortal';
17
18
  import Login from '../auth/Login';
18
19
  import Reset from '../auth/Reset';
19
20
  import Verify from '../auth/Verify';
20
21
  import TwoFactorAuth from '../auth/TwoFactorAuth';
22
+ import ClientLogin from '../auth/ClientLogin';
23
+ import ClientOTPVerify from '../auth/ClientOTPVerify';
21
24
 
22
25
  const ProtectedRoute = ({ user, loading, redirectPath = '/login' }) => {
23
26
  const location = useLocation();
@@ -33,6 +36,24 @@ const ProtectedRoute = ({ user, loading, redirectPath = '/login' }) => {
33
36
  );
34
37
  };
35
38
 
39
+ const ClientProtectedRoute = ({
40
+ clientUser,
41
+ loading,
42
+ redirectPath = '/client/login',
43
+ }) => {
44
+ const location = useLocation();
45
+
46
+ return loading ? null : !clientUser || !clientUser.id ? (
47
+ <Navigate
48
+ to={redirectPath}
49
+ replace
50
+ state={{ previousUrl: location.pathname }}
51
+ />
52
+ ) : (
53
+ <Outlet />
54
+ );
55
+ };
56
+
36
57
  const GenericAuth = ({
37
58
  layout = 'full',
38
59
  loginBg,
@@ -43,12 +64,16 @@ const GenericAuth = ({
43
64
  routeConfig,
44
65
  themeType = 'primary',
45
66
  enforce2FA = false, // Default to not enforcing 2FA
67
+ clientPortalConfig = null, // Configuration for client portal with component and URLs
46
68
  }) => {
47
69
  const navigate = useNavigate();
48
70
  const location = useLocation();
49
71
  const [userProfile, setUserProfile] = useState({});
72
+ const [clientProfile, setClientProfile] = useState({});
50
73
  const [isLoading, setIsLoading] = useState(true);
74
+ const [isClientLoading, setIsClientLoading] = useState(true);
51
75
  const [isAuthenticated, setIsAuthenticated] = useState(false);
76
+ const [isClientAuthenticated, setIsClientAuthenticated] = useState(false);
52
77
 
53
78
  const updateAuthStatus = async () => {
54
79
  try {
@@ -74,10 +99,68 @@ const GenericAuth = ({
74
99
  }
75
100
  };
76
101
 
102
+ const updateClientAuthStatus = async () => {
103
+ // Skip if client portal config is not provided
104
+ if (!clientPortalConfig) {
105
+ setIsClientLoading(false);
106
+ return;
107
+ }
108
+
109
+ // Get the profile URL from config or use default
110
+ const profileUrl =
111
+ clientPortalConfig.urls?.profile || '/client/profile';
112
+
113
+ try {
114
+ const result = await CustomFetch(profileUrl, 'GET');
115
+
116
+ if (
117
+ result.data &&
118
+ result.data[clientPortalConfig.model] &&
119
+ result.data[clientPortalConfig.model].id
120
+ ) {
121
+ setClientProfile(result.data[clientPortalConfig.model]);
122
+ setIsClientAuthenticated(true);
123
+ } else {
124
+ setIsClientAuthenticated(false);
125
+ }
126
+ setIsClientLoading(false);
127
+
128
+ // Get login and verify URLs from config
129
+ const loginPath = '/client/login';
130
+ const verifyPath = '/client/verify';
131
+
132
+ // If on client login page but already authenticated, redirect to portal
133
+ if (
134
+ isClientAuthenticated &&
135
+ (location.pathname === loginPath ||
136
+ location.pathname === verifyPath)
137
+ ) {
138
+ navigate('/client/portal', { replace: true });
139
+ }
140
+ } catch (error) {
141
+ setIsClientLoading(false);
142
+ setIsClientAuthenticated(false);
143
+
144
+ // Get login path
145
+ const loginPath = '/client/login';
146
+
147
+ // Only redirect if currently on a client portal page
148
+ if (location.pathname.startsWith('/client/portal')) {
149
+ navigate(loginPath, {
150
+ state: { previousUrl: location.pathname },
151
+ });
152
+ }
153
+ }
154
+ };
155
+
77
156
  useEffect(() => {
78
157
  updateAuthStatus();
79
158
  }, [isAuthenticated]);
80
159
 
160
+ useEffect(() => {
161
+ updateClientAuthStatus();
162
+ }, [isClientAuthenticated, clientPortalConfig, location.pathname]);
163
+
81
164
  return (
82
165
  <>
83
166
  <Routes>
@@ -93,6 +176,7 @@ const GenericAuth = ({
93
176
  />
94
177
  }
95
178
  />
179
+ {/* Client Portal Routes */}
96
180
  <Route
97
181
  path="reset"
98
182
  element={
@@ -124,6 +208,73 @@ const GenericAuth = ({
124
208
  />
125
209
  }
126
210
  />
211
+ {/* Client Portal Routes and Protected Routes */}
212
+ {clientPortalConfig && (
213
+ <Route path="client">
214
+ {/* Client Auth Routes */}
215
+ <Route
216
+ path="login"
217
+ element={
218
+ <ClientLoginComponent
219
+ loginBg={loginBg}
220
+ logo={logo}
221
+ setIsAuthenticated={
222
+ setIsClientAuthenticated
223
+ }
224
+ setUserProfile={setClientProfile}
225
+ urls={clientPortalConfig.urls}
226
+ />
227
+ }
228
+ />
229
+ <Route
230
+ path="verify"
231
+ element={
232
+ <ClientOTPVerifyComponent
233
+ loginBg={loginBg}
234
+ logo={logo}
235
+ setIsAuthenticated={
236
+ setIsClientAuthenticated
237
+ }
238
+ setUserProfile={setClientProfile}
239
+ urls={clientPortalConfig.urls}
240
+ />
241
+ }
242
+ />
243
+
244
+ {/* Client Portal Protected Route */}
245
+ {clientPortalConfig.component && (
246
+ <Route
247
+ element={
248
+ <ClientProtectedRoute
249
+ clientUser={clientProfile}
250
+ loading={isClientLoading}
251
+ />
252
+ }
253
+ >
254
+ <Route
255
+ path="portal/*"
256
+ element={
257
+ <GenericClientPortal
258
+ clientProfile={clientProfile}
259
+ setClientProfile={setClientProfile}
260
+ setClientAuth={
261
+ setIsClientAuthenticated
262
+ }
263
+ logo={headerLogo || logo}
264
+ portalConfig={clientPortalConfig}
265
+ themeType={themeType}
266
+ urls={clientPortalConfig.urls}
267
+ >
268
+ {clientPortalConfig.component}
269
+ </GenericClientPortal>
270
+ }
271
+ />
272
+ </Route>
273
+ )}
274
+ </Route>
275
+ )}
276
+
277
+ {/* Main Admin Routes */}
127
278
  <Route
128
279
  element={
129
280
  <ProtectedRoute
@@ -199,4 +350,28 @@ const TwoFactorAuthComponent = React.memo(
199
350
  )
200
351
  );
201
352
 
353
+ const ClientLoginComponent = React.memo(
354
+ ({ loginBg, logo, setIsAuthenticated, setUserProfile, urls }) => (
355
+ <ClientLogin
356
+ loginBg={loginBg}
357
+ logo={logo}
358
+ setSystemAuth={setIsAuthenticated}
359
+ setUserProfile={setUserProfile}
360
+ urls={urls}
361
+ />
362
+ )
363
+ );
364
+
365
+ const ClientOTPVerifyComponent = React.memo(
366
+ ({ loginBg, logo, setIsAuthenticated, setUserProfile, urls }) => (
367
+ <ClientOTPVerify
368
+ loginBg={loginBg}
369
+ logo={logo}
370
+ setSystemAuth={setIsAuthenticated}
371
+ setUserProfile={setUserProfile}
372
+ urls={urls}
373
+ />
374
+ )
375
+ );
376
+
202
377
  export default GenericAuth;
@@ -0,0 +1,165 @@
1
+ import React from 'react';
2
+ import { Routes, Route, useNavigate } from 'react-router-dom';
3
+ import { toast } from 'react-toastify';
4
+ import { SignOut } from 'akar-icons';
5
+
6
+ import CustomFetch from '../Fetch';
7
+
8
+ import styles from './styles/GenericClientPortal.module.scss';
9
+
10
+ /**
11
+ * GenericClientPortal - A wrapper component for client portal content
12
+ *
13
+ * Note: The main component (clientPortalConfig.component) is passed as children
14
+ * from GenericAuth.jsx and rendered in the default route.
15
+ *
16
+ * To pass props to the component, you can provide a function instead of a component instance:
17
+ * clientPortalConfig={{
18
+ * component: (props) => <YourComponent {...props} customProp="value" />,
19
+ * // other config...
20
+ * }}
21
+ *
22
+ * The function will receive { clientProfile, setClientProfile, clientId } as arguments.
23
+ * The clientId is extracted from clientProfile.id for convenience.
24
+ *
25
+ * @param {Object} props
26
+ * @param {Object} props.clientProfile - The client user profile data
27
+ * @param {Function} props.setClientProfile - Function to update client profile
28
+ * @param {Function} props.setClientAuth - Function to update client authentication state
29
+ * @param {string} props.logo - URL for the logo to display in header
30
+ * @param {Object} props.portalConfig - Configuration for the portal
31
+ * @param {Object} props.portalConfig.routes - Additional route components to render
32
+ * @param {Object} props.portalConfig.keys - Configuration for mapping client profile fields
33
+ * @param {string|string[]} props.portalConfig.keys.name - Field(s) to use for client name display
34
+ * @param {React.ReactNode|Function} props.portalConfig.component - Main component passed from GenericAuth (received as children prop).
35
+ * If a function is provided, it will be called with { clientProfile, setClientProfile, clientId } as arguments
36
+ * @param {Object} props.urls - URLs for API endpoints
37
+ * @param {string} props.urls.logout - URL for logout endpoint
38
+ * @param {string} props.themeType - Theme type (primary, secondary, etc.)
39
+ * @param {React.ReactNode|Function} props.children - Child components to render in default route.
40
+ * If a function is provided, it will be called with { clientProfile, setClientProfile, clientId } as arguments
41
+ */
42
+ const GenericClientPortal = ({
43
+ clientProfile,
44
+ setClientProfile,
45
+ setClientAuth,
46
+ logo,
47
+ portalConfig = { routes: {}, keys: {} },
48
+ themeType = 'primary',
49
+ urls = {},
50
+ children,
51
+ }) => {
52
+ const navigate = useNavigate();
53
+
54
+ // Handle logout
55
+ const handleLogout = async () => {
56
+ try {
57
+ const logoutUrl = urls.logout || '/client/logout';
58
+ await CustomFetch(logoutUrl, 'POST', {});
59
+ setClientProfile({});
60
+ setClientAuth(false);
61
+ toast.success('You have been logged out successfully');
62
+ const loginUrl = '/client/login';
63
+ navigate(loginUrl);
64
+ } catch (error) {
65
+ toast.error('Error logging out. Please try again.');
66
+ }
67
+ };
68
+
69
+ // Get client name based on configuration or fallback to defaults
70
+ const getClientName = () => {
71
+ // Check if keys.name is configured
72
+ if (portalConfig.keys && portalConfig.keys.name) {
73
+ // If keys.name is an array, concatenate the values
74
+ if (Array.isArray(portalConfig.keys.name)) {
75
+ const nameParts = portalConfig.keys.name
76
+ .map((key) => clientProfile[key] || '')
77
+ .filter((part) => part !== '');
78
+
79
+ if (nameParts.length > 0) {
80
+ return nameParts.join(' ');
81
+ }
82
+ } else {
83
+ // If keys.name is a string, use it directly
84
+ const nameValue = clientProfile[portalConfig.keys.name];
85
+ if (nameValue) return nameValue;
86
+ }
87
+ }
88
+
89
+ // Fallback to existing fields
90
+ return clientProfile.name || clientProfile.email || 'Client';
91
+ };
92
+
93
+ const clientName = getClientName();
94
+
95
+ return (
96
+ <div className={`${styles.portalContainer} ${styles[themeType]}`}>
97
+ <header className={styles.portalHeader}>
98
+ <div className={styles.logoContainer}>
99
+ <img
100
+ src={logo}
101
+ alt="Client Portal"
102
+ className={styles.logo}
103
+ />
104
+ </div>
105
+ <div className={styles.userInfo}>
106
+ <span className={styles.welcomeText}>
107
+ Welcome, <strong>{clientName}</strong>
108
+ </span>
109
+ <button
110
+ onClick={handleLogout}
111
+ className={styles.logoutButton}
112
+ title="Logout"
113
+ aria-label="Logout"
114
+ >
115
+ <SignOut strokeWidth={2} size={16} />
116
+ </button>
117
+ </div>
118
+ </header>
119
+
120
+ <main className={styles.portalContent}>
121
+ <div className={styles.mainContent}>
122
+ <Routes>
123
+ {/* Render provided routes if any */}
124
+ {portalConfig.routes &&
125
+ Object.entries(portalConfig.routes).map(
126
+ ([path, Component]) => (
127
+ <Route
128
+ key={path}
129
+ path={path}
130
+ element={
131
+ <Component
132
+ clientProfile={clientProfile}
133
+ setClientProfile={
134
+ setClientProfile
135
+ }
136
+ dataId={
137
+ clientProfile?.id || null
138
+ }
139
+ />
140
+ }
141
+ />
142
+ )
143
+ )}
144
+
145
+ {/* Default route for main component (passed as children from GenericAuth) */}
146
+ <Route
147
+ path="/"
148
+ element={
149
+ typeof children === 'function'
150
+ ? children({
151
+ clientProfile,
152
+ setClientProfile,
153
+ dataId: clientProfile?.id || null,
154
+ })
155
+ : children
156
+ }
157
+ />
158
+ </Routes>
159
+ </div>
160
+ </main>
161
+ </div>
162
+ );
163
+ };
164
+
165
+ export default GenericClientPortal;