@djangocfg/layouts 2.1.10 → 2.1.14

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.
Files changed (105) hide show
  1. package/README.md +53 -161
  2. package/package.json +6 -6
  3. package/src/components/RedirectPage/RedirectPage.tsx +1 -1
  4. package/src/index.ts +0 -6
  5. package/src/layouts/AppLayout/AppLayout.tsx +1 -1
  6. package/src/layouts/AppLayout/BaseApp.tsx +1 -1
  7. package/src/layouts/AuthLayout/AuthContext.tsx +1 -1
  8. package/src/layouts/AuthLayout/OAuthCallback.tsx +1 -1
  9. package/src/layouts/AuthLayout/OAuthProviders.tsx +1 -1
  10. package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -1
  11. package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +1 -1
  12. package/src/layouts/ProfileLayout/components/AvatarSection.tsx +2 -2
  13. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
  14. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +1 -1
  15. package/src/layouts/_components/UserMenu.tsx +1 -1
  16. package/src/layouts/index.ts +0 -2
  17. package/src/snippets/Analytics/useAnalytics.ts +1 -1
  18. package/src/snippets/index.ts +0 -3
  19. package/src/auth/README.md +0 -962
  20. package/src/auth/context/AccountsContext.tsx +0 -240
  21. package/src/auth/context/AuthContext.tsx +0 -604
  22. package/src/auth/context/index.ts +0 -4
  23. package/src/auth/context/types.ts +0 -68
  24. package/src/auth/hooks/index.ts +0 -17
  25. package/src/auth/hooks/useAuthForm.ts +0 -332
  26. package/src/auth/hooks/useAuthGuard.ts +0 -25
  27. package/src/auth/hooks/useAuthRedirect.ts +0 -51
  28. package/src/auth/hooks/useAutoAuth.ts +0 -49
  29. package/src/auth/hooks/useGithubAuth.ts +0 -184
  30. package/src/auth/hooks/useLocalStorage.ts +0 -214
  31. package/src/auth/hooks/useProfileCache.ts +0 -146
  32. package/src/auth/hooks/useSessionStorage.ts +0 -189
  33. package/src/auth/index.ts +0 -10
  34. package/src/auth/middlewares/index.ts +0 -1
  35. package/src/auth/middlewares/proxy.ts +0 -32
  36. package/src/auth/server.ts +0 -6
  37. package/src/auth/utils/errors.ts +0 -34
  38. package/src/auth/utils/index.ts +0 -2
  39. package/src/auth/utils/validation.ts +0 -14
  40. package/src/contexts/LeadsContext.tsx +0 -156
  41. package/src/contexts/NewsletterContext.tsx +0 -263
  42. package/src/contexts/SupportContext.tsx +0 -256
  43. package/src/contexts/index.ts +0 -59
  44. package/src/contexts/knowbase/ChatContext.tsx +0 -174
  45. package/src/contexts/knowbase/DocumentsContext.tsx +0 -304
  46. package/src/contexts/knowbase/SessionsContext.tsx +0 -174
  47. package/src/contexts/knowbase/index.ts +0 -61
  48. package/src/contexts/payments/BalancesContext.tsx +0 -65
  49. package/src/contexts/payments/CurrenciesContext.tsx +0 -66
  50. package/src/contexts/payments/OverviewContext.tsx +0 -174
  51. package/src/contexts/payments/PaymentsContext.tsx +0 -132
  52. package/src/contexts/payments/README.md +0 -201
  53. package/src/contexts/payments/RootPaymentsContext.tsx +0 -68
  54. package/src/contexts/payments/index.ts +0 -50
  55. package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +0 -92
  56. package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +0 -291
  57. package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +0 -290
  58. package/src/layouts/PaymentsLayout/components/index.ts +0 -2
  59. package/src/layouts/PaymentsLayout/events.ts +0 -47
  60. package/src/layouts/PaymentsLayout/index.ts +0 -16
  61. package/src/layouts/PaymentsLayout/types.ts +0 -6
  62. package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +0 -128
  63. package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +0 -142
  64. package/src/layouts/PaymentsLayout/views/overview/components/index.ts +0 -2
  65. package/src/layouts/PaymentsLayout/views/overview/index.tsx +0 -20
  66. package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +0 -276
  67. package/src/layouts/PaymentsLayout/views/payments/components/index.ts +0 -1
  68. package/src/layouts/PaymentsLayout/views/payments/index.tsx +0 -17
  69. package/src/layouts/PaymentsLayout/views/transactions/components/TransactionsList.tsx +0 -273
  70. package/src/layouts/PaymentsLayout/views/transactions/components/index.ts +0 -1
  71. package/src/layouts/PaymentsLayout/views/transactions/index.tsx +0 -17
  72. package/src/layouts/SupportLayout/README.md +0 -91
  73. package/src/layouts/SupportLayout/SupportLayout.tsx +0 -179
  74. package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +0 -155
  75. package/src/layouts/SupportLayout/components/MessageInput.tsx +0 -92
  76. package/src/layouts/SupportLayout/components/MessageList.tsx +0 -314
  77. package/src/layouts/SupportLayout/components/TicketCard.tsx +0 -96
  78. package/src/layouts/SupportLayout/components/TicketList.tsx +0 -153
  79. package/src/layouts/SupportLayout/components/index.ts +0 -6
  80. package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +0 -263
  81. package/src/layouts/SupportLayout/context/index.ts +0 -2
  82. package/src/layouts/SupportLayout/events.ts +0 -33
  83. package/src/layouts/SupportLayout/hooks/index.ts +0 -2
  84. package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +0 -119
  85. package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +0 -92
  86. package/src/layouts/SupportLayout/index.ts +0 -8
  87. package/src/layouts/SupportLayout/types.ts +0 -21
  88. package/src/snippets/Chat/ChatUIContext.tsx +0 -110
  89. package/src/snippets/Chat/ChatWidget.tsx +0 -476
  90. package/src/snippets/Chat/README.md +0 -122
  91. package/src/snippets/Chat/components/MessageInput.tsx +0 -124
  92. package/src/snippets/Chat/components/MessageList.tsx +0 -169
  93. package/src/snippets/Chat/components/SessionList.tsx +0 -192
  94. package/src/snippets/Chat/components/index.ts +0 -9
  95. package/src/snippets/Chat/hooks/index.ts +0 -6
  96. package/src/snippets/Chat/hooks/useInfiniteSessions.ts +0 -82
  97. package/src/snippets/Chat/index.tsx +0 -45
  98. package/src/snippets/Chat/types.ts +0 -80
  99. package/src/snippets/ContactForm/ContactForm.tsx +0 -346
  100. package/src/snippets/ContactForm/ContactFormProvider.tsx +0 -153
  101. package/src/snippets/ContactForm/ContactInfo.tsx +0 -114
  102. package/src/snippets/ContactForm/ContactPage.tsx +0 -131
  103. package/src/snippets/ContactForm/dynamic.tsx +0 -55
  104. package/src/snippets/ContactForm/index.ts +0 -34
  105. package/src/snippets/ContactForm/types.ts +0 -110
@@ -1,332 +0,0 @@
1
- "use client"
2
-
3
- import { useCallback, useEffect, useState } from 'react';
4
-
5
- import { useAuth } from '../context';
6
- import { useAutoAuth } from './useAutoAuth';
7
- import { useLocalStorage } from './useLocalStorage';
8
- import { authLogger } from '../../utils/logger';
9
-
10
- export interface AuthFormState {
11
- identifier: string; // Email or phone number
12
- channel: 'email' | 'phone';
13
- otp: string;
14
- isLoading: boolean;
15
- acceptedTerms: boolean;
16
- step: 'identifier' | 'otp';
17
- error: string;
18
- }
19
-
20
- export interface AuthFormHandlers {
21
- setIdentifier: (identifier: string) => void;
22
- setChannel: (channel: 'email' | 'phone') => void;
23
- setOtp: (otp: string) => void;
24
- setAcceptedTerms: (accepted: boolean) => void;
25
- setError: (error: string) => void;
26
- clearError: () => void;
27
- handleIdentifierSubmit: (e: React.FormEvent) => Promise<void>;
28
- handleOTPSubmit: (e: React.FormEvent) => Promise<void>;
29
- handleResendOTP: () => Promise<void>;
30
- handleBackToIdentifier: () => void;
31
- forceOTPStep: () => void;
32
- // Utility methods
33
- detectChannelFromIdentifier: (identifier: string) => 'email' | 'phone' | null;
34
- validateIdentifier: (identifier: string, channel?: 'email' | 'phone') => boolean;
35
- }
36
-
37
- export interface UseAuthFormOptions {
38
- onIdentifierSuccess?: (identifier: string, channel: 'email' | 'phone') => void;
39
- onOTPSuccess?: () => void;
40
- onError?: (message: string) => void;
41
- sourceUrl: string;
42
- }
43
-
44
- export const useAuthForm = (options: UseAuthFormOptions): AuthFormState & AuthFormHandlers => {
45
- const { onIdentifierSuccess, onOTPSuccess, onError, sourceUrl } = options;
46
-
47
- // Form state
48
- const [identifier, setIdentifier] = useState('');
49
- const [channel, setChannel] = useState<'email' | 'phone'>('email');
50
- const [otp, setOtp] = useState('');
51
- const [isLoading, setIsLoading] = useState(false);
52
- const [acceptedTerms, setAcceptedTerms] = useState(false);
53
- const [step, setStep] = useState<'identifier' | 'otp'>('identifier');
54
- const [error, setError] = useState('');
55
-
56
-
57
-
58
- // Auth hooks
59
- const { requestOTP, verifyOTP, getSavedEmail, saveEmail, getSavedPhone, savePhone } = useAuth();
60
- const [savedTermsAccepted, setSavedTermsAccepted] = useLocalStorage('auth_terms_accepted', false);
61
- const [savedEmail, setSavedEmail] = useLocalStorage('auth_email', '');
62
- const [savedPhone, setSavedPhone] = useLocalStorage('auth_phone', '');
63
-
64
- // Utility functions
65
- const detectChannelFromIdentifier = useCallback((identifier: string): 'email' | 'phone' | null => {
66
- if (!identifier) return null;
67
-
68
- // Email detection
69
- if (identifier.includes('@')) {
70
- return 'email';
71
- }
72
-
73
- // Phone detection (starts with + and contains digits)
74
- if (identifier.startsWith('+') && /^\+[1-9]\d{6,14}$/.test(identifier)) {
75
- return 'phone';
76
- }
77
-
78
- return null;
79
- }, []);
80
-
81
- const validateIdentifier = useCallback((identifier: string, channelType?: 'email' | 'phone'): boolean => {
82
- if (!identifier) return false;
83
-
84
- const detectedChannel = channelType || detectChannelFromIdentifier(identifier);
85
-
86
- if (detectedChannel === 'email') {
87
- // Basic email validation
88
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(identifier);
89
- } else if (detectedChannel === 'phone') {
90
- // E.164 phone validation
91
- return /^\+[1-9]\d{6,14}$/.test(identifier);
92
- }
93
-
94
- return false;
95
- }, [detectChannelFromIdentifier]);
96
-
97
- // Load saved data on mount
98
- useEffect(() => {
99
- const authSavedEmail = getSavedEmail();
100
- const authSavedPhone = getSavedPhone();
101
-
102
- // Prioritize phone over email if both exist
103
- if (authSavedPhone) {
104
- setIdentifier(authSavedPhone);
105
- setChannel('phone');
106
- } else if (authSavedEmail) {
107
- setIdentifier(authSavedEmail);
108
- setChannel('email');
109
- }
110
-
111
- if (savedTermsAccepted) {
112
- setAcceptedTerms(savedTermsAccepted);
113
- }
114
- }, [getSavedEmail, getSavedPhone, savedTermsAccepted]);
115
-
116
- // Auto-detect channel when identifier changes
117
- useEffect(() => {
118
- if (identifier) {
119
- const detectedChannel = detectChannelFromIdentifier(identifier);
120
- if (detectedChannel && detectedChannel !== channel) {
121
- setChannel(detectedChannel);
122
- }
123
- }
124
- }, [identifier, channel, detectChannelFromIdentifier]);
125
-
126
-
127
-
128
- const clearError = useCallback(() => setError(''), []);
129
-
130
- const handleIdentifierSubmit = useCallback(async (e: React.FormEvent) => {
131
- e.preventDefault();
132
-
133
- if (!identifier) {
134
- const message = channel === 'phone' ? 'Please enter your phone number' : 'Please enter your email address';
135
- setError(message);
136
- onError?.(message);
137
- return;
138
- }
139
-
140
- // Validate identifier format
141
- if (!validateIdentifier(identifier, channel)) {
142
- const message = channel === 'phone'
143
- ? 'Please enter a valid phone number (e.g., +1234567890)'
144
- : 'Please enter a valid email address';
145
- setError(message);
146
- onError?.(message);
147
- return;
148
- }
149
-
150
- if (!acceptedTerms) {
151
- const message = 'Please accept the Terms of Service and Privacy Policy';
152
- setError(message);
153
- onError?.(message);
154
- return;
155
- }
156
-
157
- setIsLoading(true);
158
- clearError();
159
-
160
- try {
161
- const result = await requestOTP(identifier, channel, sourceUrl);
162
-
163
- if (result.success) {
164
- // Save identifier and terms acceptance on successful request, clear opposite channel
165
- if (channel === 'email') {
166
- saveEmail(identifier);
167
- setSavedPhone(''); // Clear phone storage
168
- } else if (channel === 'phone') {
169
- savePhone(identifier);
170
- setSavedEmail(''); // Clear email storage
171
- }
172
- setSavedTermsAccepted(true);
173
- setStep('otp');
174
- onIdentifierSuccess?.(identifier, channel);
175
- } else {
176
- setError(result.message);
177
- onError?.(result.message);
178
- }
179
- } catch (error) {
180
- const message = 'An unexpected error occurred';
181
- setError(message);
182
- onError?.(message);
183
- } finally {
184
- setIsLoading(false);
185
- }
186
- }, [identifier, channel, acceptedTerms, validateIdentifier, requestOTP, saveEmail, clearError, setSavedTermsAccepted, onIdentifierSuccess, onError, sourceUrl]);
187
-
188
- const handleOTPSubmit = useCallback(async (e: React.FormEvent) => {
189
- e.preventDefault();
190
-
191
- if (!otp || otp.length < 6) {
192
- const message = 'Please enter the 6-digit verification code';
193
- setError(message);
194
- onError?.(message);
195
- return;
196
- }
197
-
198
- setIsLoading(true);
199
- clearError();
200
-
201
- try {
202
- const result = await verifyOTP(identifier, otp, channel, sourceUrl);
203
-
204
- if (result.success) {
205
- // Save identifier on successful login, clear opposite channel
206
- if (channel === 'email') {
207
- setSavedEmail(identifier);
208
- setSavedPhone(''); // Clear phone storage
209
- } else if (channel === 'phone') {
210
- setSavedPhone(identifier);
211
- setSavedEmail(''); // Clear email storage
212
- }
213
- onOTPSuccess?.();
214
- } else {
215
- setError(result.message);
216
- onError?.(result.message);
217
- }
218
- } catch (error) {
219
- const message = 'An unexpected error occurred';
220
- setError(message);
221
- onError?.(message);
222
- } finally {
223
- setIsLoading(false);
224
- }
225
- }, [identifier, otp, channel, verifyOTP, clearError, setSavedEmail, onOTPSuccess, onError, sourceUrl]);
226
-
227
- const handleResendOTP = useCallback(async () => {
228
- setIsLoading(true);
229
- clearError();
230
-
231
- try {
232
- const result = await requestOTP(identifier, channel, sourceUrl);
233
-
234
- if (result.success) {
235
- // Save identifier and clear OTP input, clear opposite channel
236
- if (channel === 'email') {
237
- saveEmail(identifier);
238
- setSavedPhone(''); // Clear phone storage
239
- } else if (channel === 'phone') {
240
- savePhone(identifier);
241
- setSavedEmail(''); // Clear email storage
242
- }
243
- setOtp('');
244
- } else {
245
- setError(result.message);
246
- onError?.(result.message);
247
- }
248
- } catch (error) {
249
- const message = 'Failed to resend verification code';
250
- setError(message);
251
- onError?.(message);
252
- } finally {
253
- setIsLoading(false);
254
- }
255
- }, [identifier, channel, requestOTP, saveEmail, clearError, setOtp, onError, sourceUrl]);
256
-
257
- const handleBackToIdentifier = useCallback(() => {
258
- setStep('identifier');
259
- clearError();
260
- }, [clearError]);
261
-
262
- const forceOTPStep = useCallback(() => {
263
- setStep('otp');
264
- clearError();
265
- }, [clearError]);
266
-
267
- const handleAcceptedTermsChange = useCallback((checked: boolean) => {
268
- setAcceptedTerms(checked);
269
- setSavedTermsAccepted(checked);
270
- }, [setSavedTermsAccepted]);
271
-
272
- // Auto-detect OTP from URL query parameters
273
- useAutoAuth({
274
- onOTPDetected: (otp: string) => {
275
- authLogger.info('OTP detected, auto-submitting');
276
-
277
- // Get saved identifier from auth context
278
- const savedEmail = getSavedEmail();
279
- const savedPhone = getSavedPhone();
280
-
281
- // Prioritize phone over email if both exist
282
- if (savedPhone) {
283
- setIdentifier(savedPhone);
284
- setChannel('phone');
285
- } else if (savedEmail) {
286
- setIdentifier(savedEmail);
287
- setChannel('email');
288
- }
289
-
290
- // Set OTP and force OTP step
291
- setOtp(otp);
292
- setStep('otp');
293
-
294
- // Auto-submit after a short delay to ensure state is updated
295
- setTimeout(() => {
296
- const fakeEvent = { preventDefault: () => {} } as React.FormEvent;
297
- handleOTPSubmit(fakeEvent);
298
- }, 200);
299
- },
300
- cleanupUrl: true,
301
- });
302
-
303
- return {
304
- // Form state
305
- identifier,
306
- channel,
307
- otp,
308
- isLoading,
309
- acceptedTerms,
310
- step,
311
- error,
312
-
313
- // Form handlers
314
- setIdentifier,
315
- setChannel,
316
- setOtp,
317
- setAcceptedTerms: handleAcceptedTermsChange,
318
- setError,
319
- clearError,
320
-
321
- // Auth handlers
322
- handleIdentifierSubmit,
323
- handleOTPSubmit,
324
- handleResendOTP,
325
- handleBackToIdentifier,
326
- forceOTPStep,
327
-
328
- // Utility methods
329
- detectChannelFromIdentifier,
330
- validateIdentifier,
331
- };
332
- };
@@ -1,25 +0,0 @@
1
- "use client"
2
-
3
- import { useEffect } from 'react';
4
-
5
- import { useAuth } from '../context';
6
- import { useCfgRouter } from '@djangocfg/ui-nextjs/hooks';
7
-
8
- interface UseAuthGuardOptions {
9
- redirectTo?: string;
10
- requireAuth?: boolean;
11
- }
12
-
13
- export const useAuthGuard = (options: UseAuthGuardOptions = {}) => {
14
- const { redirectTo = '/auth', requireAuth = true } = options;
15
- const { isAuthenticated, isLoading } = useAuth();
16
- const router = useCfgRouter();
17
-
18
- useEffect(() => {
19
- if (!isLoading && requireAuth && !isAuthenticated) {
20
- router.push(redirectTo);
21
- }
22
- }, [isAuthenticated, isLoading, router, redirectTo, requireAuth]);
23
-
24
- return { isAuthenticated, isLoading };
25
- };
@@ -1,51 +0,0 @@
1
- import { useSessionStorage } from './useSessionStorage';
2
-
3
- const AUTH_REDIRECT_KEY = 'auth_redirect_url';
4
-
5
- export interface AuthRedirectOptions {
6
- fallbackUrl?: string;
7
- clearOnUse?: boolean;
8
- }
9
-
10
- export const useAuthRedirectManager = (options: AuthRedirectOptions = {}) => {
11
- const { fallbackUrl = '/dashboard', clearOnUse = true } = options;
12
- const [redirectUrl, setRedirectUrl, removeRedirectUrl] = useSessionStorage<string>(AUTH_REDIRECT_KEY, '');
13
-
14
- const setRedirect = (url: string) => {
15
- setRedirectUrl(url);
16
- };
17
-
18
- const getRedirect = () => {
19
- return redirectUrl;
20
- };
21
-
22
- const clearRedirect = () => {
23
- removeRedirectUrl();
24
- };
25
-
26
- const hasRedirect = () => {
27
- return redirectUrl.length > 0;
28
- };
29
-
30
- const getFinalRedirectUrl = () => {
31
- return redirectUrl || fallbackUrl;
32
- };
33
-
34
- const useAndClearRedirect = () => {
35
- const finalUrl = getFinalRedirectUrl();
36
- if (clearOnUse) {
37
- clearRedirect();
38
- }
39
- return finalUrl;
40
- };
41
-
42
- return {
43
- redirectUrl,
44
- setRedirect,
45
- getRedirect,
46
- clearRedirect,
47
- hasRedirect,
48
- getFinalRedirectUrl,
49
- useAndClearRedirect
50
- };
51
- };
@@ -1,49 +0,0 @@
1
- 'use client';
2
-
3
- import { useEffect } from 'react';
4
- import { usePathname } from 'next/navigation';
5
- import { useQueryParams, useCfgRouter } from '@djangocfg/ui-nextjs/hooks';
6
- import { authLogger } from '../../utils/logger';
7
-
8
- export interface UseAutoAuthOptions {
9
- onOTPDetected?: (otp: string) => void;
10
- cleanupUrl?: boolean;
11
- }
12
-
13
- /**
14
- * Hook for automatic authentication from URL query parameters
15
- * Detects OTP from URL and triggers callback
16
- */
17
- export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
18
- const { onOTPDetected, cleanupUrl = true } = options;
19
- const queryParams = useQueryParams();
20
- const pathname = usePathname();
21
- const router = useCfgRouter();
22
-
23
- const isReady = !!pathname && !!queryParams.get('otp');
24
- const hasOTP = !!(queryParams.get('otp'));
25
-
26
- useEffect(() => {
27
- if (!isReady) return;
28
-
29
- const queryOtp = queryParams.get('otp') as string;
30
-
31
- // Handle OTP detection
32
- if (queryOtp && typeof queryOtp === 'string' && queryOtp.length === 6) {
33
- authLogger.info('OTP detected in URL:', queryOtp);
34
- onOTPDetected?.(queryOtp);
35
- }
36
-
37
- // Clean up URL to remove sensitive params for security
38
- if (cleanupUrl && queryOtp) {
39
- const cleanQuery = Object.fromEntries(queryParams.entries());
40
- delete cleanQuery.otp;
41
- router.push(`${pathname}?${new URLSearchParams(cleanQuery).toString()}`);
42
- }
43
- }, [pathname, queryParams, onOTPDetected, cleanupUrl, router]);
44
-
45
- return {
46
- isReady,
47
- hasOTP,
48
- };
49
- };
@@ -1,184 +0,0 @@
1
- 'use client';
2
-
3
- import { useCallback, useState } from 'react';
4
-
5
- import { api } from '@djangocfg/api';
6
- import { useCfgRouter } from '@djangocfg/ui-nextjs/hooks';
7
- import { authLogger } from '../../utils/logger';
8
- import { Analytics, AnalyticsEvent, AnalyticsCategory } from '../../snippets/Analytics';
9
-
10
- export interface UseGithubAuthOptions {
11
- sourceUrl?: string;
12
- onSuccess?: (user: any, isNewUser: boolean) => void;
13
- onError?: (error: string) => void;
14
- redirectUrl?: string;
15
- }
16
-
17
- export interface UseGithubAuthReturn {
18
- isLoading: boolean;
19
- error: string | null;
20
- startGithubAuth: () => Promise<void>;
21
- handleGithubCallback: (code: string, state: string) => Promise<void>;
22
- }
23
-
24
- /**
25
- * Hook for GitHub OAuth authentication flow.
26
- *
27
- * Usage:
28
- * 1. Call startGithubAuth() to redirect user to GitHub
29
- * 2. After GitHub redirects back, call handleGithubCallback(code, state)
30
- *
31
- * @example
32
- * ```tsx
33
- * const { isLoading, error, startGithubAuth } = useGithubAuth({
34
- * onSuccess: (user) => router.push('/dashboard'),
35
- * onError: (error) => console.error(error),
36
- * });
37
- *
38
- * <Button onClick={startGithubAuth} disabled={isLoading}>
39
- * Continue with GitHub
40
- * </Button>
41
- * ```
42
- */
43
- export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuthReturn => {
44
- const { sourceUrl, onSuccess, onError, redirectUrl } = options;
45
- const router = useCfgRouter();
46
-
47
- const [isLoading, setIsLoading] = useState(false);
48
- const [error, setError] = useState<string | null>(null);
49
-
50
- /**
51
- * Start GitHub OAuth flow - redirects user to GitHub authorization page.
52
- */
53
- const startGithubAuth = useCallback(async () => {
54
- setIsLoading(true);
55
- setError(null);
56
-
57
- try {
58
- authLogger.info('Starting GitHub OAuth flow...');
59
-
60
- // Track OAuth start
61
- Analytics.event(AnalyticsEvent.AUTH_OAUTH_START, {
62
- category: AnalyticsCategory.AUTH,
63
- label: 'github',
64
- });
65
-
66
- // Call API to get authorization URL
67
- // The API will auto-generate redirect_uri from config if not provided
68
- const response = await api.cfg_oauth.accountsOauthGithubAuthorizeCreate({
69
- source_url: sourceUrl || (typeof window !== 'undefined' ? window.location.href : ''),
70
- });
71
-
72
- if (!response.authorization_url) {
73
- throw new Error('Failed to get authorization URL');
74
- }
75
-
76
- authLogger.info('Redirecting to GitHub...', response.authorization_url);
77
-
78
- // Store state in sessionStorage for verification on callback
79
- if (typeof window !== 'undefined') {
80
- sessionStorage.setItem('oauth_state', response.state);
81
- sessionStorage.setItem('oauth_provider', 'github');
82
- }
83
-
84
- // Redirect to GitHub
85
- window.location.href = response.authorization_url;
86
-
87
- } catch (err) {
88
- const errorMessage = err instanceof Error ? err.message : 'Failed to start GitHub authentication';
89
- authLogger.error('GitHub OAuth start error:', err);
90
- setError(errorMessage);
91
- onError?.(errorMessage);
92
-
93
- // Track OAuth error
94
- Analytics.event(AnalyticsEvent.AUTH_OAUTH_FAIL, {
95
- category: AnalyticsCategory.AUTH,
96
- label: 'github',
97
- });
98
- } finally {
99
- setIsLoading(false);
100
- }
101
- }, [sourceUrl, onError]);
102
-
103
- /**
104
- * Handle GitHub OAuth callback - exchanges code for JWT tokens.
105
- *
106
- * @param code - Authorization code from GitHub callback
107
- * @param state - State token for CSRF verification
108
- */
109
- const handleGithubCallback = useCallback(async (code: string, state: string) => {
110
- setIsLoading(true);
111
- setError(null);
112
-
113
- try {
114
- authLogger.info('Processing GitHub OAuth callback...');
115
-
116
- // Verify state matches what we stored
117
- if (typeof window !== 'undefined') {
118
- const storedState = sessionStorage.getItem('oauth_state');
119
- if (storedState && storedState !== state) {
120
- throw new Error('Invalid OAuth state - possible CSRF attack');
121
- }
122
- // Clear stored state
123
- sessionStorage.removeItem('oauth_state');
124
- sessionStorage.removeItem('oauth_provider');
125
- }
126
-
127
- // Exchange code for tokens
128
- // The API will auto-generate redirect_uri from config if not provided
129
- const response = await api.cfg_oauth.accountsOauthGithubCallbackCreate({
130
- code,
131
- state,
132
- });
133
-
134
- if (!response.access || !response.refresh) {
135
- throw new Error('Invalid response from OAuth callback');
136
- }
137
-
138
- authLogger.info('GitHub OAuth successful, user:', response.user);
139
-
140
- // Save tokens using API client
141
- api.setToken(response.access, response.refresh);
142
-
143
- // Track successful OAuth
144
- Analytics.event(AnalyticsEvent.AUTH_LOGIN_SUCCESS, {
145
- category: AnalyticsCategory.AUTH,
146
- label: 'github',
147
- });
148
-
149
- // Set user ID for future tracking
150
- if (response.user?.id) {
151
- Analytics.setUser(String(response.user.id));
152
- }
153
-
154
- // Call success callback
155
- onSuccess?.(response.user, response.is_new_user || false);
156
-
157
- // Redirect to dashboard or specified URL
158
- // Use hardPush for full page reload - ensures all React contexts reinitialize
159
- const finalRedirectUrl = redirectUrl || '/dashboard';
160
- router.hardPush(finalRedirectUrl);
161
-
162
- } catch (err) {
163
- const errorMessage = err instanceof Error ? err.message : 'GitHub authentication failed';
164
- authLogger.error('GitHub OAuth callback error:', err);
165
- setError(errorMessage);
166
- onError?.(errorMessage);
167
-
168
- // Track OAuth error
169
- Analytics.event(AnalyticsEvent.AUTH_OAUTH_FAIL, {
170
- category: AnalyticsCategory.AUTH,
171
- label: 'github',
172
- });
173
- } finally {
174
- setIsLoading(false);
175
- }
176
- }, [onSuccess, onError, redirectUrl, router]);
177
-
178
- return {
179
- isLoading,
180
- error,
181
- startGithubAuth,
182
- handleGithubCallback,
183
- };
184
- };