@djangocfg/api 2.1.57 → 2.1.58
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/README.md +125 -9
- package/dist/auth.cjs +1865 -402
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +352 -76
- package/dist/auth.d.ts +352 -76
- package/dist/auth.mjs +1867 -404
- package/dist/auth.mjs.map +1 -1
- package/dist/clients.cjs +1637 -137
- package/dist/clients.cjs.map +1 -1
- package/dist/clients.d.cts +1394 -282
- package/dist/clients.d.ts +1394 -282
- package/dist/clients.mjs +1637 -137
- package/dist/clients.mjs.map +1 -1
- package/dist/hooks.cjs +24 -11
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +88 -21
- package/dist/hooks.d.ts +88 -21
- package/dist/hooks.mjs +24 -11
- package/dist/hooks.mjs.map +1 -1
- package/dist/index.cjs +38 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +94 -21
- package/dist/index.d.ts +94 -21
- package/dist/index.mjs +38 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/auth/context/AccountsContext.tsx +8 -1
- package/src/auth/context/AuthContext.tsx +31 -8
- package/src/auth/context/types.ts +8 -1
- package/src/auth/hooks/index.ts +29 -5
- package/src/auth/hooks/useAuthForm.ts +292 -226
- package/src/auth/hooks/useAuthFormState.ts +60 -0
- package/src/auth/hooks/useAuthValidation.ts +77 -0
- package/src/auth/hooks/useGithubAuth.ts +26 -5
- package/src/auth/hooks/useTwoFactor.ts +239 -0
- package/src/auth/hooks/useTwoFactorSetup.ts +213 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/types/form.ts +194 -0
- package/src/auth/types/index.ts +28 -0
- package/src/clients.ts +10 -0
- package/src/generated/cfg_accounts/_utils/schemas/OAuthTokenResponse.schema.ts +26 -3
- package/src/generated/cfg_accounts/_utils/schemas/OTPVerifyResponse.schema.ts +26 -3
- package/src/generated/cfg_accounts/accounts/client.ts +4 -1
- package/src/generated/cfg_accounts/accounts/models.ts +15 -6
- package/src/generated/cfg_accounts/accounts__oauth/models.ts +16 -7
- package/src/generated/cfg_accounts/client.ts +5 -2
- package/src/generated/cfg_accounts/http.ts +8 -2
- package/src/generated/cfg_accounts/schema.json +47 -19
- package/src/generated/cfg_centrifugo/client.ts +5 -2
- package/src/generated/cfg_centrifugo/http.ts +8 -2
- package/src/generated/cfg_totp/CLAUDE.md +12 -12
- package/src/generated/cfg_totp/_utils/fetchers/index.ts +3 -3
- package/src/generated/cfg_totp/_utils/fetchers/{totp__2fa_management.ts → totp__totp_management.ts} +3 -3
- package/src/generated/cfg_totp/_utils/fetchers/{totp__2fa_setup.ts → totp__totp_setup.ts} +3 -3
- package/src/generated/cfg_totp/_utils/fetchers/{totp__2fa_verification.ts → totp__totp_verification.ts} +3 -3
- package/src/generated/cfg_totp/_utils/hooks/index.ts +3 -3
- package/src/generated/cfg_totp/_utils/hooks/{totp__2fa_management.ts → totp__totp_management.ts} +2 -2
- package/src/generated/cfg_totp/_utils/hooks/{totp__2fa_setup.ts → totp__totp_setup.ts} +2 -2
- package/src/generated/cfg_totp/_utils/hooks/{totp__2fa_verification.ts → totp__totp_verification.ts} +2 -2
- package/src/generated/cfg_totp/_utils/schemas/DeviceList.schema.ts +1 -1
- package/src/generated/cfg_totp/client.ts +14 -11
- package/src/generated/cfg_totp/http.ts +8 -2
- package/src/generated/cfg_totp/index.ts +16 -16
- package/src/generated/cfg_totp/schema.json +8 -7
- package/src/generated/cfg_totp/{totp__2fa_management → totp__totp_management}/client.ts +2 -2
- package/src/generated/cfg_totp/{totp__2fa_management → totp__totp_management}/models.ts +1 -1
- package/src/generated/cfg_totp/{totp__2fa_setup → totp__totp_setup}/client.ts +4 -4
- package/src/generated/cfg_totp/{totp__2fa_verification → totp__totp_verification}/client.ts +2 -2
- package/src/generated/cfg_webpush/client.ts +5 -2
- package/src/generated/cfg_webpush/http.ts +8 -2
- /package/src/generated/cfg_totp/{totp__2fa_management → totp__totp_management}/index.ts +0 -0
- /package/src/generated/cfg_totp/{totp__2fa_setup → totp__totp_setup}/index.ts +0 -0
- /package/src/generated/cfg_totp/{totp__2fa_setup → totp__totp_setup}/models.ts +0 -0
- /package/src/generated/cfg_totp/{totp__2fa_verification → totp__totp_verification}/index.ts +0 -0
- /package/src/generated/cfg_totp/{totp__2fa_verification → totp__totp_verification}/models.ts +0 -0
|
@@ -1,339 +1,405 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect,
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
4
|
|
|
5
5
|
import { useAuth } from '../context';
|
|
6
|
+
import type { AuthChannel, AuthFormReturn, UseAuthFormOptions } from '../types';
|
|
6
7
|
import { authLogger } from '../utils/logger';
|
|
8
|
+
import { useAuthFormState } from './useAuthFormState';
|
|
9
|
+
import { useAuthValidation } from './useAuthValidation';
|
|
7
10
|
import { useAutoAuth } from './useAutoAuth';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
} else if (detectedChannel === 'phone') {
|
|
96
|
-
// E.164 phone validation
|
|
97
|
-
return /^\+[1-9]\d{6,14}$/.test(identifier);
|
|
11
|
+
import { useTwoFactor } from './useTwoFactor';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Complete auth form hook.
|
|
15
|
+
* Composes smaller hooks for state, validation, and submission.
|
|
16
|
+
*/
|
|
17
|
+
export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
|
|
18
|
+
const {
|
|
19
|
+
onIdentifierSuccess,
|
|
20
|
+
onOTPSuccess,
|
|
21
|
+
onError,
|
|
22
|
+
sourceUrl,
|
|
23
|
+
redirectUrl,
|
|
24
|
+
requireTermsAcceptance = false,
|
|
25
|
+
authPath = '/auth',
|
|
26
|
+
} = options;
|
|
27
|
+
|
|
28
|
+
// Compose smaller hooks
|
|
29
|
+
const formState = useAuthFormState();
|
|
30
|
+
const validation = useAuthValidation();
|
|
31
|
+
|
|
32
|
+
// Ref to track auto-submit from URL
|
|
33
|
+
const isAutoSubmitFromUrlRef = useRef(false);
|
|
34
|
+
|
|
35
|
+
// Auth context for API calls and storage
|
|
36
|
+
const {
|
|
37
|
+
requestOTP,
|
|
38
|
+
verifyOTP,
|
|
39
|
+
getSavedEmail,
|
|
40
|
+
saveEmail,
|
|
41
|
+
clearSavedEmail,
|
|
42
|
+
getSavedPhone,
|
|
43
|
+
savePhone,
|
|
44
|
+
clearSavedPhone,
|
|
45
|
+
} = useAuth();
|
|
46
|
+
|
|
47
|
+
// Destructure for convenience
|
|
48
|
+
const {
|
|
49
|
+
identifier,
|
|
50
|
+
channel,
|
|
51
|
+
otp,
|
|
52
|
+
isLoading,
|
|
53
|
+
acceptedTerms,
|
|
54
|
+
twoFactorSessionId,
|
|
55
|
+
twoFactorCode,
|
|
56
|
+
useBackupCode,
|
|
57
|
+
setIdentifier,
|
|
58
|
+
setChannel,
|
|
59
|
+
setOtp,
|
|
60
|
+
setStep,
|
|
61
|
+
setError,
|
|
62
|
+
setIsLoading,
|
|
63
|
+
clearError,
|
|
64
|
+
setTwoFactorSessionId,
|
|
65
|
+
setShouldPrompt2FA,
|
|
66
|
+
setTwoFactorCode,
|
|
67
|
+
setUseBackupCode,
|
|
68
|
+
} = formState;
|
|
69
|
+
|
|
70
|
+
// 2FA verification hook - skip redirect so we can show success screen
|
|
71
|
+
const twoFactor = useTwoFactor({
|
|
72
|
+
onSuccess: () => {
|
|
73
|
+
authLogger.info('2FA verification successful, showing success screen');
|
|
74
|
+
setStep('success');
|
|
75
|
+
onOTPSuccess?.();
|
|
76
|
+
},
|
|
77
|
+
onError: (error) => {
|
|
78
|
+
setError(error);
|
|
79
|
+
onError?.(error);
|
|
80
|
+
},
|
|
81
|
+
redirectUrl,
|
|
82
|
+
skipRedirect: true, // We handle navigation via success step
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const { detectChannelFromIdentifier, validateIdentifier } = validation;
|
|
86
|
+
|
|
87
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
88
|
+
// Storage Helper
|
|
89
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
const saveIdentifierToStorage = useCallback((id: string, ch: AuthChannel) => {
|
|
92
|
+
if (ch === 'email') {
|
|
93
|
+
saveEmail(id);
|
|
94
|
+
clearSavedPhone();
|
|
95
|
+
} else {
|
|
96
|
+
savePhone(id);
|
|
97
|
+
clearSavedEmail();
|
|
98
98
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
}, [saveEmail, savePhone, clearSavedEmail, clearSavedPhone]);
|
|
100
|
+
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
102
|
+
// Effects
|
|
103
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
102
104
|
|
|
103
|
-
// Load saved
|
|
105
|
+
// Load saved identifier on mount
|
|
104
106
|
useEffect(() => {
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
// Prioritize phone over email
|
|
109
|
-
if (
|
|
110
|
-
setIdentifier(
|
|
107
|
+
const savedPhone = getSavedPhone();
|
|
108
|
+
const savedEmail = getSavedEmail();
|
|
109
|
+
|
|
110
|
+
// Prioritize phone over email
|
|
111
|
+
if (savedPhone) {
|
|
112
|
+
setIdentifier(savedPhone);
|
|
111
113
|
setChannel('phone');
|
|
112
|
-
} else if (
|
|
113
|
-
setIdentifier(
|
|
114
|
+
} else if (savedEmail) {
|
|
115
|
+
setIdentifier(savedEmail);
|
|
114
116
|
setChannel('email');
|
|
115
117
|
}
|
|
116
|
-
|
|
117
|
-
if (savedTermsAccepted) {
|
|
118
|
-
setAcceptedTerms(savedTermsAccepted);
|
|
119
|
-
}
|
|
120
|
-
}, [getSavedEmail, getSavedPhone, savedTermsAccepted]);
|
|
118
|
+
}, [getSavedEmail, getSavedPhone, setIdentifier, setChannel]);
|
|
121
119
|
|
|
122
120
|
// Auto-detect channel when identifier changes
|
|
123
121
|
useEffect(() => {
|
|
124
122
|
if (identifier) {
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
127
|
-
setChannel(
|
|
123
|
+
const detected = detectChannelFromIdentifier(identifier);
|
|
124
|
+
if (detected && detected !== channel) {
|
|
125
|
+
setChannel(detected);
|
|
128
126
|
}
|
|
129
127
|
}
|
|
130
|
-
}, [identifier, channel, detectChannelFromIdentifier]);
|
|
131
|
-
|
|
128
|
+
}, [identifier, channel, detectChannelFromIdentifier, setChannel]);
|
|
132
129
|
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
131
|
+
// Submit Handlers
|
|
132
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
135
133
|
|
|
136
134
|
const handleIdentifierSubmit = useCallback(async (e: React.FormEvent) => {
|
|
137
135
|
e.preventDefault();
|
|
138
|
-
|
|
136
|
+
|
|
139
137
|
if (!identifier) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
const msg = channel === 'phone'
|
|
139
|
+
? 'Please enter your phone number'
|
|
140
|
+
: 'Please enter your email address';
|
|
141
|
+
setError(msg);
|
|
142
|
+
onError?.(msg);
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
// Validate identifier format
|
|
147
146
|
if (!validateIdentifier(identifier, channel)) {
|
|
148
|
-
const
|
|
149
|
-
? 'Please enter a valid phone number (e.g., +1234567890)'
|
|
147
|
+
const msg = channel === 'phone'
|
|
148
|
+
? 'Please enter a valid phone number (e.g., +1234567890)'
|
|
150
149
|
: 'Please enter a valid email address';
|
|
151
|
-
setError(
|
|
152
|
-
onError?.(
|
|
150
|
+
setError(msg);
|
|
151
|
+
onError?.(msg);
|
|
153
152
|
return;
|
|
154
153
|
}
|
|
155
154
|
|
|
156
155
|
if (requireTermsAcceptance && !acceptedTerms) {
|
|
157
|
-
const
|
|
158
|
-
setError(
|
|
159
|
-
onError?.(
|
|
156
|
+
const msg = 'Please accept the Terms of Service and Privacy Policy';
|
|
157
|
+
setError(msg);
|
|
158
|
+
onError?.(msg);
|
|
160
159
|
return;
|
|
161
160
|
}
|
|
162
161
|
|
|
163
162
|
setIsLoading(true);
|
|
164
163
|
clearError();
|
|
165
|
-
|
|
164
|
+
|
|
166
165
|
try {
|
|
167
166
|
const result = await requestOTP(identifier, channel, sourceUrl);
|
|
168
|
-
|
|
167
|
+
|
|
169
168
|
if (result.success) {
|
|
170
|
-
|
|
171
|
-
if (channel === 'email') {
|
|
172
|
-
saveEmail(identifier);
|
|
173
|
-
setSavedPhone(''); // Clear phone storage
|
|
174
|
-
} else if (channel === 'phone') {
|
|
175
|
-
savePhone(identifier);
|
|
176
|
-
setSavedEmail(''); // Clear email storage
|
|
177
|
-
}
|
|
178
|
-
setSavedTermsAccepted(true);
|
|
169
|
+
saveIdentifierToStorage(identifier, channel);
|
|
179
170
|
setStep('otp');
|
|
180
171
|
onIdentifierSuccess?.(identifier, channel);
|
|
181
172
|
} else {
|
|
182
173
|
setError(result.message);
|
|
183
174
|
onError?.(result.message);
|
|
184
175
|
}
|
|
185
|
-
} catch
|
|
186
|
-
const
|
|
187
|
-
setError(
|
|
188
|
-
onError?.(
|
|
176
|
+
} catch {
|
|
177
|
+
const msg = 'An unexpected error occurred';
|
|
178
|
+
setError(msg);
|
|
179
|
+
onError?.(msg);
|
|
189
180
|
} finally {
|
|
190
181
|
setIsLoading(false);
|
|
191
182
|
}
|
|
192
|
-
}, [
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
183
|
+
}, [
|
|
184
|
+
identifier, channel, acceptedTerms, requireTermsAcceptance,
|
|
185
|
+
validateIdentifier, requestOTP, saveIdentifierToStorage,
|
|
186
|
+
setError, setIsLoading, setStep, clearError,
|
|
187
|
+
onIdentifierSuccess, onError, sourceUrl,
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
// Core OTP submit - accepts explicit values to avoid stale closures
|
|
191
|
+
const submitOTP = useCallback(async (
|
|
192
|
+
submitIdentifier: string,
|
|
193
|
+
submitOtp: string,
|
|
194
|
+
submitChannel: AuthChannel
|
|
195
|
+
): Promise<boolean> => {
|
|
196
|
+
if (!submitOtp || submitOtp.length < 6) {
|
|
197
|
+
const msg = 'Please enter the 6-digit verification code';
|
|
198
|
+
setError(msg);
|
|
199
|
+
onError?.(msg);
|
|
200
|
+
return false;
|
|
202
201
|
}
|
|
203
202
|
|
|
204
203
|
setIsLoading(true);
|
|
205
204
|
clearError();
|
|
206
205
|
|
|
207
206
|
try {
|
|
208
|
-
|
|
207
|
+
// Skip redirect - we'll show success screen first
|
|
208
|
+
const result = await verifyOTP(submitIdentifier, submitOtp, submitChannel, sourceUrl, redirectUrl, true);
|
|
209
|
+
|
|
210
|
+
// Check if 2FA is required (user has TOTP device)
|
|
211
|
+
if (result.requires_2fa && result.session_id) {
|
|
212
|
+
authLogger.info('2FA required after OTP verification');
|
|
213
|
+
setTwoFactorSessionId(result.session_id);
|
|
214
|
+
setShouldPrompt2FA(result.should_prompt_2fa || false);
|
|
215
|
+
setStep('2fa');
|
|
216
|
+
saveIdentifierToStorage(submitIdentifier, submitChannel);
|
|
217
|
+
return true; // OTP was successful, now need 2FA
|
|
218
|
+
}
|
|
209
219
|
|
|
210
220
|
if (result.success) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
221
|
+
saveIdentifierToStorage(submitIdentifier, submitChannel);
|
|
222
|
+
|
|
223
|
+
// Check if user should be prompted to set up 2FA
|
|
224
|
+
if (result.should_prompt_2fa) {
|
|
225
|
+
authLogger.info('OTP verification successful, prompting 2FA setup');
|
|
226
|
+
setShouldPrompt2FA(true);
|
|
227
|
+
setStep('2fa-setup');
|
|
228
|
+
onOTPSuccess?.();
|
|
229
|
+
return true;
|
|
218
230
|
}
|
|
231
|
+
|
|
232
|
+
authLogger.info('OTP verification successful, showing success screen');
|
|
233
|
+
setStep('success');
|
|
219
234
|
onOTPSuccess?.();
|
|
235
|
+
return true;
|
|
220
236
|
} else {
|
|
221
237
|
setError(result.message);
|
|
222
238
|
onError?.(result.message);
|
|
239
|
+
return false;
|
|
223
240
|
}
|
|
224
|
-
} catch
|
|
225
|
-
const
|
|
226
|
-
setError(
|
|
227
|
-
onError?.(
|
|
241
|
+
} catch {
|
|
242
|
+
const msg = 'An unexpected error occurred';
|
|
243
|
+
setError(msg);
|
|
244
|
+
onError?.(msg);
|
|
245
|
+
return false;
|
|
228
246
|
} finally {
|
|
229
247
|
setIsLoading(false);
|
|
230
248
|
}
|
|
231
|
-
}, [
|
|
249
|
+
}, [verifyOTP, saveIdentifierToStorage, setError, setIsLoading, clearError, onOTPSuccess, onError, sourceUrl, redirectUrl, setTwoFactorSessionId, setShouldPrompt2FA, setStep]);
|
|
250
|
+
|
|
251
|
+
const handleOTPSubmit = useCallback(async (e: React.FormEvent) => {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
await submitOTP(identifier, otp, channel);
|
|
254
|
+
}, [identifier, otp, channel, submitOTP]);
|
|
232
255
|
|
|
233
256
|
const handleResendOTP = useCallback(async () => {
|
|
234
257
|
setIsLoading(true);
|
|
235
258
|
clearError();
|
|
236
|
-
|
|
259
|
+
|
|
237
260
|
try {
|
|
238
261
|
const result = await requestOTP(identifier, channel, sourceUrl);
|
|
239
|
-
|
|
262
|
+
|
|
240
263
|
if (result.success) {
|
|
241
|
-
|
|
242
|
-
if (channel === 'email') {
|
|
243
|
-
saveEmail(identifier);
|
|
244
|
-
setSavedPhone(''); // Clear phone storage
|
|
245
|
-
} else if (channel === 'phone') {
|
|
246
|
-
savePhone(identifier);
|
|
247
|
-
setSavedEmail(''); // Clear email storage
|
|
248
|
-
}
|
|
264
|
+
saveIdentifierToStorage(identifier, channel);
|
|
249
265
|
setOtp('');
|
|
250
266
|
} else {
|
|
251
267
|
setError(result.message);
|
|
252
268
|
onError?.(result.message);
|
|
253
269
|
}
|
|
254
|
-
} catch
|
|
255
|
-
const
|
|
256
|
-
setError(
|
|
257
|
-
onError?.(
|
|
270
|
+
} catch {
|
|
271
|
+
const msg = 'Failed to resend verification code';
|
|
272
|
+
setError(msg);
|
|
273
|
+
onError?.(msg);
|
|
258
274
|
} finally {
|
|
259
275
|
setIsLoading(false);
|
|
260
276
|
}
|
|
261
|
-
}, [identifier, channel, requestOTP,
|
|
277
|
+
}, [identifier, channel, requestOTP, saveIdentifierToStorage, setOtp, setError, setIsLoading, clearError, onError, sourceUrl]);
|
|
262
278
|
|
|
263
279
|
const handleBackToIdentifier = useCallback(() => {
|
|
264
280
|
setStep('identifier');
|
|
265
281
|
clearError();
|
|
266
|
-
}, [clearError]);
|
|
282
|
+
}, [setStep, clearError]);
|
|
267
283
|
|
|
268
284
|
const forceOTPStep = useCallback(() => {
|
|
269
285
|
setStep('otp');
|
|
270
286
|
clearError();
|
|
271
|
-
}, [clearError]);
|
|
287
|
+
}, [setStep, clearError]);
|
|
288
|
+
|
|
289
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
290
|
+
// 2FA Handlers
|
|
291
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
const handle2FASubmit = useCallback(async (e: React.FormEvent) => {
|
|
294
|
+
e.preventDefault();
|
|
272
295
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
296
|
+
if (!twoFactorSessionId) {
|
|
297
|
+
const msg = 'Missing 2FA session';
|
|
298
|
+
setError(msg);
|
|
299
|
+
onError?.(msg);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (useBackupCode) {
|
|
304
|
+
await twoFactor.verifyBackupCode(twoFactorSessionId, twoFactorCode);
|
|
305
|
+
} else {
|
|
306
|
+
await twoFactor.verifyTOTP(twoFactorSessionId, twoFactorCode);
|
|
307
|
+
}
|
|
308
|
+
}, [twoFactorSessionId, twoFactorCode, useBackupCode, twoFactor, setError, onError]);
|
|
309
|
+
|
|
310
|
+
const handleUseBackupCode = useCallback(() => {
|
|
311
|
+
setUseBackupCode(true);
|
|
312
|
+
setTwoFactorCode('');
|
|
313
|
+
clearError();
|
|
314
|
+
}, [setUseBackupCode, setTwoFactorCode, clearError]);
|
|
315
|
+
|
|
316
|
+
const handleUseTOTP = useCallback(() => {
|
|
317
|
+
setUseBackupCode(false);
|
|
318
|
+
setTwoFactorCode('');
|
|
319
|
+
clearError();
|
|
320
|
+
}, [setUseBackupCode, setTwoFactorCode, clearError]);
|
|
321
|
+
|
|
322
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
323
|
+
// Auto-auth from URL
|
|
324
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
277
325
|
|
|
278
|
-
// Auto-detect OTP from URL query parameters (only on auth page)
|
|
279
326
|
useAutoAuth({
|
|
280
327
|
allowedPaths: [authPath],
|
|
281
|
-
onOTPDetected: (
|
|
282
|
-
|
|
328
|
+
onOTPDetected: (detectedOtp: string) => {
|
|
329
|
+
if (isAutoSubmitFromUrlRef.current || isLoading) return;
|
|
330
|
+
isAutoSubmitFromUrlRef.current = true;
|
|
331
|
+
|
|
332
|
+
authLogger.info('OTP detected from URL, auto-submitting');
|
|
283
333
|
|
|
284
|
-
// Get saved identifier from auth context
|
|
285
|
-
const savedEmail = getSavedEmail();
|
|
286
334
|
const savedPhone = getSavedPhone();
|
|
335
|
+
const savedEmail = getSavedEmail();
|
|
336
|
+
|
|
337
|
+
let autoIdentifier = '';
|
|
338
|
+
let autoChannel: AuthChannel = 'email';
|
|
287
339
|
|
|
288
|
-
// Prioritize phone over email if both exist
|
|
289
340
|
if (savedPhone) {
|
|
290
|
-
|
|
291
|
-
|
|
341
|
+
autoIdentifier = savedPhone;
|
|
342
|
+
autoChannel = 'phone';
|
|
292
343
|
} else if (savedEmail) {
|
|
293
|
-
|
|
294
|
-
|
|
344
|
+
autoIdentifier = savedEmail;
|
|
345
|
+
autoChannel = 'email';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!autoIdentifier) {
|
|
349
|
+
authLogger.warn('No saved identifier found for auto-submit');
|
|
350
|
+
isAutoSubmitFromUrlRef.current = false;
|
|
351
|
+
return;
|
|
295
352
|
}
|
|
296
353
|
|
|
297
|
-
//
|
|
298
|
-
|
|
354
|
+
// Update UI state
|
|
355
|
+
setIdentifier(autoIdentifier);
|
|
356
|
+
setChannel(autoChannel);
|
|
357
|
+
setOtp(detectedOtp);
|
|
299
358
|
setStep('otp');
|
|
300
359
|
|
|
301
|
-
//
|
|
302
|
-
setTimeout(() => {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
360
|
+
// Submit with explicit values
|
|
361
|
+
setTimeout(async () => {
|
|
362
|
+
try {
|
|
363
|
+
await submitOTP(autoIdentifier, detectedOtp, autoChannel);
|
|
364
|
+
} finally {
|
|
365
|
+
isAutoSubmitFromUrlRef.current = false;
|
|
366
|
+
}
|
|
367
|
+
}, 100);
|
|
306
368
|
},
|
|
307
369
|
cleanupUrl: true,
|
|
308
370
|
});
|
|
309
371
|
|
|
372
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
373
|
+
// Return
|
|
374
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
375
|
+
|
|
310
376
|
return {
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
isLoading,
|
|
316
|
-
acceptedTerms,
|
|
317
|
-
step,
|
|
318
|
-
error,
|
|
319
|
-
|
|
320
|
-
// Form handlers
|
|
321
|
-
setIdentifier,
|
|
322
|
-
setChannel,
|
|
323
|
-
setOtp,
|
|
324
|
-
setAcceptedTerms: handleAcceptedTermsChange,
|
|
325
|
-
setError,
|
|
326
|
-
clearError,
|
|
327
|
-
|
|
328
|
-
// Auth handlers
|
|
377
|
+
// State
|
|
378
|
+
...formState,
|
|
379
|
+
|
|
380
|
+
// Submit handlers
|
|
329
381
|
handleIdentifierSubmit,
|
|
330
382
|
handleOTPSubmit,
|
|
331
383
|
handleResendOTP,
|
|
332
384
|
handleBackToIdentifier,
|
|
333
385
|
forceOTPStep,
|
|
334
|
-
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
386
|
+
|
|
387
|
+
// 2FA handlers
|
|
388
|
+
handle2FASubmit,
|
|
389
|
+
handleUseBackupCode,
|
|
390
|
+
handleUseTOTP,
|
|
391
|
+
|
|
392
|
+
// Validation
|
|
393
|
+
...validation,
|
|
394
|
+
|
|
395
|
+
// Auto-submit ref
|
|
396
|
+
isAutoSubmittingFromUrl: isAutoSubmitFromUrlRef,
|
|
397
|
+
|
|
398
|
+
// 2FA state from hook (for loading indicator)
|
|
399
|
+
is2FALoading: twoFactor.isLoading,
|
|
400
|
+
twoFactorWarning: twoFactor.warning,
|
|
338
401
|
};
|
|
339
|
-
};
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Re-export types for convenience
|
|
405
|
+
export type { AuthFormReturn, UseAuthFormOptions } from '../types';
|