@djangocfg/layouts 1.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/LICENSE +21 -0
- package/README.md +77 -0
- package/package.json +86 -0
- package/src/auth/README.md +962 -0
- package/src/auth/context/AuthContext.tsx +458 -0
- package/src/auth/context/index.ts +2 -0
- package/src/auth/context/types.ts +63 -0
- package/src/auth/hooks/index.ts +6 -0
- package/src/auth/hooks/useAuthForm.ts +329 -0
- package/src/auth/hooks/useAuthGuard.ts +23 -0
- package/src/auth/hooks/useAuthRedirect.ts +51 -0
- package/src/auth/hooks/useAutoAuth.ts +42 -0
- package/src/auth/hooks/useLocalStorage.ts +211 -0
- package/src/auth/hooks/useSessionStorage.ts +186 -0
- package/src/auth/index.ts +10 -0
- package/src/auth/middlewares/index.ts +1 -0
- package/src/auth/middlewares/proxy.ts +24 -0
- package/src/auth/server.ts +6 -0
- package/src/auth/utils/errors.ts +34 -0
- package/src/auth/utils/index.ts +2 -0
- package/src/auth/utils/validation.ts +14 -0
- package/src/index.ts +15 -0
- package/src/layouts/AppLayout/AppLayout.tsx +123 -0
- package/src/layouts/AppLayout/README.md +204 -0
- package/src/layouts/AppLayout/SUMMARY.md +240 -0
- package/src/layouts/AppLayout/USAGE.md +312 -0
- package/src/layouts/AppLayout/components/PageProgress.tsx +104 -0
- package/src/layouts/AppLayout/components/Seo.tsx +87 -0
- package/src/layouts/AppLayout/components/index.ts +6 -0
- package/src/layouts/AppLayout/context/AppContext.tsx +146 -0
- package/src/layouts/AppLayout/context/index.ts +5 -0
- package/src/layouts/AppLayout/hooks/index.ts +6 -0
- package/src/layouts/AppLayout/hooks/useLayoutMode.ts +26 -0
- package/src/layouts/AppLayout/hooks/useNavigation.ts +49 -0
- package/src/layouts/AppLayout/index.ts +31 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthContext.tsx +51 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthHelp.tsx +111 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +40 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +330 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/OTPForm.tsx +158 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +13 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/types.ts +61 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +92 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardContent.tsx +60 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +170 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +164 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/index.ts +7 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/index.ts +5 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/PublicLayout.tsx +44 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/DesktopUserMenu.tsx +136 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +262 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenu.tsx +289 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +159 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/index.ts +5 -0
- package/src/layouts/AppLayout/layouts/index.ts +7 -0
- package/src/layouts/AppLayout/providers/CoreProviders.tsx +47 -0
- package/src/layouts/AppLayout/providers/index.ts +5 -0
- package/src/layouts/AppLayout/types/config.ts +40 -0
- package/src/layouts/AppLayout/types/index.ts +10 -0
- package/src/layouts/AppLayout/types/layout.ts +47 -0
- package/src/layouts/AppLayout/types/navigation.ts +41 -0
- package/src/layouts/AppLayout/types/routes.ts +45 -0
- package/src/layouts/AppLayout/utils/index.ts +5 -0
- package/src/layouts/AppLayout/utils/routeDetection.ts +31 -0
- package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +125 -0
- package/src/layouts/PaymentsLayout/README.md +133 -0
- package/src/layouts/PaymentsLayout/components/CreateApiKeyDialog.tsx +172 -0
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +203 -0
- package/src/layouts/PaymentsLayout/components/DeleteApiKeyDialog.tsx +100 -0
- package/src/layouts/PaymentsLayout/components/index.ts +4 -0
- package/src/layouts/PaymentsLayout/events.ts +106 -0
- package/src/layouts/PaymentsLayout/index.ts +20 -0
- package/src/layouts/PaymentsLayout/types.ts +19 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeyMetrics.tsx +109 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeysList.tsx +194 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/index.ts +3 -0
- package/src/layouts/PaymentsLayout/views/apikeys/index.tsx +19 -0
- package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +99 -0
- package/src/layouts/PaymentsLayout/views/overview/components/MetricsCards.tsx +103 -0
- package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +138 -0
- package/src/layouts/PaymentsLayout/views/overview/components/index.ts +4 -0
- package/src/layouts/PaymentsLayout/views/overview/index.tsx +23 -0
- package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +282 -0
- package/src/layouts/PaymentsLayout/views/payments/components/index.ts +2 -0
- package/src/layouts/PaymentsLayout/views/payments/index.tsx +18 -0
- package/src/layouts/PaymentsLayout/views/tariffs/index.tsx +29 -0
- package/src/layouts/PaymentsLayout/views/transactions/index.tsx +29 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +110 -0
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +146 -0
- package/src/layouts/ProfileLayout/components/ProfileForm.tsx +208 -0
- package/src/layouts/ProfileLayout/components/index.ts +3 -0
- package/src/layouts/ProfileLayout/index.ts +3 -0
- package/src/layouts/SupportLayout/README.md +91 -0
- package/src/layouts/SupportLayout/SupportLayout.tsx +178 -0
- package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +154 -0
- package/src/layouts/SupportLayout/components/MessageInput.tsx +92 -0
- package/src/layouts/SupportLayout/components/MessageList.tsx +312 -0
- package/src/layouts/SupportLayout/components/TicketCard.tsx +96 -0
- package/src/layouts/SupportLayout/components/TicketList.tsx +152 -0
- package/src/layouts/SupportLayout/components/index.ts +6 -0
- package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +260 -0
- package/src/layouts/SupportLayout/context/index.ts +2 -0
- package/src/layouts/SupportLayout/events.ts +31 -0
- package/src/layouts/SupportLayout/hooks/index.ts +2 -0
- package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +118 -0
- package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +91 -0
- package/src/layouts/SupportLayout/index.ts +6 -0
- package/src/layouts/SupportLayout/types.ts +23 -0
- package/src/layouts/index.ts +9 -0
- package/src/snippets/AuthDialog/AuthDialog.tsx +88 -0
- package/src/snippets/AuthDialog/events.ts +21 -0
- package/src/snippets/AuthDialog/index.ts +3 -0
- package/src/snippets/AuthDialog/useAuthDialog.ts +27 -0
- package/src/snippets/Breadcrumbs.tsx +80 -0
- package/src/snippets/Chat/ChatUIContext.tsx +110 -0
- package/src/snippets/Chat/ChatWidget.tsx +476 -0
- package/src/snippets/Chat/README.md +122 -0
- package/src/snippets/Chat/components/MessageInput.tsx +124 -0
- package/src/snippets/Chat/components/MessageList.tsx +168 -0
- package/src/snippets/Chat/components/SessionList.tsx +192 -0
- package/src/snippets/Chat/components/index.ts +9 -0
- package/src/snippets/Chat/hooks/index.ts +6 -0
- package/src/snippets/Chat/hooks/useInfiniteSessions.ts +83 -0
- package/src/snippets/Chat/index.tsx +44 -0
- package/src/snippets/Chat/types.ts +79 -0
- package/src/snippets/VideoPlayer/README.md +203 -0
- package/src/snippets/VideoPlayer/VideoControls.tsx +133 -0
- package/src/snippets/VideoPlayer/VideoPlayer.tsx +114 -0
- package/src/snippets/VideoPlayer/index.ts +8 -0
- package/src/snippets/VideoPlayer/types.ts +61 -0
- package/src/snippets/index.ts +10 -0
- package/src/styles/dashboard.css +41 -0
- package/src/styles/index.css +20 -0
- package/src/styles/sources.css +6 -0
- package/src/types/index.ts +1 -0
- package/src/types/pageConfig.ts +103 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/logger.ts +57 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { useRouter } from 'next/router';
|
|
2
|
+
import React, {
|
|
3
|
+
createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState
|
|
4
|
+
} from 'react';
|
|
5
|
+
|
|
6
|
+
import { api, Enums } from '@djangocfg/api';
|
|
7
|
+
import { useAccountsContext, AccountsProvider } from '@djangocfg/api/cfg/contexts';
|
|
8
|
+
import { useLocalStorage } from '@djangocfg/ui/hooks';
|
|
9
|
+
|
|
10
|
+
import { authLogger } from '../../utils/logger';
|
|
11
|
+
import type { AuthConfig, AuthContextType, AuthProviderProps, UserProfile } from './types';
|
|
12
|
+
|
|
13
|
+
// Default routes
|
|
14
|
+
const defaultRoutes = {
|
|
15
|
+
auth: '/auth',
|
|
16
|
+
defaultCallback: '/dashboard',
|
|
17
|
+
defaultAuthCallback: '/auth',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
21
|
+
|
|
22
|
+
// Constants
|
|
23
|
+
const EMAIL_STORAGE_KEY = 'auth_email';
|
|
24
|
+
const PHONE_STORAGE_KEY = 'auth_phone';
|
|
25
|
+
const AUTH_REDIRECT_KEY = 'auth_redirect_url';
|
|
26
|
+
|
|
27
|
+
const hasValidTokens = (): boolean => {
|
|
28
|
+
if (typeof window === 'undefined') return false;
|
|
29
|
+
return api.isAuthenticated();
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Internal provider that uses AccountsContext
|
|
33
|
+
const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config }) => {
|
|
34
|
+
const accounts = useAccountsContext();
|
|
35
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
36
|
+
const [initialized, setInitialized] = useState(false);
|
|
37
|
+
const router = useRouter();
|
|
38
|
+
|
|
39
|
+
// Use localStorage hooks for email, phone, and redirect
|
|
40
|
+
const [storedEmail, setStoredEmail, clearStoredEmail] = useLocalStorage<string | null>(EMAIL_STORAGE_KEY, null);
|
|
41
|
+
const [storedPhone, setStoredPhone, clearStoredPhone] = useLocalStorage<string | null>(PHONE_STORAGE_KEY, null);
|
|
42
|
+
const [redirectUrl, setRedirectUrl, clearRedirectUrl] = useLocalStorage<string | null>(AUTH_REDIRECT_KEY, null);
|
|
43
|
+
|
|
44
|
+
// Map AccountsContext profile to UserProfile
|
|
45
|
+
const user = accounts.profile as UserProfile | null;
|
|
46
|
+
|
|
47
|
+
// Use refs to avoid dependency issues
|
|
48
|
+
const userRef = useRef(user);
|
|
49
|
+
const configRef = useRef(config);
|
|
50
|
+
|
|
51
|
+
// Update refs when values change
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
userRef.current = user;
|
|
54
|
+
}, [user]);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
configRef.current = config;
|
|
58
|
+
}, [config]);
|
|
59
|
+
|
|
60
|
+
// Note: API URL is configured in BaseClient, not at runtime
|
|
61
|
+
|
|
62
|
+
// Common function to clear auth state
|
|
63
|
+
const clearAuthState = useCallback((caller: string) => {
|
|
64
|
+
authLogger.info('clearAuthState >> caller', caller);
|
|
65
|
+
api.clearTokens();
|
|
66
|
+
// Note: user is now managed by AccountsContext, will auto-update
|
|
67
|
+
setInitialized(true);
|
|
68
|
+
setIsLoading(false);
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
// Global error handler for auth-related errors
|
|
72
|
+
const handleGlobalAuthError = useCallback((error: any, context: string = 'API Request') => {
|
|
73
|
+
// Simple error check - if response has error flag, it's an error
|
|
74
|
+
if (error?.success === false) {
|
|
75
|
+
authLogger.warn(`Error detected in ${context}, clearing tokens`);
|
|
76
|
+
clearAuthState(`globalAuthError:${context}`);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false;
|
|
81
|
+
}, [clearAuthState]);
|
|
82
|
+
|
|
83
|
+
// Simple profile loading without retry - now uses AccountsContext
|
|
84
|
+
const loadCurrentProfile = useCallback(async (): Promise<void> => {
|
|
85
|
+
try {
|
|
86
|
+
// Ensure API clients are properly initialized with current token
|
|
87
|
+
if (!api.isAuthenticated()) {
|
|
88
|
+
throw new Error('No valid authentication token');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Refresh profile from AccountsContext
|
|
92
|
+
const refreshedProfile = await accounts.refreshProfile();
|
|
93
|
+
|
|
94
|
+
if (refreshedProfile) {
|
|
95
|
+
setInitialized(true);
|
|
96
|
+
authLogger.info('Profile loaded successfully:', refreshedProfile.id);
|
|
97
|
+
} else {
|
|
98
|
+
authLogger.warn('Profile refresh returned undefined');
|
|
99
|
+
clearAuthState('loadCurrentProfile:noProfile');
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
authLogger.error('Failed to load profile:', error);
|
|
103
|
+
// Use global error handler first, fallback to clearing state
|
|
104
|
+
if (!handleGlobalAuthError(error, 'loadCurrentProfile')) {
|
|
105
|
+
clearAuthState('loadCurrentProfile:error');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}, [clearAuthState, handleGlobalAuthError, accounts]);
|
|
109
|
+
|
|
110
|
+
// Initialize auth state once
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (initialized) return;
|
|
113
|
+
|
|
114
|
+
const initializeAuth = async () => {
|
|
115
|
+
authLogger.info('Initializing auth...');
|
|
116
|
+
setIsLoading(true);
|
|
117
|
+
|
|
118
|
+
// Debug token state
|
|
119
|
+
const token = api.getToken();
|
|
120
|
+
const refreshToken = api.getRefreshToken();
|
|
121
|
+
authLogger.info('Token from API:', token ? `${token.substring(0, 20)}...` : 'null');
|
|
122
|
+
authLogger.info('Refresh token from API:', refreshToken ? `${refreshToken.substring(0, 20)}...` : 'null');
|
|
123
|
+
authLogger.info('localStorage keys:', Object.keys(localStorage).filter(k => k.includes('token') || k.includes('auth')));
|
|
124
|
+
|
|
125
|
+
const hasTokens = hasValidTokens();
|
|
126
|
+
authLogger.info('Has tokens:', hasTokens);
|
|
127
|
+
|
|
128
|
+
if (hasTokens) {
|
|
129
|
+
try {
|
|
130
|
+
await loadCurrentProfile();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
authLogger.error('Failed to load profile during initialization:', error);
|
|
133
|
+
// If profile loading fails, clear auth state
|
|
134
|
+
clearAuthState('initializeAuth:loadProfileFailed');
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
setInitialized(true);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setIsLoading(false);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
initializeAuth();
|
|
144
|
+
}, [initialized, loadCurrentProfile, clearAuthState]);
|
|
145
|
+
|
|
146
|
+
// Redirect logic - only for unauthenticated users on protected pages
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (!initialized) return;
|
|
149
|
+
|
|
150
|
+
const isAuthenticated = !!userRef.current && api.isAuthenticated();
|
|
151
|
+
const authRoute = config?.routes?.auth || defaultRoutes.auth;
|
|
152
|
+
const isAuthPage = router.pathname === authRoute;
|
|
153
|
+
|
|
154
|
+
// Only redirect authenticated users away from auth page if they're not in a flow
|
|
155
|
+
// This prevents interference with OTP verification flow
|
|
156
|
+
if (isAuthenticated && isAuthPage && !router.query.flow) {
|
|
157
|
+
const callbackUrl = config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
|
|
158
|
+
window.location.href = callbackUrl;
|
|
159
|
+
}
|
|
160
|
+
}, [initialized, router.pathname, config?.routes, router.query.flow]);
|
|
161
|
+
|
|
162
|
+
const pushToDefaultCallbackUrl = useCallback(() => {
|
|
163
|
+
const callbackUrl = config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
|
|
164
|
+
window.location.href = callbackUrl;
|
|
165
|
+
}, [config?.routes]);
|
|
166
|
+
|
|
167
|
+
const pushToDefaultAuthCallbackUrl = useCallback(() => {
|
|
168
|
+
const authCallbackUrl = config?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
|
|
169
|
+
window.location.href = authCallbackUrl;
|
|
170
|
+
}, [config?.routes]);
|
|
171
|
+
|
|
172
|
+
// Memoized checkAuthAndRedirect function
|
|
173
|
+
const checkAuthAndRedirect = useCallback(async () => {
|
|
174
|
+
try {
|
|
175
|
+
setIsLoading(true);
|
|
176
|
+
const isAuthenticated = api.isAuthenticated();
|
|
177
|
+
|
|
178
|
+
if (isAuthenticated) {
|
|
179
|
+
await loadCurrentProfile();
|
|
180
|
+
if (userRef.current) {
|
|
181
|
+
pushToDefaultCallbackUrl();
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
pushToDefaultAuthCallbackUrl();
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
authLogger.error('Failed to check authentication:', error);
|
|
188
|
+
// Use global error handler first
|
|
189
|
+
if (!handleGlobalAuthError(error, 'checkAuthAndRedirect')) {
|
|
190
|
+
clearAuthState('checkAuthAndRedirect');
|
|
191
|
+
}
|
|
192
|
+
pushToDefaultAuthCallbackUrl();
|
|
193
|
+
} finally {
|
|
194
|
+
setIsLoading(false);
|
|
195
|
+
}
|
|
196
|
+
}, [loadCurrentProfile, clearAuthState, pushToDefaultCallbackUrl, pushToDefaultAuthCallbackUrl, handleGlobalAuthError]);
|
|
197
|
+
|
|
198
|
+
// OTP methods - supports both email and phone - now uses AccountsContext
|
|
199
|
+
const requestOTP = useCallback(
|
|
200
|
+
async (identifier: string, channel?: 'email' | 'phone', sourceUrl?: string): Promise<{ success: boolean; message: string }> => {
|
|
201
|
+
// Clear tokens before requesting OTP
|
|
202
|
+
api.clearTokens();
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const channelValue = channel === 'phone'
|
|
206
|
+
? Enums.OTPRequestRequestChannel.PHONE
|
|
207
|
+
: Enums.OTPRequestRequestChannel.EMAIL;
|
|
208
|
+
const result = await accounts.requestOTP({
|
|
209
|
+
identifier,
|
|
210
|
+
channel: channelValue,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const channelName = channel === 'phone' ? 'phone number' : 'email address';
|
|
214
|
+
return {
|
|
215
|
+
success: true,
|
|
216
|
+
message: result.message || `OTP code sent to your ${channelName}`,
|
|
217
|
+
};
|
|
218
|
+
} catch (error) {
|
|
219
|
+
authLogger.error('Request OTP error:', error);
|
|
220
|
+
return {
|
|
221
|
+
success: false,
|
|
222
|
+
message: 'Failed to send OTP',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
[accounts],
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const verifyOTP = useCallback(
|
|
230
|
+
async (identifier: string, otpCode: string, channel?: 'email' | 'phone', sourceUrl?: string): Promise<{ success: boolean; message: string; user?: UserProfile }> => {
|
|
231
|
+
try {
|
|
232
|
+
const channelValue = channel === 'phone'
|
|
233
|
+
? Enums.OTPVerifyRequestChannel.PHONE
|
|
234
|
+
: Enums.OTPVerifyRequestChannel.EMAIL;
|
|
235
|
+
// AccountsContext automatically saves tokens and refreshes profile
|
|
236
|
+
const result = await accounts.verifyOTP({
|
|
237
|
+
identifier,
|
|
238
|
+
otp: otpCode,
|
|
239
|
+
channel: channelValue,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Verify that we got valid tokens
|
|
243
|
+
if (!result.access || !result.refresh) {
|
|
244
|
+
authLogger.error('Verify OTP returned invalid response:', result);
|
|
245
|
+
return {
|
|
246
|
+
success: false,
|
|
247
|
+
message: 'Invalid OTP verification response',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Save identifier based on channel and clear opposite channel
|
|
252
|
+
if (channel === 'phone') {
|
|
253
|
+
setStoredPhone(identifier);
|
|
254
|
+
clearStoredEmail();
|
|
255
|
+
} else if (identifier.includes('@')) {
|
|
256
|
+
setStoredEmail(identifier);
|
|
257
|
+
clearStoredPhone();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Small delay to ensure profile state is updated
|
|
261
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
262
|
+
|
|
263
|
+
// Handle redirect logic here
|
|
264
|
+
const defaultCallback = config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
|
|
265
|
+
|
|
266
|
+
if (redirectUrl && redirectUrl !== defaultCallback) {
|
|
267
|
+
clearRedirectUrl();
|
|
268
|
+
window.location.href = redirectUrl;
|
|
269
|
+
} else {
|
|
270
|
+
window.location.href = defaultCallback;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
success: true,
|
|
275
|
+
message: 'Login successful',
|
|
276
|
+
user: result.user as UserProfile,
|
|
277
|
+
};
|
|
278
|
+
} catch (error) {
|
|
279
|
+
authLogger.error('Verify OTP error:', error);
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
message: 'Failed to verify OTP',
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
[setStoredEmail, setStoredPhone, clearStoredEmail, clearStoredPhone, redirectUrl, clearRedirectUrl, config?.routes?.defaultCallback, accounts],
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const refreshToken = useCallback(async (): Promise<{ success: boolean; message: string }> => {
|
|
290
|
+
try {
|
|
291
|
+
const refreshTokenValue = api.getRefreshToken();
|
|
292
|
+
if (!refreshTokenValue) {
|
|
293
|
+
clearAuthState('refreshToken:noToken');
|
|
294
|
+
return {
|
|
295
|
+
success: false,
|
|
296
|
+
message: 'No refresh token available',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await accounts.refreshToken(refreshTokenValue);
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
success: true,
|
|
304
|
+
message: 'Token refreshed',
|
|
305
|
+
};
|
|
306
|
+
} catch (error) {
|
|
307
|
+
authLogger.error('Refresh token error:', error);
|
|
308
|
+
clearAuthState('refreshToken:error');
|
|
309
|
+
return {
|
|
310
|
+
success: false,
|
|
311
|
+
message: 'Error refreshing token',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}, [clearAuthState, accounts]);
|
|
315
|
+
|
|
316
|
+
const clearRedirect = useCallback((): void => {
|
|
317
|
+
clearRedirectUrl();
|
|
318
|
+
}, [clearRedirectUrl]);
|
|
319
|
+
|
|
320
|
+
// Save current URL for redirect after authentication
|
|
321
|
+
const saveCurrentUrlForRedirect = useCallback((): void => {
|
|
322
|
+
if (typeof window !== 'undefined') {
|
|
323
|
+
const currentUrl = window.location.pathname + window.location.search;
|
|
324
|
+
setRedirectUrl(currentUrl);
|
|
325
|
+
}
|
|
326
|
+
}, [setRedirectUrl]);
|
|
327
|
+
|
|
328
|
+
const logout = useCallback(async (): Promise<void> => {
|
|
329
|
+
// Use config.onConfirm if provided, otherwise use a simple confirm
|
|
330
|
+
if (configRef.current?.onConfirm) {
|
|
331
|
+
const { confirmed } = await configRef.current.onConfirm({
|
|
332
|
+
title: 'Logout',
|
|
333
|
+
description: 'Are you sure you want to logout?',
|
|
334
|
+
confirmationButtonText: 'Logout',
|
|
335
|
+
cancellationButtonText: 'Cancel',
|
|
336
|
+
color: 'error',
|
|
337
|
+
});
|
|
338
|
+
if (confirmed) {
|
|
339
|
+
accounts.logout(); // Clear tokens and profile
|
|
340
|
+
setInitialized(true);
|
|
341
|
+
setIsLoading(false);
|
|
342
|
+
pushToDefaultAuthCallbackUrl();
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
// Fallback to browser confirm
|
|
346
|
+
const confirmed = window.confirm('Are you sure you want to logout?');
|
|
347
|
+
if (confirmed) {
|
|
348
|
+
accounts.logout(); // Clear tokens and profile
|
|
349
|
+
setInitialized(true);
|
|
350
|
+
setIsLoading(false);
|
|
351
|
+
pushToDefaultAuthCallbackUrl();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}, [accounts, pushToDefaultAuthCallbackUrl]);
|
|
355
|
+
|
|
356
|
+
// Redirect URL methods
|
|
357
|
+
const getSavedRedirectUrl = useCallback((): string | null => {
|
|
358
|
+
if (typeof window !== 'undefined') {
|
|
359
|
+
return sessionStorage.getItem(AUTH_REDIRECT_KEY);
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}, []);
|
|
363
|
+
|
|
364
|
+
const saveRedirectUrl = useCallback((url: string): void => {
|
|
365
|
+
if (typeof window !== 'undefined') {
|
|
366
|
+
sessionStorage.setItem(AUTH_REDIRECT_KEY, url);
|
|
367
|
+
}
|
|
368
|
+
}, []);
|
|
369
|
+
|
|
370
|
+
const clearSavedRedirectUrl = useCallback((): void => {
|
|
371
|
+
if (typeof window !== 'undefined') {
|
|
372
|
+
sessionStorage.removeItem(AUTH_REDIRECT_KEY);
|
|
373
|
+
}
|
|
374
|
+
}, []);
|
|
375
|
+
|
|
376
|
+
const getFinalRedirectUrl = useCallback((): string => {
|
|
377
|
+
const savedUrl = getSavedRedirectUrl();
|
|
378
|
+
return savedUrl || (config?.routes?.defaultCallback || defaultRoutes.defaultCallback);
|
|
379
|
+
}, [getSavedRedirectUrl, config?.routes?.defaultCallback]);
|
|
380
|
+
|
|
381
|
+
const useAndClearRedirectUrl = useCallback((): string => {
|
|
382
|
+
const finalUrl = getFinalRedirectUrl();
|
|
383
|
+
clearSavedRedirectUrl();
|
|
384
|
+
return finalUrl;
|
|
385
|
+
}, [getFinalRedirectUrl, clearSavedRedirectUrl]);
|
|
386
|
+
|
|
387
|
+
// Memoized context value
|
|
388
|
+
const value = useMemo<AuthContextType>(
|
|
389
|
+
() => ({
|
|
390
|
+
user,
|
|
391
|
+
isLoading,
|
|
392
|
+
isAuthenticated: !!user && api.isAuthenticated(),
|
|
393
|
+
loadCurrentProfile,
|
|
394
|
+
checkAuthAndRedirect,
|
|
395
|
+
getSavedEmail: () => storedEmail,
|
|
396
|
+
saveEmail: setStoredEmail,
|
|
397
|
+
clearSavedEmail: clearStoredEmail,
|
|
398
|
+
getSavedPhone: () => storedPhone,
|
|
399
|
+
savePhone: setStoredPhone,
|
|
400
|
+
clearSavedPhone: clearStoredPhone,
|
|
401
|
+
requestOTP,
|
|
402
|
+
verifyOTP,
|
|
403
|
+
refreshToken,
|
|
404
|
+
logout,
|
|
405
|
+
getSavedRedirectUrl,
|
|
406
|
+
saveRedirectUrl,
|
|
407
|
+
clearSavedRedirectUrl,
|
|
408
|
+
getFinalRedirectUrl,
|
|
409
|
+
useAndClearRedirectUrl,
|
|
410
|
+
saveCurrentUrlForRedirect,
|
|
411
|
+
}),
|
|
412
|
+
[
|
|
413
|
+
user,
|
|
414
|
+
isLoading,
|
|
415
|
+
loadCurrentProfile,
|
|
416
|
+
checkAuthAndRedirect,
|
|
417
|
+
storedEmail,
|
|
418
|
+
setStoredEmail,
|
|
419
|
+
clearStoredEmail,
|
|
420
|
+
storedPhone,
|
|
421
|
+
setStoredPhone,
|
|
422
|
+
clearStoredPhone,
|
|
423
|
+
requestOTP,
|
|
424
|
+
verifyOTP,
|
|
425
|
+
refreshToken,
|
|
426
|
+
logout,
|
|
427
|
+
getSavedRedirectUrl,
|
|
428
|
+
saveRedirectUrl,
|
|
429
|
+
clearSavedRedirectUrl,
|
|
430
|
+
getFinalRedirectUrl,
|
|
431
|
+
useAndClearRedirectUrl,
|
|
432
|
+
saveCurrentUrlForRedirect,
|
|
433
|
+
],
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Wrapper that provides AccountsContext
|
|
440
|
+
export const AuthProvider: React.FC<AuthProviderProps> = ({ children, config }) => {
|
|
441
|
+
return (
|
|
442
|
+
<AccountsProvider>
|
|
443
|
+
<AuthProviderInternal config={config}>
|
|
444
|
+
{children}
|
|
445
|
+
</AuthProviderInternal>
|
|
446
|
+
</AccountsProvider>
|
|
447
|
+
);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
export const useAuth = (): AuthContextType => {
|
|
451
|
+
const context = useContext(AuthContext);
|
|
452
|
+
if (context === undefined) {
|
|
453
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
454
|
+
}
|
|
455
|
+
return context;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
export default AuthContext;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { CfgUserProfileTypes } from '@djangocfg/api';
|
|
4
|
+
|
|
5
|
+
// User profile type
|
|
6
|
+
export type UserProfile = CfgUserProfileTypes.User;
|
|
7
|
+
|
|
8
|
+
// Auth configuration
|
|
9
|
+
export interface AuthConfig {
|
|
10
|
+
apiUrl?: string;
|
|
11
|
+
routes?: {
|
|
12
|
+
auth?: string;
|
|
13
|
+
defaultCallback?: string;
|
|
14
|
+
defaultAuthCallback?: string;
|
|
15
|
+
};
|
|
16
|
+
onLogout?: () => void;
|
|
17
|
+
onConfirm?: (options: {
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
confirmationButtonText: string;
|
|
21
|
+
cancellationButtonText: string;
|
|
22
|
+
color: string;
|
|
23
|
+
}) => Promise<{ confirmed: boolean }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Auth context interface
|
|
27
|
+
export interface AuthContextType {
|
|
28
|
+
user: UserProfile | null;
|
|
29
|
+
isLoading: boolean;
|
|
30
|
+
isAuthenticated: boolean;
|
|
31
|
+
loadCurrentProfile: () => Promise<void>;
|
|
32
|
+
checkAuthAndRedirect: () => Promise<void>;
|
|
33
|
+
|
|
34
|
+
// Email Methods
|
|
35
|
+
getSavedEmail: () => string | null;
|
|
36
|
+
saveEmail: (email: string) => void;
|
|
37
|
+
clearSavedEmail: () => void;
|
|
38
|
+
|
|
39
|
+
// Phone Methods
|
|
40
|
+
getSavedPhone: () => string | null;
|
|
41
|
+
savePhone: (phone: string) => void;
|
|
42
|
+
clearSavedPhone: () => void;
|
|
43
|
+
|
|
44
|
+
// OTP Methods - Multi-channel support
|
|
45
|
+
requestOTP: (identifier: string, channel?: 'email' | 'phone', sourceUrl?: string) => Promise<{ success: boolean; message: string }>;
|
|
46
|
+
verifyOTP: (identifier: string, otpCode: string, channel?: 'email' | 'phone', sourceUrl?: string) => Promise<{ success: boolean; message: string; user?: UserProfile }>;
|
|
47
|
+
refreshToken: () => Promise<{ success: boolean; message: string }>;
|
|
48
|
+
logout: () => Promise<void>;
|
|
49
|
+
|
|
50
|
+
// Redirect Methods
|
|
51
|
+
getSavedRedirectUrl: () => string | null;
|
|
52
|
+
saveRedirectUrl: (url: string) => void;
|
|
53
|
+
clearSavedRedirectUrl: () => void;
|
|
54
|
+
getFinalRedirectUrl: () => string;
|
|
55
|
+
useAndClearRedirectUrl: () => string;
|
|
56
|
+
saveCurrentUrlForRedirect: () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Provider props
|
|
60
|
+
export interface AuthProviderProps {
|
|
61
|
+
children: React.ReactNode;
|
|
62
|
+
config?: AuthConfig;
|
|
63
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { useAuthRedirectManager } from './useAuthRedirect';
|
|
2
|
+
export { useAuthGuard } from './useAuthGuard';
|
|
3
|
+
export { useSessionStorage } from './useSessionStorage';
|
|
4
|
+
export { useLocalStorage } from './useLocalStorage';
|
|
5
|
+
export { useAuthForm } from './useAuthForm';
|
|
6
|
+
export { useAutoAuth } from './useAutoAuth';
|