@djangocfg/api 2.1.226 → 2.1.228

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 (56) hide show
  1. package/README.md +8 -9
  2. package/dist/auth-server.cjs +4 -9
  3. package/dist/auth-server.cjs.map +1 -1
  4. package/dist/auth-server.mjs +4 -9
  5. package/dist/auth-server.mjs.map +1 -1
  6. package/dist/auth.cjs +120 -158
  7. package/dist/auth.cjs.map +1 -1
  8. package/dist/auth.d.cts +120 -177
  9. package/dist/auth.d.ts +120 -177
  10. package/dist/auth.mjs +149 -191
  11. package/dist/auth.mjs.map +1 -1
  12. package/dist/clients.cjs +5 -11
  13. package/dist/clients.cjs.map +1 -1
  14. package/dist/clients.d.cts +218 -219
  15. package/dist/clients.d.ts +218 -219
  16. package/dist/clients.mjs +5 -11
  17. package/dist/clients.mjs.map +1 -1
  18. package/dist/hooks.cjs +4 -9
  19. package/dist/hooks.cjs.map +1 -1
  20. package/dist/hooks.d.cts +70 -91
  21. package/dist/hooks.d.ts +70 -91
  22. package/dist/hooks.mjs +4 -9
  23. package/dist/hooks.mjs.map +1 -1
  24. package/dist/index.cjs +5 -11
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +116 -106
  27. package/dist/index.d.ts +116 -106
  28. package/dist/index.mjs +5 -11
  29. package/dist/index.mjs.map +1 -1
  30. package/package.json +2 -2
  31. package/src/_api/generated/cfg_accounts/_utils/schemas/OTPErrorResponse.schema.ts +24 -2
  32. package/src/_api/generated/cfg_accounts/_utils/schemas/OTPRequestRequest.schema.ts +0 -2
  33. package/src/_api/generated/cfg_accounts/_utils/schemas/OTPVerifyRequest.schema.ts +0 -2
  34. package/src/_api/generated/cfg_accounts/accounts/client.ts +1 -1
  35. package/src/_api/generated/cfg_accounts/accounts/models.ts +25 -26
  36. package/src/_api/generated/cfg_accounts/accounts__auth/models.ts +5 -5
  37. package/src/_api/generated/cfg_accounts/accounts__oauth/models.ts +42 -42
  38. package/src/_api/generated/cfg_accounts/accounts__user_profile/models.ts +23 -23
  39. package/src/_api/generated/cfg_accounts/enums.ts +0 -10
  40. package/src/_api/generated/cfg_accounts/schema.json +31 -25
  41. package/src/_api/generated/cfg_centrifugo/centrifugo__centrifugo_admin_api/models.ts +57 -57
  42. package/src/_api/generated/cfg_centrifugo/centrifugo__centrifugo_monitoring/models.ts +24 -24
  43. package/src/_api/generated/cfg_centrifugo/centrifugo__centrifugo_testing/models.ts +14 -14
  44. package/src/_api/generated/cfg_totp/totp__backup_codes/models.ts +14 -14
  45. package/src/_api/generated/cfg_totp/totp__totp_setup/models.ts +10 -10
  46. package/src/_api/generated/cfg_totp/totp__totp_verification/models.ts +8 -8
  47. package/src/auth/context/AccountsContext.tsx +6 -2
  48. package/src/auth/context/AuthContext.tsx +32 -39
  49. package/src/auth/context/types.ts +5 -9
  50. package/src/auth/hooks/index.ts +1 -1
  51. package/src/auth/hooks/useAuthForm.ts +42 -75
  52. package/src/auth/hooks/useAuthFormState.ts +35 -6
  53. package/src/auth/hooks/useAuthValidation.ts +5 -65
  54. package/src/auth/hooks/useTwoFactor.ts +17 -2
  55. package/src/auth/types/form.ts +25 -70
  56. package/src/auth/types/index.ts +2 -6
@@ -9,7 +9,7 @@ import { useAuthValidation } from './useAuthValidation';
9
9
  import { useAutoAuth } from './useAutoAuth';
10
10
  import { useTwoFactor } from './useTwoFactor';
11
11
 
12
- import type { AuthChannel, AuthFormReturn, UseAuthFormOptions } from '../types';
12
+ import type { AuthFormReturn, UseAuthFormOptions } from '../types';
13
13
  /**
14
14
  * Complete auth form hook.
15
15
  * Composes smaller hooks for state, validation, and submission.
@@ -39,16 +39,11 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
39
39
  verifyOTP,
40
40
  getSavedEmail,
41
41
  saveEmail,
42
- clearSavedEmail,
43
- getSavedPhone,
44
- savePhone,
45
- clearSavedPhone,
46
42
  } = useAuth();
47
43
 
48
44
  // Destructure for convenience
49
45
  const {
50
46
  identifier,
51
- channel,
52
47
  otp,
53
48
  isLoading,
54
49
  acceptedTerms,
@@ -56,7 +51,6 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
56
51
  twoFactorCode,
57
52
  useBackupCode,
58
53
  setIdentifier,
59
- setChannel,
60
54
  setOtp,
61
55
  setStep,
62
56
  setError,
@@ -66,6 +60,7 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
66
60
  setShouldPrompt2FA,
67
61
  setTwoFactorCode,
68
62
  setUseBackupCode,
63
+ startRateLimitCountdown,
69
64
  } = formState;
70
65
 
71
66
  // 2FA verification hook - skip redirect so we can show success screen
@@ -83,21 +78,15 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
83
78
  skipRedirect: true, // We handle navigation via success step
84
79
  });
85
80
 
86
- const { detectChannelFromIdentifier, validateIdentifier } = validation;
81
+ const { validateIdentifier } = validation;
87
82
 
88
83
  // ─────────────────────────────────────────────────────────────────────────
89
84
  // Storage Helper
90
85
  // ─────────────────────────────────────────────────────────────────────────
91
86
 
92
- const saveIdentifierToStorage = useCallback((id: string, ch: AuthChannel) => {
93
- if (ch === 'email') {
94
- saveEmail(id);
95
- clearSavedPhone();
96
- } else {
97
- savePhone(id);
98
- clearSavedEmail();
99
- }
100
- }, [saveEmail, savePhone, clearSavedEmail, clearSavedPhone]);
87
+ const saveIdentifierToStorage = useCallback((id: string) => {
88
+ saveEmail(id);
89
+ }, [saveEmail]);
101
90
 
102
91
  // ─────────────────────────────────────────────────────────────────────────
103
92
  // Effects
@@ -105,28 +94,12 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
105
94
 
106
95
  // Load saved identifier on mount
107
96
  useEffect(() => {
108
- const savedPhone = getSavedPhone();
109
97
  const savedEmail = getSavedEmail();
110
-
111
- // Prioritize phone over email
112
- if (savedPhone) {
113
- setIdentifier(savedPhone);
114
- setChannel('phone');
115
- } else if (savedEmail) {
98
+ if (savedEmail) {
116
99
  setIdentifier(savedEmail);
117
- setChannel('email');
118
100
  }
119
- }, [getSavedEmail, getSavedPhone, setIdentifier, setChannel]);
120
-
121
- // Auto-detect channel when identifier changes
122
- useEffect(() => {
123
- if (identifier) {
124
- const detected = detectChannelFromIdentifier(identifier);
125
- if (detected && detected !== channel) {
126
- setChannel(detected);
127
- }
128
- }
129
- }, [identifier, channel, detectChannelFromIdentifier, setChannel]);
101
+ // eslint-disable-next-line react-hooks/exhaustive-deps
102
+ }, []);
130
103
 
131
104
  // ─────────────────────────────────────────────────────────────────────────
132
105
  // Submit Handlers
@@ -136,18 +109,14 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
136
109
  e.preventDefault();
137
110
 
138
111
  if (!identifier) {
139
- const msg = channel === 'phone'
140
- ? 'Please enter your phone number'
141
- : 'Please enter your email address';
112
+ const msg = 'Please enter your email address';
142
113
  setError(msg);
143
114
  onError?.(msg);
144
115
  return;
145
116
  }
146
117
 
147
- if (!validateIdentifier(identifier, channel)) {
148
- const msg = channel === 'phone'
149
- ? 'Please enter a valid phone number (e.g., +1234567890)'
150
- : 'Please enter a valid email address';
118
+ if (!validateIdentifier(identifier)) {
119
+ const msg = 'Please enter a valid email address';
151
120
  setError(msg);
152
121
  onError?.(msg);
153
122
  return;
@@ -164,15 +133,20 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
164
133
  clearError();
165
134
 
166
135
  try {
167
- const result = await requestOTP(identifier, channel, sourceUrl);
136
+ const result = await requestOTP(identifier, sourceUrl);
168
137
 
169
138
  if (result.success) {
170
- saveIdentifierToStorage(identifier, channel);
139
+ saveIdentifierToStorage(identifier);
171
140
  setStep('otp');
172
- onIdentifierSuccess?.(identifier, channel);
141
+ onIdentifierSuccess?.(identifier);
173
142
  } else {
174
- setError(result.message);
175
- onError?.(result.message);
143
+ if (result.retryAfter) {
144
+ startRateLimitCountdown(result.retryAfter);
145
+ clearError();
146
+ } else {
147
+ setError(result.message);
148
+ onError?.(result.message);
149
+ }
176
150
  }
177
151
  } catch {
178
152
  const msg = 'An unexpected error occurred';
@@ -182,17 +156,16 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
182
156
  setIsLoading(false);
183
157
  }
184
158
  }, [
185
- identifier, channel, acceptedTerms, requireTermsAcceptance,
159
+ identifier, acceptedTerms, requireTermsAcceptance,
186
160
  validateIdentifier, requestOTP, saveIdentifierToStorage,
187
161
  setError, setIsLoading, setStep, clearError,
188
- onIdentifierSuccess, onError, sourceUrl,
162
+ startRateLimitCountdown, onIdentifierSuccess, onError, sourceUrl,
189
163
  ]);
190
164
 
191
165
  // Core OTP submit - accepts explicit values to avoid stale closures
192
166
  const submitOTP = useCallback(async (
193
167
  submitIdentifier: string,
194
168
  submitOtp: string,
195
- submitChannel: AuthChannel
196
169
  ): Promise<boolean> => {
197
170
  if (!submitOtp || submitOtp.length < 6) {
198
171
  const msg = 'Please enter the 6-digit verification code';
@@ -206,7 +179,7 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
206
179
 
207
180
  try {
208
181
  // Skip redirect - we'll show success screen first
209
- const result = await verifyOTP(submitIdentifier, submitOtp, submitChannel, sourceUrl, redirectUrl, true);
182
+ const result = await verifyOTP(submitIdentifier, submitOtp, sourceUrl, redirectUrl, true);
210
183
 
211
184
  // Check if 2FA is required (user has TOTP device)
212
185
  if (result.requires_2fa && result.session_id) {
@@ -214,12 +187,12 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
214
187
  setTwoFactorSessionId(result.session_id);
215
188
  setShouldPrompt2FA(result.should_prompt_2fa || false);
216
189
  setStep('2fa');
217
- saveIdentifierToStorage(submitIdentifier, submitChannel);
190
+ saveIdentifierToStorage(submitIdentifier);
218
191
  return true; // OTP was successful, now need 2FA
219
192
  }
220
193
 
221
194
  if (result.success) {
222
- saveIdentifierToStorage(submitIdentifier, submitChannel);
195
+ saveIdentifierToStorage(submitIdentifier);
223
196
 
224
197
  // Check if user should be prompted to set up 2FA
225
198
  // Only show 2FA setup prompt if enable2FASetup is true (default)
@@ -257,22 +230,27 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
257
230
 
258
231
  const handleOTPSubmit = useCallback(async (e: React.FormEvent) => {
259
232
  e.preventDefault();
260
- await submitOTP(identifier, otp, channel);
261
- }, [identifier, otp, channel, submitOTP]);
233
+ await submitOTP(identifier, otp);
234
+ }, [identifier, otp, submitOTP]);
262
235
 
263
236
  const handleResendOTP = useCallback(async () => {
264
237
  setIsLoading(true);
265
238
  clearError();
266
239
 
267
240
  try {
268
- const result = await requestOTP(identifier, channel, sourceUrl);
241
+ const result = await requestOTP(identifier, sourceUrl);
269
242
 
270
243
  if (result.success) {
271
- saveIdentifierToStorage(identifier, channel);
244
+ saveIdentifierToStorage(identifier);
272
245
  setOtp('');
273
246
  } else {
274
- setError(result.message);
275
- onError?.(result.message);
247
+ if (result.retryAfter) {
248
+ startRateLimitCountdown(result.retryAfter);
249
+ clearError(); // countdown in rateLimitLabel is enough feedback
250
+ } else {
251
+ setError(result.message);
252
+ onError?.(result.message);
253
+ }
276
254
  }
277
255
  } catch {
278
256
  const msg = 'Failed to resend verification code';
@@ -281,7 +259,7 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
281
259
  } finally {
282
260
  setIsLoading(false);
283
261
  }
284
- }, [identifier, channel, requestOTP, saveIdentifierToStorage, setOtp, setError, setIsLoading, clearError, onError, sourceUrl]);
262
+ }, [identifier, requestOTP, saveIdentifierToStorage, setOtp, setError, setIsLoading, clearError, startRateLimitCountdown, onError, sourceUrl]);
285
263
 
286
264
  const handleBackToIdentifier = useCallback(() => {
287
265
  setStep('identifier');
@@ -338,19 +316,8 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
338
316
 
339
317
  authLogger.info('OTP detected from URL, auto-submitting');
340
318
 
341
- const savedPhone = getSavedPhone();
342
319
  const savedEmail = getSavedEmail();
343
-
344
- let autoIdentifier = '';
345
- let autoChannel: AuthChannel = 'email';
346
-
347
- if (savedPhone) {
348
- autoIdentifier = savedPhone;
349
- autoChannel = 'phone';
350
- } else if (savedEmail) {
351
- autoIdentifier = savedEmail;
352
- autoChannel = 'email';
353
- }
320
+ const autoIdentifier = savedEmail || '';
354
321
 
355
322
  if (!autoIdentifier) {
356
323
  authLogger.warn('No saved identifier found for auto-submit');
@@ -360,14 +327,13 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
360
327
 
361
328
  // Update UI state
362
329
  setIdentifier(autoIdentifier);
363
- setChannel(autoChannel);
364
330
  setOtp(detectedOtp);
365
331
  setStep('otp');
366
332
 
367
333
  // Submit with explicit values
368
334
  setTimeout(async () => {
369
335
  try {
370
- await submitOTP(autoIdentifier, detectedOtp, autoChannel);
336
+ await submitOTP(autoIdentifier, detectedOtp);
371
337
  } finally {
372
338
  isAutoSubmitFromUrlRef.current = false;
373
339
  }
@@ -405,6 +371,7 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
405
371
  // 2FA state from hook (for loading indicator)
406
372
  is2FALoading: twoFactor.isLoading,
407
373
  twoFactorWarning: twoFactor.warning,
374
+ twoFactorAttemptsRemaining: twoFactor.attemptsRemaining,
408
375
  };
409
376
  };
410
377
 
@@ -1,8 +1,14 @@
1
1
  "use client"
2
2
 
3
- import { useCallback, useState } from 'react';
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
4
 
5
- import type { AuthChannel, AuthFormState, AuthFormStateHandlers, AuthStep } from '../types';
5
+ import type { AuthFormState, AuthFormStateHandlers, AuthStep } from '../types';
6
+
7
+ const formatCountdown = (s: number): string => {
8
+ if (s <= 0) return '';
9
+ const m = Math.floor(s / 60);
10
+ return m > 0 ? `${m}:${String(s % 60).padStart(2, '0')}` : `${s}s`;
11
+ };
6
12
 
7
13
  export interface UseAuthFormStateReturn extends AuthFormState, AuthFormStateHandlers {}
8
14
 
@@ -12,10 +18,8 @@ export interface UseAuthFormStateReturn extends AuthFormState, AuthFormStateHand
12
18
  */
13
19
  export const useAuthFormState = (
14
20
  initialIdentifier = '',
15
- initialChannel: AuthChannel = 'email'
16
21
  ): UseAuthFormStateReturn => {
17
22
  const [identifier, setIdentifier] = useState(initialIdentifier);
18
- const [channel, setChannel] = useState<AuthChannel>(initialChannel);
19
23
  const [otp, setOtp] = useState('');
20
24
  const [isLoading, setIsLoading] = useState(false);
21
25
  const [acceptedTerms, setAcceptedTerms] = useState(true);
@@ -28,12 +32,34 @@ export const useAuthFormState = (
28
32
  const [twoFactorCode, setTwoFactorCode] = useState('');
29
33
  const [useBackupCode, setUseBackupCode] = useState(false);
30
34
 
35
+ // Rate limit countdown
36
+ const [rateLimitSeconds, setRateLimitSeconds] = useState(0);
37
+ const rateLimitTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
38
+
39
+ const startRateLimitCountdown = useCallback((seconds: number) => {
40
+ if (rateLimitTimerRef.current) clearInterval(rateLimitTimerRef.current);
41
+ setRateLimitSeconds(seconds);
42
+ rateLimitTimerRef.current = setInterval(() => {
43
+ setRateLimitSeconds((prev) => {
44
+ if (prev <= 1) {
45
+ clearInterval(rateLimitTimerRef.current!);
46
+ rateLimitTimerRef.current = null;
47
+ return 0;
48
+ }
49
+ return prev - 1;
50
+ });
51
+ }, 1000);
52
+ }, []);
53
+
54
+ useEffect(() => () => {
55
+ if (rateLimitTimerRef.current) clearInterval(rateLimitTimerRef.current);
56
+ }, []);
57
+
31
58
  const clearError = useCallback(() => setError(''), []);
32
59
 
33
60
  return {
34
61
  // State
35
62
  identifier,
36
- channel,
37
63
  otp,
38
64
  isLoading,
39
65
  acceptedTerms,
@@ -43,9 +69,11 @@ export const useAuthFormState = (
43
69
  shouldPrompt2FA,
44
70
  twoFactorCode,
45
71
  useBackupCode,
72
+ rateLimitSeconds,
73
+ isRateLimited: rateLimitSeconds > 0,
74
+ rateLimitLabel: formatCountdown(rateLimitSeconds),
46
75
  // Handlers
47
76
  setIdentifier,
48
- setChannel,
49
77
  setOtp,
50
78
  setAcceptedTerms,
51
79
  setError,
@@ -56,5 +84,6 @@ export const useAuthFormState = (
56
84
  setShouldPrompt2FA,
57
85
  setTwoFactorCode,
58
86
  setUseBackupCode,
87
+ startRateLimitCountdown,
59
88
  };
60
89
  };
@@ -2,76 +2,16 @@
2
2
 
3
3
  import { useCallback } from 'react';
4
4
 
5
- import type { AuthChannel, AuthFormValidation } from '../types';
5
+ import type { AuthFormValidation } from '../types';
6
6
 
7
- // Email regex - RFC 5322 simplified
8
7
  const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9
8
 
10
- // Phone regex - E.164 format (+[country][number], 7-15 digits)
11
- const PHONE_REGEX = /^\+[1-9]\d{6,14}$/;
12
-
13
- /**
14
- * Hook for auth identifier validation.
15
- * Pure functions - no state, no side effects.
16
- */
17
9
  export const useAuthValidation = (): AuthFormValidation => {
18
- /**
19
- * Detect channel type from identifier string.
20
- * Returns 'email' if contains @, 'phone' if starts with + and matches E.164.
21
- */
22
- const detectChannelFromIdentifier = useCallback((id: string): AuthChannel | null => {
23
- if (!id) return null;
24
-
25
- // Email detection - contains @
26
- if (id.includes('@')) {
27
- return 'email';
28
- }
29
-
30
- // Phone detection - starts with + and matches E.164 format
31
- if (id.startsWith('+') && PHONE_REGEX.test(id)) {
32
- return 'phone';
33
- }
34
-
35
- return null;
10
+ const validateIdentifier = useCallback((id: string): boolean => {
11
+ return EMAIL_REGEX.test(id);
36
12
  }, []);
37
13
 
38
- /**
39
- * Validate identifier format based on channel type.
40
- */
41
- const validateIdentifier = useCallback((id: string, channelType?: AuthChannel): boolean => {
42
- if (!id) return false;
43
-
44
- const channel = channelType || detectChannelFromIdentifier(id);
45
-
46
- if (channel === 'email') {
47
- return EMAIL_REGEX.test(id);
48
- }
49
-
50
- if (channel === 'phone') {
51
- return PHONE_REGEX.test(id);
52
- }
53
-
54
- return false;
55
- }, [detectChannelFromIdentifier]);
56
-
57
- return {
58
- detectChannelFromIdentifier,
59
- validateIdentifier,
60
- };
14
+ return { validateIdentifier };
61
15
  };
62
16
 
63
- // Export standalone functions for use outside React
64
- export const detectChannelFromIdentifier = (id: string): AuthChannel | null => {
65
- if (!id) return null;
66
- if (id.includes('@')) return 'email';
67
- if (id.startsWith('+') && PHONE_REGEX.test(id)) return 'phone';
68
- return null;
69
- };
70
-
71
- export const validateIdentifier = (id: string, channelType?: AuthChannel): boolean => {
72
- if (!id) return false;
73
- const channel = channelType || detectChannelFromIdentifier(id);
74
- if (channel === 'email') return EMAIL_REGEX.test(id);
75
- if (channel === 'phone') return PHONE_REGEX.test(id);
76
- return false;
77
- };
17
+ export const validateIdentifier = (id: string): boolean => EMAIL_REGEX.test(id);
@@ -3,6 +3,7 @@
3
3
  import { useCallback, useState } from 'react';
4
4
 
5
5
  import { apiAccounts, apiTotp } from '../../clients';
6
+ import { APIError } from '../../_api/generated/cfg_totp/errors';
6
7
  import { Analytics, AnalyticsCategory, AnalyticsEvent } from '../utils/analytics';
7
8
  import { authLogger } from '../utils/logger';
8
9
  import { useCfgRouter } from './useCfgRouter';
@@ -27,6 +28,8 @@ export interface UseTwoFactorReturn {
27
28
  warning: string | null;
28
29
  /** Remaining backup codes (if backup code was used) */
29
30
  remainingBackupCodes: number | null;
31
+ /** Remaining verification attempts before lockout */
32
+ attemptsRemaining: number | null;
30
33
  /** Verify TOTP code */
31
34
  verifyTOTP: (sessionId: string, code: string) => Promise<boolean>;
32
35
  /** Verify backup code */
@@ -68,6 +71,7 @@ export const useTwoFactor = (options: UseTwoFactorOptions = {}): UseTwoFactorRet
68
71
  const [error, setError] = useState<string | null>(null);
69
72
  const [warning, setWarning] = useState<string | null>(null);
70
73
  const [remainingBackupCodes, setRemainingBackupCodes] = useState<number | null>(null);
74
+ const [attemptsRemaining, setAttemptsRemaining] = useState<number | null>(null);
71
75
 
72
76
  const clearError = useCallback(() => {
73
77
  setError(null);
@@ -155,8 +159,13 @@ export const useTwoFactor = (options: UseTwoFactorOptions = {}): UseTwoFactorRet
155
159
  return true;
156
160
 
157
161
  } catch (err) {
158
- const errorMessage = err instanceof Error ? err.message : 'Invalid verification code';
159
162
  authLogger.error('2FA TOTP verification error:', err);
163
+ const errorMessage = err instanceof APIError
164
+ ? (err.response?.error || err.response?.detail || err.response?.message || err.errorMessage)
165
+ : (err instanceof Error ? err.message : 'Invalid verification code');
166
+ if (err instanceof APIError && typeof err.response?.attempts_remaining === 'number') {
167
+ setAttemptsRemaining(err.response.attempts_remaining);
168
+ }
160
169
  setError(errorMessage);
161
170
  onError?.(errorMessage);
162
171
 
@@ -209,8 +218,13 @@ export const useTwoFactor = (options: UseTwoFactorOptions = {}): UseTwoFactorRet
209
218
  return true;
210
219
 
211
220
  } catch (err) {
212
- const errorMessage = err instanceof Error ? err.message : 'Invalid backup code';
213
221
  authLogger.error('2FA backup code verification error:', err);
222
+ const errorMessage = err instanceof APIError
223
+ ? (err.response?.error || err.response?.detail || err.response?.message || err.errorMessage)
224
+ : (err instanceof Error ? err.message : 'Invalid backup code');
225
+ if (err instanceof APIError && typeof err.response?.attempts_remaining === 'number') {
226
+ setAttemptsRemaining(err.response.attempts_remaining);
227
+ }
214
228
  setError(errorMessage);
215
229
  onError?.(errorMessage);
216
230
 
@@ -231,6 +245,7 @@ export const useTwoFactor = (options: UseTwoFactorOptions = {}): UseTwoFactorRet
231
245
  error,
232
246
  warning,
233
247
  remainingBackupCodes,
248
+ attemptsRemaining,
234
249
  verifyTOTP,
235
250
  verifyBackupCode,
236
251
  clearError,
@@ -8,21 +8,29 @@
8
8
  import type { MutableRefObject } from 'react';
9
9
 
10
10
  // ─────────────────────────────────────────────────────────────────────────────
11
- // Channel & Step Types
11
+ // Step Types
12
12
  // ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- export type AuthChannel = 'email' | 'phone';
15
14
  export type AuthStep = 'identifier' | 'otp' | '2fa' | '2fa-setup' | 'success';
16
15
 
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // OTP Result Types
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+
20
+ export interface OTPRequestResult {
21
+ success: boolean;
22
+ message: string;
23
+ statusCode?: number;
24
+ retryAfter?: number;
25
+ }
26
+
17
27
  // ─────────────────────────────────────────────────────────────────────────────
18
28
  // Form State
19
29
  // ─────────────────────────────────────────────────────────────────────────────
20
30
 
21
31
  export interface AuthFormState {
22
- /** Email or phone number */
32
+ /** Email address */
23
33
  identifier: string;
24
- /** Current auth channel */
25
- channel: AuthChannel;
26
34
  /** OTP code input */
27
35
  otp: string;
28
36
  /** Loading state */
@@ -41,6 +49,12 @@ export interface AuthFormState {
41
49
  twoFactorCode: string;
42
50
  /** Using backup code instead of TOTP */
43
51
  useBackupCode: boolean;
52
+ /** Seconds remaining until rate limit lifts (0 = not rate limited) */
53
+ rateLimitSeconds: number;
54
+ /** True when rateLimitSeconds > 0 — submit should be disabled */
55
+ isRateLimited: boolean;
56
+ /** Formatted countdown label, e.g. "19:00" or "42s" */
57
+ rateLimitLabel: string;
44
58
  }
45
59
 
46
60
  // ─────────────────────────────────────────────────────────────────────────────
@@ -49,7 +63,6 @@ export interface AuthFormState {
49
63
 
50
64
  export interface AuthFormStateHandlers {
51
65
  setIdentifier: (identifier: string) => void;
52
- setChannel: (channel: AuthChannel) => void;
53
66
  setOtp: (otp: string) => void;
54
67
  setAcceptedTerms: (accepted: boolean) => void;
55
68
  setError: (error: string) => void;
@@ -60,6 +73,8 @@ export interface AuthFormStateHandlers {
60
73
  setShouldPrompt2FA: (prompt: boolean) => void;
61
74
  setTwoFactorCode: (code: string) => void;
62
75
  setUseBackupCode: (useBackup: boolean) => void;
76
+ /** Start a countdown timer that disables submit for `seconds` seconds */
77
+ startRateLimitCountdown: (seconds: number) => void;
63
78
  }
64
79
 
65
80
  export interface AuthFormSubmitHandlers {
@@ -81,10 +96,8 @@ export interface AuthFormSubmitHandlers {
81
96
  // ─────────────────────────────────────────────────────────────────────────────
82
97
 
83
98
  export interface AuthFormValidation {
84
- /** Detect channel from identifier string */
85
- detectChannelFromIdentifier: (identifier: string) => AuthChannel | null;
86
99
  /** Validate identifier format */
87
- validateIdentifier: (identifier: string, channel?: AuthChannel) => boolean;
100
+ validateIdentifier: (identifier: string) => boolean;
88
101
  }
89
102
 
90
103
  // ─────────────────────────────────────────────────────────────────────────────
@@ -105,6 +118,8 @@ export interface AuthForm2FAState {
105
118
  is2FALoading: boolean;
106
119
  /** Warning message from 2FA (e.g., low backup codes) */
107
120
  twoFactorWarning: string | null;
121
+ /** Remaining attempts before 2FA lockout (null = unknown) */
122
+ twoFactorAttemptsRemaining: number | null;
108
123
  }
109
124
 
110
125
  // ─────────────────────────────────────────────────────────────────────────────
@@ -125,7 +140,7 @@ export interface AuthFormReturn extends
125
140
 
126
141
  export interface UseAuthFormOptions {
127
142
  /** Callback when identifier step succeeds */
128
- onIdentifierSuccess?: (identifier: string, channel: AuthChannel) => void;
143
+ onIdentifierSuccess?: (identifier: string) => void;
129
144
  /** Callback when OTP verification succeeds */
130
145
  onOTPSuccess?: () => void;
131
146
  /** Callback on any error */
@@ -147,63 +162,3 @@ export interface UseAuthFormOptions {
147
162
  enable2FASetup?: boolean;
148
163
  }
149
164
 
150
- // ─────────────────────────────────────────────────────────────────────────────
151
- // Layout Context (UI-specific additions)
152
- // ─────────────────────────────────────────────────────────────────────────────
153
-
154
- export interface AuthLayoutConfig {
155
- /** Support page URL */
156
- supportUrl?: string;
157
- /** Terms of service URL */
158
- termsUrl?: string;
159
- /** Privacy policy URL */
160
- privacyUrl?: string;
161
- /** Source URL for tracking */
162
- sourceUrl: string;
163
- /** Enable phone authentication tab */
164
- enablePhoneAuth?: boolean;
165
- /** Enable GitHub OAuth button */
166
- enableGithubAuth?: boolean;
167
- /** Logo URL for success screen (SVG recommended) */
168
- logoUrl?: string;
169
- /** URL to redirect after successful auth (default: /dashboard) */
170
- redirectUrl?: string;
171
- /**
172
- * Enable 2FA setup prompt after successful authentication.
173
- * When true (default), users without 2FA will see a setup prompt after login.
174
- * When false, users go directly to success without 2FA setup prompt.
175
- * Note: This only affects the setup prompt - existing 2FA verification still works.
176
- * @default true
177
- */
178
- enable2FASetup?: boolean;
179
- }
180
-
181
- export interface AuthFormContextType extends AuthFormReturn, AuthLayoutConfig {}
182
-
183
- // ─────────────────────────────────────────────────────────────────────────────
184
- // Layout Props
185
- // ─────────────────────────────────────────────────────────────────────────────
186
-
187
- export interface AuthLayoutProps extends AuthLayoutConfig {
188
- children?: React.ReactNode;
189
- className?: string;
190
- /** URL to redirect after successful auth (default: /dashboard) */
191
- redirectUrl?: string;
192
- /** Callback when identifier step succeeds */
193
- onIdentifierSuccess?: (identifier: string, channel: AuthChannel) => void;
194
- /** Callback when OTP verification succeeds */
195
- onOTPSuccess?: () => void;
196
- /** Callback when OAuth succeeds */
197
- onOAuthSuccess?: (user: any, isNewUser: boolean, provider: string) => void;
198
- /** Callback on any error */
199
- onError?: (message: string) => void;
200
- }
201
-
202
- // ─────────────────────────────────────────────────────────────────────────────
203
- // Help Component Props
204
- // ─────────────────────────────────────────────────────────────────────────────
205
-
206
- export interface AuthHelpProps {
207
- className?: string;
208
- variant?: 'default' | 'compact';
209
- }
@@ -2,9 +2,8 @@
2
2
  * Auth Types - Single source of truth
3
3
  */
4
4
 
5
- // Form types (for auth forms and layouts)
5
+ // Form types (auth logic only)
6
6
  export type {
7
- AuthChannel,
8
7
  AuthStep,
9
8
  AuthFormState,
10
9
  AuthFormStateHandlers,
@@ -13,10 +12,7 @@ export type {
13
12
  AuthFormAutoSubmit,
14
13
  AuthFormReturn,
15
14
  UseAuthFormOptions,
16
- AuthLayoutConfig,
17
- AuthFormContextType,
18
- AuthLayoutProps,
19
- AuthHelpProps,
15
+ OTPRequestResult,
20
16
  } from './form';
21
17
 
22
18
  // Re-export context types