@djangocfg/api 2.1.459 → 2.1.462

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/api",
3
- "version": "2.1.459",
3
+ "version": "2.1.462",
4
4
  "description": "Auto-generated TypeScript API client with React hooks, SWR integration, and Zod validation for Django REST Framework backends",
5
5
  "keywords": [
6
6
  "django",
@@ -84,7 +84,7 @@
84
84
  "zod": "^4.3.6"
85
85
  },
86
86
  "devDependencies": {
87
- "@djangocfg/typescript-config": "^2.1.459",
87
+ "@djangocfg/typescript-config": "^2.1.462",
88
88
  "@testing-library/react": "^16.3.2",
89
89
  "@types/node": "^25.2.3",
90
90
  "@types/react": "^19.2.15",
@@ -35,8 +35,8 @@ vi.mock('../context', () => ({
35
35
  }),
36
36
  }));
37
37
 
38
- vi.mock('../hooks/useTwoFactor', () => ({
39
- useTwoFactor: () => ({
38
+ vi.mock('../hooks/useTwoFactorVerify', () => ({
39
+ useTwoFactorVerify: () => ({
40
40
  isLoading: false,
41
41
  warning: null,
42
42
  verifyTOTP: vi.fn(),
@@ -20,19 +20,23 @@ export { useAutoAuth, type UseAutoAuthOptions } from './useAutoAuth';
20
20
  // OAuth
21
21
  export { useGithubAuth, type UseGithubAuthOptions, type UseGithubAuthReturn } from './useGithubAuth';
22
22
 
23
- // 2FA hooks
24
- export { useTwoFactor, type UseTwoFactorOptions, type UseTwoFactorReturn } from './useTwoFactor';
23
+ // 2FA — one consolidated hook (verify / setup / status namespaces),
24
+ // plus the underlying sub-hooks for single-concern consumers.
25
25
  export {
26
+ useTwoFactor,
27
+ useTwoFactorVerify,
26
28
  useTwoFactorSetup,
29
+ useTwoFactorStatus,
30
+ type UseTwoFactorOptions,
31
+ type UseTwoFactorReturn,
32
+ type UseTwoFactorVerifyOptions,
33
+ type UseTwoFactorVerifyReturn,
27
34
  type UseTwoFactorSetupOptions,
28
35
  type UseTwoFactorSetupReturn,
29
- type TwoFactorSetupData,
30
- } from './useTwoFactorSetup';
31
- export {
32
- useTwoFactorStatus,
33
36
  type UseTwoFactorStatusReturn,
37
+ type TwoFactorSetupData,
34
38
  type TwoFactorDevice,
35
- } from './useTwoFactorStatus';
39
+ } from './useTwoFactor';
36
40
 
37
41
  // Auth guards and redirects
38
42
  export {
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import { APIError } from '../../_api/generated/helpers';
4
+
5
+ /**
6
+ * Shared internals for the 2FA hook family (verify / setup / status).
7
+ *
8
+ * Before consolidation each of the three hooks re-implemented error extraction
9
+ * and 6-digit validation slightly differently — three copies that drifted. This
10
+ * module is the single source for both, so every 2FA verb reports errors and
11
+ * validates codes identically.
12
+ */
13
+
14
+ const TOTP_CODE_LENGTH = 6;
15
+
16
+ /** A 2FA API error body may carry any of these message-ish fields. */
17
+ interface TwoFactorErrorBody {
18
+ error?: unknown;
19
+ detail?: unknown;
20
+ message?: unknown;
21
+ attempts_remaining?: unknown;
22
+ }
23
+
24
+ /**
25
+ * Extract a human-readable message from a thrown error, unwrapping the
26
+ * APIError response body (error → detail → message) before falling back.
27
+ */
28
+ export function extractTwoFactorError(err: unknown, fallback: string): string {
29
+ if (err instanceof APIError) {
30
+ const body = (err.response ?? null) as TwoFactorErrorBody | null;
31
+ if (typeof body?.error === 'string') return body.error;
32
+ if (typeof body?.detail === 'string') return body.detail;
33
+ if (typeof body?.message === 'string') return body.message;
34
+ return err.errorMessage ?? err.message;
35
+ }
36
+ if (err instanceof Error) return err.message;
37
+ return fallback;
38
+ }
39
+
40
+ /** Pull `attempts_remaining` off an APIError body, or null if absent. */
41
+ export function extractAttemptsRemaining(err: unknown): number | null {
42
+ if (err instanceof APIError) {
43
+ const body = (err.response ?? null) as TwoFactorErrorBody | null;
44
+ if (typeof body?.attempts_remaining === 'number') return body.attempts_remaining;
45
+ }
46
+ return null;
47
+ }
48
+
49
+ /** True when `code` is a well-formed 6-digit TOTP code. */
50
+ export function isValidTotpCode(code: string): boolean {
51
+ return !!code && code.length === TOTP_CODE_LENGTH;
52
+ }
53
+
54
+ export { TOTP_CODE_LENGTH };
@@ -8,7 +8,7 @@ import { authLogger } from '../utils/logger';
8
8
  import { useAuthFormState } from './useAuthFormState';
9
9
  import { useAuthValidation } from './useAuthValidation';
10
10
  import { useAutoAuth } from './useAutoAuth';
11
- import { useTwoFactor } from './useTwoFactor';
11
+ import { useTwoFactorVerify } from './useTwoFactorVerify';
12
12
 
13
13
  import type { AuthFormReturn, UseAuthFormOptions } from '../types';
14
14
  /**
@@ -70,7 +70,7 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
70
70
  } = formState;
71
71
 
72
72
  // 2FA verification hook - skip redirect so we can show success screen
73
- const twoFactor = useTwoFactor({
73
+ const twoFactor = useTwoFactorVerify({
74
74
  onSuccess: () => {
75
75
  authLogger.info('2FA verification successful, showing success screen');
76
76
  setStep('success');
@@ -1,262 +1,76 @@
1
1
  'use client';
2
2
 
3
- import { useCallback, useState } from 'react';
4
-
5
- import { apiTotp } from '../../clients';
6
- import { auth } from '../../_api/generated/helpers/auth';
7
- import { APIError } from '../../_api/generated/helpers';
8
- import { CfgTotp, CfgTotpVerify } from '../../_api/generated/sdk.gen';
9
- import { Analytics, AnalyticsCategory, AnalyticsEvent } from '../utils/analytics';
10
- import { authLogger } from '../utils/logger';
11
- import { consumeSavedRedirect } from './useAuthRedirect';
12
- import { useCfgRouter } from './useCfgRouter';
3
+ import {
4
+ useTwoFactorVerify,
5
+ type UseTwoFactorVerifyOptions,
6
+ type UseTwoFactorVerifyReturn,
7
+ } from './useTwoFactorVerify';
8
+ import {
9
+ useTwoFactorSetup,
10
+ type TwoFactorSetupData,
11
+ type UseTwoFactorSetupOptions,
12
+ type UseTwoFactorSetupReturn,
13
+ } from './useTwoFactorSetup';
14
+ import {
15
+ useTwoFactorStatus,
16
+ type TwoFactorDevice,
17
+ type UseTwoFactorStatusReturn,
18
+ } from './useTwoFactorStatus';
13
19
 
14
20
  export interface UseTwoFactorOptions {
15
- /** Callback on successful 2FA verification */
16
- onSuccess?: (user: any) => void;
17
- /** Callback on error */
18
- onError?: (error: string) => void;
19
- /** URL to redirect after successful verification */
20
- redirectUrl?: string;
21
- /** Skip automatic redirect after success (caller handles navigation) */
22
- skipRedirect?: boolean;
21
+ /** Options for the login-time verification namespace */
22
+ verify?: UseTwoFactorVerifyOptions;
23
+ /** Options for the setup (enable) namespace */
24
+ setup?: UseTwoFactorSetupOptions;
23
25
  }
24
26
 
25
27
  export interface UseTwoFactorReturn {
26
- /** Loading state */
27
- isLoading: boolean;
28
- /** Error message */
29
- error: string | null;
30
- /** Warning message (e.g., low backup codes) */
31
- warning: string | null;
32
- /** Remaining backup codes (if backup code was used) */
33
- remainingBackupCodes: number | null;
34
- /** Remaining verification attempts before lockout */
35
- attemptsRemaining: number | null;
36
- /** Verify TOTP code */
37
- verifyTOTP: (sessionId: string, code: string) => Promise<boolean>;
38
- /** Verify backup code */
39
- verifyBackupCode: (sessionId: string, backupCode: string) => Promise<boolean>;
40
- /** Clear error */
41
- clearError: () => void;
28
+ /** Verify a 2FA challenge during login (TOTP / backup code). */
29
+ verify: UseTwoFactorVerifyReturn;
30
+ /** Enable 2FA — QR provisioning → confirm → backup codes. */
31
+ setup: UseTwoFactorSetupReturn;
32
+ /** Read device list / enabled flag and disable 2FA. */
33
+ status: UseTwoFactorStatusReturn;
42
34
  }
43
35
 
44
36
  /**
45
- * Hook for 2FA verification during login.
37
+ * Single entry point for all two-factor authentication flows.
46
38
  *
47
- * Usage:
48
- * 1. After OTP/OAuth verification returns requires_2fa=true and session_id
49
- * 2. Show 2FA form and collect TOTP code from user
50
- * 3. Call verifyTOTP(sessionId, code) to complete authentication
39
+ * The three concerns — **verify** (login), **setup** (enable) and **status**
40
+ * (manage/disable) are distinct verbs over the same `CfgTotp*` API, so each
41
+ * is exposed as its own namespace with an independent loading/error surface:
51
42
  *
52
43
  * @example
53
44
  * ```tsx
54
- * const { isLoading, error, verifyTOTP, verifyBackupCode } = useTwoFactor({
55
- * onSuccess: (user) => console.log('Logged in:', user),
56
- * onError: (error) => console.error(error),
57
- * redirectUrl: '/dashboard',
58
- * });
45
+ * const two = useTwoFactor({ verify: { redirectUrl: '/dashboard' } });
59
46
  *
60
- * const handleSubmit = async (code: string) => {
61
- * if (useBackupCode) {
62
- * await verifyBackupCode(sessionId, code);
63
- * } else {
64
- * await verifyTOTP(sessionId, code);
65
- * }
66
- * };
47
+ * await two.verify.verifyTOTP(sessionId, code); // login
48
+ * await two.setup.startSetup('My iPhone'); // enable
49
+ * await two.status.fetchStatus(); // manage
67
50
  * ```
51
+ *
52
+ * Consumers that only need one concern can compose the underlying hooks
53
+ * directly (`useTwoFactorVerify` / `useTwoFactorSetup` / `useTwoFactorStatus`),
54
+ * all re-exported alongside this one, to avoid mounting the others' state.
68
55
  */
69
56
  export const useTwoFactor = (options: UseTwoFactorOptions = {}): UseTwoFactorReturn => {
70
- const { onSuccess, onError, redirectUrl, skipRedirect = false } = options;
71
- const router = useCfgRouter();
72
-
73
- const [isLoading, setIsLoading] = useState(false);
74
- const [error, setError] = useState<string | null>(null);
75
- const [warning, setWarning] = useState<string | null>(null);
76
- const [remainingBackupCodes, setRemainingBackupCodes] = useState<number | null>(null);
77
- const [attemptsRemaining, setAttemptsRemaining] = useState<number | null>(null);
78
-
79
- const clearError = useCallback(() => {
80
- setError(null);
81
- }, []);
82
-
83
- /**
84
- * Handle successful 2FA verification
85
- */
86
- const handleSuccess = useCallback((response: {
87
- access_token: string;
88
- refresh_token: string;
89
- user: any;
90
- warning?: string;
91
- remaining_backup_codes?: number;
92
- }) => {
93
- // Save tokens — the ONE write path (atomic pair + session notification).
94
- auth.setSession({ access: response.access_token, refresh: response.refresh_token });
95
-
96
- // Set warning if any
97
- if (response.warning) {
98
- setWarning(response.warning);
99
- }
100
-
101
- // Set remaining backup codes if provided
102
- if (response.remaining_backup_codes !== undefined) {
103
- setRemainingBackupCodes(response.remaining_backup_codes);
104
- }
105
-
106
- // Track successful 2FA
107
- Analytics.event(AnalyticsEvent.AUTH_LOGIN_SUCCESS, {
108
- category: AnalyticsCategory.AUTH,
109
- label: '2fa',
110
- });
111
-
112
- // Set user ID for tracking
113
- if (response.user?.id) {
114
- Analytics.setUser(String(response.user.id));
115
- }
116
-
117
- // Call success callback
118
- onSuccess?.(response.user);
119
-
120
- // Redirect (unless skipRedirect is true).
121
- // Priority: saved back-url (guard intent) > explicit option > default.
122
- if (!skipRedirect) {
123
- const finalRedirectUrl = consumeSavedRedirect() || redirectUrl || '/';
124
- authLogger.info('2FA successful, redirecting to:', finalRedirectUrl);
125
- router.hardPush(finalRedirectUrl);
126
- }
127
- }, [onSuccess, redirectUrl, router, skipRedirect]);
128
-
129
- /**
130
- * Verify TOTP code from authenticator app
131
- */
132
- const verifyTOTP = useCallback(async (sessionId: string, code: string): Promise<boolean> => {
133
- if (!sessionId) {
134
- const msg = 'Missing 2FA session ID';
135
- setError(msg);
136
- onError?.(msg);
137
- return false;
138
- }
139
-
140
- if (!code || code.length !== 6) {
141
- const msg = 'Please enter a 6-digit code';
142
- setError(msg);
143
- onError?.(msg);
144
- return false;
145
- }
146
-
147
- setIsLoading(true);
148
- setError(null);
149
-
150
- try {
151
- authLogger.info('Verifying TOTP code...');
152
-
153
- const result = await CfgTotpVerify.cfgTotpVerifyCreate({
154
- body: { session_id: sessionId, code },
155
- throwOnError: true,
156
- });
157
- const response = result.data;
57
+ const verify = useTwoFactorVerify(options.verify);
58
+ const setup = useTwoFactorSetup(options.setup);
59
+ const status = useTwoFactorStatus();
158
60
 
159
- if (!response.access_token || !response.refresh_token) {
160
- throw new Error('Invalid response from 2FA verification');
161
- }
162
-
163
- handleSuccess(response);
164
- return true;
165
-
166
- } catch (err) {
167
- authLogger.error('2FA TOTP verification error:', err);
168
- const errorMessage = err instanceof APIError
169
- ? (err.response?.error || err.response?.detail || err.response?.message || err.errorMessage)
170
- : (err instanceof Error ? err.message : 'Invalid verification code');
171
- if (err instanceof APIError && typeof err.response?.attempts_remaining === 'number') {
172
- setAttemptsRemaining(err.response.attempts_remaining);
173
- }
174
- setError(errorMessage);
175
- onError?.(errorMessage);
176
-
177
- // Track failed 2FA
178
- Analytics.event(AnalyticsEvent.AUTH_OTP_VERIFY_FAIL, {
179
- category: AnalyticsCategory.AUTH,
180
- label: '2fa-totp',
181
- });
182
-
183
- return false;
184
- } finally {
185
- setIsLoading(false);
186
- }
187
- }, [handleSuccess, onError]);
188
-
189
- /**
190
- * Verify backup recovery code
191
- */
192
- const verifyBackupCode = useCallback(async (sessionId: string, backupCode: string): Promise<boolean> => {
193
- if (!sessionId) {
194
- const msg = 'Missing 2FA session ID';
195
- setError(msg);
196
- onError?.(msg);
197
- return false;
198
- }
199
-
200
- if (!backupCode || backupCode.length < 8) {
201
- const msg = 'Please enter your backup code';
202
- setError(msg);
203
- onError?.(msg);
204
- return false;
205
- }
206
-
207
- setIsLoading(true);
208
- setError(null);
209
-
210
- try {
211
- authLogger.info('Verifying backup code...');
212
-
213
- const result = await CfgTotpVerify.cfgTotpVerifyBackupCreate({
214
- body: {
215
- session_id: sessionId,
216
- backup_code: backupCode.replace(/\s+/g, ''),
217
- },
218
- throwOnError: true,
219
- });
220
- const response = result.data;
221
-
222
- if (!response.access_token || !response.refresh_token) {
223
- throw new Error('Invalid response from backup code verification');
224
- }
225
-
226
- handleSuccess(response);
227
- return true;
228
-
229
- } catch (err) {
230
- authLogger.error('2FA backup code verification error:', err);
231
- const errorMessage = err instanceof APIError
232
- ? (err.response?.error || err.response?.detail || err.response?.message || err.errorMessage)
233
- : (err instanceof Error ? err.message : 'Invalid backup code');
234
- if (err instanceof APIError && typeof err.response?.attempts_remaining === 'number') {
235
- setAttemptsRemaining(err.response.attempts_remaining);
236
- }
237
- setError(errorMessage);
238
- onError?.(errorMessage);
239
-
240
- // Track failed 2FA
241
- Analytics.event(AnalyticsEvent.AUTH_OTP_VERIFY_FAIL, {
242
- category: AnalyticsCategory.AUTH,
243
- label: '2fa-backup',
244
- });
245
-
246
- return false;
247
- } finally {
248
- setIsLoading(false);
249
- }
250
- }, [handleSuccess, onError]);
61
+ return { verify, setup, status };
62
+ };
251
63
 
252
- return {
253
- isLoading,
254
- error,
255
- warning,
256
- remainingBackupCodes,
257
- attemptsRemaining,
258
- verifyTOTP,
259
- verifyBackupCode,
260
- clearError,
261
- };
64
+ // Namespaced sub-hooks — use directly when only one concern is needed.
65
+ export {
66
+ useTwoFactorVerify,
67
+ useTwoFactorSetup,
68
+ useTwoFactorStatus,
69
+ type UseTwoFactorVerifyOptions,
70
+ type UseTwoFactorVerifyReturn,
71
+ type UseTwoFactorSetupOptions,
72
+ type UseTwoFactorSetupReturn,
73
+ type UseTwoFactorStatusReturn,
74
+ type TwoFactorSetupData,
75
+ type TwoFactorDevice,
262
76
  };
@@ -2,9 +2,9 @@
2
2
 
3
3
  import { useCallback, useState } from 'react';
4
4
 
5
- import { apiTotp } from '../../clients';
6
5
  import { CfgTotpSetup } from '../../_api/generated/sdk.gen';
7
6
  import { authLogger } from '../utils/logger';
7
+ import { extractTwoFactorError, isValidTotpCode } from './twoFactor.shared';
8
8
 
9
9
  export interface TwoFactorSetupData {
10
10
  /** Device ID to use for confirmation */
@@ -50,40 +50,13 @@ export interface UseTwoFactorSetupReturn {
50
50
  }
51
51
 
52
52
  /**
53
- * Hook for 2FA setup (enabling TOTP authentication).
53
+ * 2FA setup enabling TOTP (internal, composed by {@link useTwoFactor}).
54
54
  *
55
- * Flow:
56
- * 1. Call startSetup() to get QR code and provisioning URI
57
- * 2. User scans QR code with authenticator app
58
- * 3. Call confirmSetup(code) with the 6-digit code from app
59
- * 4. Show backup codes to user (they must save these!)
60
- *
61
- * @example
62
- * ```tsx
63
- * const {
64
- * isLoading,
65
- * error,
66
- * setupData,
67
- * backupCodes,
68
- * setupStep,
69
- * startSetup,
70
- * confirmSetup,
71
- * } = useTwoFactorSetup({
72
- * onComplete: (codes) => console.log('Backup codes:', codes),
73
- * onError: (error) => console.error(error),
74
- * });
75
- *
76
- * // Start setup
77
- * const data = await startSetup('My iPhone');
78
- *
79
- * // Show QR code
80
- * <QRCodeSVG value={data.provisioningUri} />
81
- *
82
- * // Confirm with code from app
83
- * const codes = await confirmSetup('123456');
84
- * ```
55
+ * Flow: startSetup() → user scans QR → confirmSetup(code) → show backup codes.
85
56
  */
86
- export const useTwoFactorSetup = (options: UseTwoFactorSetupOptions = {}): UseTwoFactorSetupReturn => {
57
+ export const useTwoFactorSetup = (
58
+ options: UseTwoFactorSetupOptions = {},
59
+ ): UseTwoFactorSetupReturn => {
87
60
  const { onComplete, onError } = options;
88
61
 
89
62
  const [isLoading, setIsLoading] = useState(false);
@@ -105,9 +78,6 @@ export const useTwoFactorSetup = (options: UseTwoFactorSetupOptions = {}): UseTw
105
78
  setError(null);
106
79
  }, []);
107
80
 
108
- /**
109
- * Start 2FA setup - generates QR code and secret
110
- */
111
81
  const startSetup = useCallback(async (deviceName?: string): Promise<TwoFactorSetupData | null> => {
112
82
  setIsLoading(true);
113
83
  setError(null);
@@ -134,9 +104,8 @@ export const useTwoFactorSetup = (options: UseTwoFactorSetupOptions = {}): UseTw
134
104
  authLogger.info('2FA setup initiated, expires in:', data.expiresIn, 'seconds');
135
105
 
136
106
  return data;
137
-
138
107
  } catch (err) {
139
- const errorMessage = err instanceof Error ? err.message : 'Failed to start 2FA setup';
108
+ const errorMessage = extractTwoFactorError(err, 'Failed to start 2FA setup');
140
109
  authLogger.error('2FA setup error:', err);
141
110
  setError(errorMessage);
142
111
  setSetupStep('idle');
@@ -147,9 +116,6 @@ export const useTwoFactorSetup = (options: UseTwoFactorSetupOptions = {}): UseTw
147
116
  }
148
117
  }, [onError]);
149
118
 
150
- /**
151
- * Confirm 2FA setup with TOTP code from authenticator app
152
- */
153
119
  const confirmSetup = useCallback(async (code: string): Promise<string[] | null> => {
154
120
  if (!setupData) {
155
121
  const msg = 'Setup not started. Call startSetup() first.';
@@ -157,8 +123,7 @@ export const useTwoFactorSetup = (options: UseTwoFactorSetupOptions = {}): UseTw
157
123
  onError?.(msg);
158
124
  return null;
159
125
  }
160
-
161
- if (!code || code.length !== 6) {
126
+ if (!isValidTotpCode(code)) {
162
127
  const msg = 'Please enter a 6-digit code';
163
128
  setError(msg);
164
129
  onError?.(msg);
@@ -185,13 +150,10 @@ export const useTwoFactorSetup = (options: UseTwoFactorSetupOptions = {}): UseTw
185
150
 
186
151
  authLogger.info('2FA setup confirmed, backup codes generated:', codes.length);
187
152
 
188
- // Call completion callback
189
153
  onComplete?.(codes);
190
-
191
154
  return codes;
192
-
193
155
  } catch (err) {
194
- const errorMessage = err instanceof Error ? err.message : 'Invalid code. Please try again.';
156
+ const errorMessage = extractTwoFactorError(err, 'Invalid code. Please try again.');
195
157
  authLogger.error('2FA setup confirmation error:', err);
196
158
  setError(errorMessage);
197
159
  setSetupStep('scanning'); // Go back to scanning step
@@ -2,10 +2,9 @@
2
2
 
3
3
  import { useCallback, useState } from 'react';
4
4
 
5
- import { apiTotp } from '../../clients';
6
- import { APIError } from '../../_api/generated/helpers';
7
5
  import { CfgTotp } from '../../_api/generated/sdk.gen';
8
6
  import { authLogger } from '../utils/logger';
7
+ import { extractTwoFactorError, isValidTotpCode } from './twoFactor.shared';
9
8
 
10
9
  export interface TwoFactorDevice {
11
10
  id: string;
@@ -33,35 +32,10 @@ export interface UseTwoFactorStatusReturn {
33
32
  }
34
33
 
35
34
  /**
36
- * Hook for checking and managing 2FA status.
35
+ * Read and manage 2FA status (internal, composed by {@link useTwoFactor}).
37
36
  *
38
- * @example
39
- * ```tsx
40
- * const { has2FAEnabled, devices, fetchStatus, disable2FA, isLoading } = useTwoFactorStatus();
41
- *
42
- * useEffect(() => {
43
- * fetchStatus();
44
- * }, [fetchStatus]);
45
- *
46
- * if (has2FAEnabled) {
47
- * // Show disable button
48
- * } else {
49
- * // Show enable button
50
- * }
51
- * ```
37
+ * Fetches the device list / enabled flag and disables 2FA.
52
38
  */
53
- /** Extract a human-readable message from a thrown error. */
54
- function extractErrorMessage(err: unknown, fallback: string): string {
55
- if (err instanceof APIError) {
56
- const body = err.response as Record<string, unknown> | null;
57
- if (typeof body?.error === 'string') return body.error;
58
- if (typeof body?.detail === 'string') return body.detail;
59
- return err.message;
60
- }
61
- if (err instanceof Error) return err.message;
62
- return fallback;
63
- }
64
-
65
39
  export const useTwoFactorStatus = (): UseTwoFactorStatusReturn => {
66
40
  const [isLoading, setIsLoading] = useState(false);
67
41
  const [error, setError] = useState<string | null>(null);
@@ -72,9 +46,6 @@ export const useTwoFactorStatus = (): UseTwoFactorStatusReturn => {
72
46
  setError(null);
73
47
  }, []);
74
48
 
75
- /**
76
- * Fetch current 2FA status and devices
77
- */
78
49
  const fetchStatus = useCallback(async (): Promise<void> => {
79
50
  setIsLoading(true);
80
51
  setError(null);
@@ -98,9 +69,8 @@ export const useTwoFactorStatus = (): UseTwoFactorStatusReturn => {
98
69
  setHas2FAEnabled(response.has_2fa_enabled);
99
70
 
100
71
  authLogger.info('2FA status:', response.has_2fa_enabled ? 'enabled' : 'disabled');
101
-
102
72
  } catch (err) {
103
- const errorMessage = extractErrorMessage(err, 'Failed to fetch 2FA status');
73
+ const errorMessage = extractTwoFactorError(err, 'Failed to fetch 2FA status');
104
74
  authLogger.error('Failed to fetch 2FA status:', err);
105
75
  setError(errorMessage);
106
76
  } finally {
@@ -108,11 +78,8 @@ export const useTwoFactorStatus = (): UseTwoFactorStatusReturn => {
108
78
  }
109
79
  }, []);
110
80
 
111
- /**
112
- * Disable 2FA for the account
113
- */
114
81
  const disable2FA = useCallback(async (code: string): Promise<boolean> => {
115
- if (!code || code.length !== 6) {
82
+ if (!isValidTotpCode(code)) {
116
83
  setError('Please enter a 6-digit code');
117
84
  return false;
118
85
  }
@@ -130,9 +97,8 @@ export const useTwoFactorStatus = (): UseTwoFactorStatusReturn => {
130
97
 
131
98
  authLogger.info('2FA disabled successfully');
132
99
  return true;
133
-
134
100
  } catch (err: unknown) {
135
- const errorMessage = extractErrorMessage(err, 'Invalid verification code');
101
+ const errorMessage = extractTwoFactorError(err, 'Invalid verification code');
136
102
  authLogger.error('Failed to disable 2FA:', err);
137
103
  setError(errorMessage);
138
104
  return false;