@djangocfg/api 2.1.566 → 2.1.569

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.566",
3
+ "version": "2.1.569",
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",
@@ -85,7 +85,7 @@
85
85
  "zod": "^4.3.6"
86
86
  },
87
87
  "devDependencies": {
88
- "@djangocfg/typescript-config": "^2.1.566",
88
+ "@djangocfg/typescript-config": "^2.1.569",
89
89
  "@testing-library/react": "^16.3.2",
90
90
  "@types/node": "^25.9.5",
91
91
  "@types/react": "19.2.15",
@@ -5,8 +5,8 @@
5
5
  import { z } from "zod";
6
6
 
7
7
  export const OAuthAuthorizeRequestRequestSchema = z.object({
8
- redirect_uri: z.url().optional(),
9
- source_url: z.url().optional(),
8
+ redirect_uri: z.url().or(z.literal("")).optional(),
9
+ source_url: z.url().or(z.literal("")).optional(),
10
10
  });
11
11
 
12
12
  export type OAuthAuthorizeRequestRequest = z.infer<typeof OAuthAuthorizeRequestRequestSchema>;
@@ -5,7 +5,7 @@
5
5
  import { z } from "zod";
6
6
 
7
7
  export const OAuthAuthorizeResponseSchema = z.object({
8
- authorization_url: z.url(),
8
+ authorization_url: z.url().or(z.literal("")),
9
9
  state: z.string(),
10
10
  });
11
11
 
@@ -7,7 +7,7 @@ import { z } from "zod";
7
7
  export const OAuthCallbackRequestRequestSchema = z.object({
8
8
  code: z.string().min(10).max(500),
9
9
  state: z.string().min(20).max(100),
10
- redirect_uri: z.url().optional(),
10
+ redirect_uri: z.url().or(z.literal("")).optional(),
11
11
  remember_me: z.boolean().default(false).optional(),
12
12
  });
13
13
 
@@ -10,8 +10,8 @@ export const OAuthConnectionSchema = z.object({
10
10
  provider: OAuthProviderEnumSchema,
11
11
  provider_display: z.string(),
12
12
  provider_username: z.string(),
13
- provider_email: z.email(),
14
- provider_avatar_url: z.url(),
13
+ provider_email: z.email().or(z.literal("")),
14
+ provider_avatar_url: z.url().or(z.literal("")),
15
15
  connected_at: z.string().datetime({ offset: true }),
16
16
  last_login_at: z.string().datetime({ offset: true }),
17
17
  });
@@ -6,7 +6,7 @@ import { z } from "zod";
6
6
 
7
7
  export const OTPRequestRequestSchema = z.object({
8
8
  identifier: z.string().min(1),
9
- source_url: z.url().optional(),
9
+ source_url: z.url().or(z.literal("")).optional(),
10
10
  marketing_consent: z.boolean().nullable().optional(),
11
11
  consent_disclosure_version: z.string().max(64).optional(),
12
12
  });
@@ -7,7 +7,7 @@ import { z } from "zod";
7
7
  export const OTPVerifyRequestSchema = z.object({
8
8
  identifier: z.string().min(1),
9
9
  otp: z.string().min(4).max(4),
10
- source_url: z.url().optional(),
10
+ source_url: z.url().or(z.literal("")).optional(),
11
11
  remember_me: z.boolean().default(false).optional(),
12
12
  });
13
13
 
@@ -6,7 +6,7 @@ import { z } from "zod";
6
6
 
7
7
  export const UserSchema = z.object({
8
8
  id: z.number().int(),
9
- email: z.email(),
9
+ email: z.email().or(z.literal("")),
10
10
  first_name: z.string().max(50).nullable().optional(),
11
11
  last_name: z.string().max(50).nullable().optional(),
12
12
  full_name: z.string(),
@@ -17,7 +17,7 @@ export const UserSchema = z.object({
17
17
  position: z.string().max(100).nullable().optional(),
18
18
  language: z.string().max(10).nullable().optional(),
19
19
  timezone: z.string().max(64).nullable().optional(),
20
- avatar: z.url().nullable(),
20
+ avatar: z.url().or(z.literal("")).nullable(),
21
21
  is_staff: z.boolean(),
22
22
  is_superuser: z.boolean(),
23
23
  date_joined: z.string().datetime({ offset: true }),
@@ -8,7 +8,7 @@ import { WebmailLinkProviderEnumSchema } from "./WebmailLinkProviderEnum";
8
8
  export const WebmailLinkSchema = z.object({
9
9
  provider: WebmailLinkProviderEnumSchema,
10
10
  provider_name: z.string(),
11
- url: z.url(),
11
+ url: z.url().or(z.literal("")),
12
12
  is_search: z.boolean(),
13
13
  });
14
14
 
@@ -6,7 +6,7 @@ import { z } from "zod";
6
6
 
7
7
  export const TotpVerifyUserSchema = z.object({
8
8
  id: z.number().int(),
9
- email: z.email(),
9
+ email: z.email().or(z.literal("")),
10
10
  first_name: z.string().max(50).nullable().optional(),
11
11
  last_name: z.string().max(50).nullable().optional(),
12
12
  full_name: z.string(),
@@ -17,7 +17,7 @@ export const TotpVerifyUserSchema = z.object({
17
17
  position: z.string().max(100).nullable().optional(),
18
18
  language: z.string().max(10).nullable().optional(),
19
19
  timezone: z.string().max(64).nullable().optional(),
20
- avatar: z.url().nullable(),
20
+ avatar: z.url().or(z.literal("")).nullable(),
21
21
  is_staff: z.boolean(),
22
22
  is_superuser: z.boolean(),
23
23
  date_joined: z.string().datetime({ offset: true }),
@@ -1,15 +1,15 @@
1
1
  /**
2
- * formatAuthError tests (auth/utils/errors.ts).
2
+ * Auth error resolution tests (auth/utils/errors.ts).
3
3
  *
4
4
  * Turns whatever the auth layer throws (string, Error, DRF `{detail}`, axios-ish
5
- * nested `response.data.detail`) into a user-facing string, with a stable
6
- * fallback. The precedence order matters: message > detail > nested detail, and
7
- * it must never throw on null/odd shapes.
5
+ * nested `response.data.detail`, an `APIError`-shaped object) into a user-facing
6
+ * string, with a stable fallback. The precedence order matters: server prose >
7
+ * status-code copy > fallback, and it must never throw on null/odd shapes.
8
8
  */
9
9
 
10
10
  import { describe, expect, it } from 'vitest';
11
11
 
12
- import { formatAuthError } from '../utils/errors';
12
+ import { AUTH_ERROR_KEYS, formatAuthError, resolveAuthError } from '../utils/errors';
13
13
 
14
14
  describe('formatAuthError', () => {
15
15
  it('returns a plain string as-is', () => {
@@ -39,10 +39,74 @@ describe('formatAuthError', () => {
39
39
  });
40
40
 
41
41
  it('falls back for null / undefined / empty object', () => {
42
- const FALLBACK = 'An unexpected error occurred';
42
+ const FALLBACK = 'Something went wrong';
43
43
  expect(formatAuthError(null)).toBe(FALLBACK);
44
44
  expect(formatAuthError(undefined)).toBe(FALLBACK);
45
45
  expect(formatAuthError({})).toBe(FALLBACK);
46
46
  expect(formatAuthError(42)).toBe(FALLBACK);
47
47
  });
48
+
49
+ it("prefers the caller's fallback when nothing else identifies the failure", () => {
50
+ expect(formatAuthError({}, 'Failed to send OTP')).toBe('Failed to send OTP');
51
+ });
52
+ });
53
+
54
+ /*
55
+ * The regression this module exists for: a bare `HTTP 404:` under a sign-in
56
+ * field. `APIError` builds that string itself when the response carries no
57
+ * message, and `statusText` is empty under HTTP/2 — so it is a status line
58
+ * wearing a message's clothes, and every extractor used to hand it straight to
59
+ * the UI.
60
+ */
61
+ describe('resolveAuthError — transport strings never reach the UI', () => {
62
+ it('rejects the bare HTTP fallback and explains the status instead', () => {
63
+ const err = { message: 'HTTP 404: ', statusCode: 404, name: 'APIError' };
64
+ const { message, key } = resolveAuthError(err);
65
+
66
+ expect(message).not.toMatch(/HTTP/);
67
+ expect(key).toBe(AUTH_ERROR_KEYS.serverUnavailable);
68
+ });
69
+
70
+ it('rejects it even when statusText filled it in', () => {
71
+ expect(formatAuthError({ message: 'HTTP 500: Internal Server Error', statusCode: 500 }))
72
+ .not.toMatch(/HTTP/);
73
+ });
74
+
75
+ it("outranks the caller's fallback with status-derived copy", () => {
76
+ // A 404 reported as "Failed to send the code" blames the code, not the
77
+ // endpoint that is missing.
78
+ const { key } = resolveAuthError({ message: 'HTTP 404: ', statusCode: 404 }, 'Failed to send OTP');
79
+ expect(key).toBe(AUTH_ERROR_KEYS.serverUnavailable);
80
+ });
81
+ });
82
+
83
+ describe('resolveAuthError — status codes', () => {
84
+ it.each([
85
+ [401, AUTH_ERROR_KEYS.invalidCredentials],
86
+ [403, AUTH_ERROR_KEYS.invalidCredentials],
87
+ [404, AUTH_ERROR_KEYS.serverUnavailable],
88
+ [429, AUTH_ERROR_KEYS.rateLimited],
89
+ [500, AUTH_ERROR_KEYS.serverUnavailable],
90
+ [503, AUTH_ERROR_KEYS.serverUnavailable],
91
+ ])('maps %i to its key', (statusCode, expected) => {
92
+ expect(resolveAuthError({ statusCode }).key).toBe(expected);
93
+ });
94
+
95
+ it('reads a Response-shaped `status` too', () => {
96
+ expect(resolveAuthError({ status: 429 }).key).toBe(AUTH_ERROR_KEYS.rateLimited);
97
+ });
98
+
99
+ it('server prose outranks the status code', () => {
100
+ const err = { response: { detail: 'This code has expired' }, statusCode: 400 };
101
+ const { message, key } = resolveAuthError(err);
102
+
103
+ expect(message).toBe('This code has expired');
104
+ // No key: the backend already chose the words, in the caller's locale.
105
+ expect(key).toBeUndefined();
106
+ });
107
+
108
+ it('reads the APIError body spellings the auth endpoints actually use', () => {
109
+ expect(formatAuthError({ response: { error: 'Too many attempts' } })).toBe('Too many attempts');
110
+ expect(formatAuthError({ response: { message: 'Account locked' } })).toBe('Account locked');
111
+ });
48
112
  });
@@ -0,0 +1,89 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { logAuthFailure } from '../utils/errors';
4
+
5
+ /**
6
+ * A mistyped OTP must not page anyone.
7
+ *
8
+ * `@djangocfg/devtools` captures `console.error` as an ERROR event, so every
9
+ * error-level auth log became a production alert. A customer fat-fingering
10
+ * their one-time code raised "❌ Error" in Telegram — with consola's `%c` CSS
11
+ * in the message body, because the capture takes the console string verbatim.
12
+ * Real faults are unfindable in that.
13
+ */
14
+
15
+ const sink = () => ({ warn: vi.fn(), error: vi.fn() });
16
+
17
+ describe('logAuthFailure', () => {
18
+ describe('the user got it wrong — warn, no alert', () => {
19
+ it.each([400, 401, 403, 404, 429, 499])('treats %i as a warning', (statusCode) => {
20
+ const log = sink();
21
+ logAuthFailure(log, 'Verify OTP failed:', { statusCode });
22
+ expect(log.warn).toHaveBeenCalledOnce();
23
+ expect(log.error).not.toHaveBeenCalled();
24
+ });
25
+
26
+ it('reads `status` too, for a rejection that skipped the interceptor', () => {
27
+ const log = sink();
28
+ logAuthFailure(log, 'Verify OTP failed:', { status: 401 });
29
+ expect(log.warn).toHaveBeenCalledOnce();
30
+ expect(log.error).not.toHaveBeenCalled();
31
+ });
32
+
33
+ it('passes the message and the error through unchanged', () => {
34
+ const log = sink();
35
+ const error = { statusCode: 401 };
36
+ logAuthFailure(log, 'Verify OTP failed:', error);
37
+ expect(log.warn).toHaveBeenCalledWith('Verify OTP failed:', error);
38
+ });
39
+ });
40
+
41
+ describe('something is actually broken — error, raise the alert', () => {
42
+ it.each([500, 502, 503, 504])('treats %i as an error', (statusCode) => {
43
+ const log = sink();
44
+ logAuthFailure(log, 'Verify OTP failed:', { statusCode });
45
+ expect(log.error).toHaveBeenCalledOnce();
46
+ expect(log.warn).not.toHaveBeenCalled();
47
+ });
48
+
49
+ it('keeps a network failure at error — it carries no status at all', () => {
50
+ const log = sink();
51
+ logAuthFailure(log, 'Verify OTP failed:', new TypeError('Failed to fetch'));
52
+ expect(log.error).toHaveBeenCalledOnce();
53
+ expect(log.warn).not.toHaveBeenCalled();
54
+ });
55
+
56
+ it.each([null, undefined, 'a string', 42, { statusCode: 'nonsense' }])(
57
+ 'keeps an unreadable error (%s) at error rather than silencing it',
58
+ (error) => {
59
+ const log = sink();
60
+ logAuthFailure(log, 'Verify OTP failed:', error);
61
+ expect(log.error).toHaveBeenCalledOnce();
62
+ expect(log.warn).not.toHaveBeenCalled();
63
+ },
64
+ );
65
+ });
66
+
67
+ describe('the boundaries', () => {
68
+ it('399 is not a client error', () => {
69
+ const log = sink();
70
+ logAuthFailure(log, 'x', { statusCode: 399 });
71
+ expect(log.error).toHaveBeenCalledOnce();
72
+ });
73
+
74
+ it('500 is not a client error', () => {
75
+ const log = sink();
76
+ logAuthFailure(log, 'x', { statusCode: 500 });
77
+ expect(log.error).toHaveBeenCalledOnce();
78
+ });
79
+
80
+ it('prefers statusCode when both fields disagree', () => {
81
+ /* `statusCode` is the field APIError declares; `status` is the fallback
82
+ for a foreign shape, so it must not override the real one. */
83
+ const log = sink();
84
+ logAuthFailure(log, 'x', { statusCode: 401, status: 500 });
85
+ expect(log.warn).toHaveBeenCalledOnce();
86
+ expect(log.error).not.toHaveBeenCalled();
87
+ });
88
+ });
89
+ });
@@ -35,6 +35,7 @@ import { useCfgRouter, useLocalStorage, useQueryParams, useSession } from '../ho
35
35
  import { clearProfileCache } from '../hooks/useProfileCache';
36
36
  import { useAuthRedirectManager } from '../hooks/useAuthRedirect';
37
37
  import { Analytics, AnalyticsCategory, AnalyticsEvent } from '../utils/analytics';
38
+ import { formatAuthError, logAuthFailure } from '../utils/errors';
38
39
  import { isDev, isBrowser } from '../utils/env';
39
40
  import { authLogger } from '../utils/logger';
40
41
  import { AccountsProvider, useAccountsContext } from './AccountsContext';
@@ -71,14 +72,6 @@ const EMAIL_STORAGE_KEY = 'auth_email';
71
72
 
72
73
  const AuthContext = createContext<AuthContextType | undefined>(undefined);
73
74
 
74
- /** Extract a user-facing message from an APIError response body. */
75
- const apiErrorMessage = (error: APIError): string => {
76
- const body = error.response as
77
- | { error?: string; detail?: string; message?: string }
78
- | undefined;
79
- return body?.error || body?.detail || body?.message || error.errorMessage;
80
- };
81
-
82
75
  const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config }) => {
83
76
  const accounts = useAccountsContext();
84
77
  const session = useSession();
@@ -266,7 +259,7 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
266
259
  webmail: result.webmail ?? null,
267
260
  };
268
261
  } catch (error) {
269
- authLogger.error('Request OTP error:', error);
262
+ logAuthFailure(authLogger, 'Request OTP failed:', error);
270
263
 
271
264
  if (error instanceof APIError) {
272
265
  const body = error.response as
@@ -276,11 +269,11 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
276
269
  return {
277
270
  success: false,
278
271
  statusCode: error.statusCode,
279
- message: apiErrorMessage(error),
272
+ message: formatAuthError(error),
280
273
  retryAfter: typeof retryAfter === 'number' ? retryAfter : undefined,
281
274
  };
282
275
  }
283
- return { success: false, message: 'Failed to send OTP' };
276
+ return { success: false, message: formatAuthError(error, 'Failed to send OTP') };
284
277
  }
285
278
  },
286
279
  [],
@@ -359,15 +352,15 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
359
352
  should_prompt_2fa: result.should_prompt_2fa,
360
353
  };
361
354
  } catch (error) {
362
- authLogger.error('Verify OTP error:', error);
355
+ logAuthFailure(authLogger, 'Verify OTP failed:', error);
363
356
  Analytics.event(AnalyticsEvent.AUTH_OTP_VERIFY_FAIL, {
364
357
  category: AnalyticsCategory.AUTH,
365
358
  label: 'email',
366
359
  });
367
360
  if (error instanceof APIError) {
368
- return { success: false, message: apiErrorMessage(error) };
361
+ return { success: false, message: formatAuthError(error) };
369
362
  }
370
- return { success: false, message: 'Failed to verify OTP' };
363
+ return { success: false, message: formatAuthError(error, 'Failed to verify OTP') };
371
364
  }
372
365
  },
373
366
  [setStoredEmail, redirectManager, router],
@@ -440,7 +433,7 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
440
433
  should_prompt_2fa: result.should_prompt_2fa,
441
434
  };
442
435
  } catch (error) {
443
- authLogger.error('Password login error:', error);
436
+ logAuthFailure(authLogger, 'Password login failed:', error);
444
437
  Analytics.event(AnalyticsEvent.AUTH_PASSWORD_LOGIN_FAIL, {
445
438
  category: AnalyticsCategory.AUTH,
446
439
  label: 'password',
@@ -454,12 +447,12 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
454
447
  return {
455
448
  success: false,
456
449
  statusCode: error.statusCode,
457
- message: apiErrorMessage(error),
450
+ message: formatAuthError(error),
458
451
  errorCode: body?.error_code,
459
452
  retryAfter: typeof body?.retry_after === 'number' ? body.retry_after : undefined,
460
453
  };
461
454
  }
462
- return { success: false, message: 'Failed to sign in' };
455
+ return { success: false, message: formatAuthError(error, 'Failed to sign in') };
463
456
  }
464
457
  },
465
458
  [setStoredEmail, redirectManager, router],
@@ -9,34 +9,19 @@ import { APIError } from '../../_api/generated/helpers';
9
9
  * and 6-digit validation slightly differently — three copies that drifted. This
10
10
  * module is the single source for both, so every 2FA verb reports errors and
11
11
  * validates codes identically.
12
+ *
13
+ * Error extraction itself now lives one level up, in `utils/errors`: this
14
+ * module's own copy was the second of three spellings of the same chain across
15
+ * the auth layer, and the one that could not tell a status code from prose.
12
16
  */
13
17
 
14
18
  const TOTP_CODE_LENGTH = 6;
15
19
 
16
- /** A 2FA API error body may carry any of these message-ish fields. */
20
+ /** The one field the shared resolver does not read for us. */
17
21
  interface TwoFactorErrorBody {
18
- error?: unknown;
19
- detail?: unknown;
20
- message?: unknown;
21
22
  attempts_remaining?: unknown;
22
23
  }
23
24
 
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
25
  /** Pull `attempts_remaining` off an APIError body, or null if absent. */
41
26
  export function extractAttemptsRemaining(err: unknown): number | null {
42
27
  if (err instanceof APIError) {
@@ -6,6 +6,7 @@ import { auth } from '../../_api/generated/helpers/auth';
6
6
  import { CfgAccountsOauth } from '../../_api/generated/sdk.gen';
7
7
  import type { OAuthTokenResponse } from '../../_api/generated/types.gen';
8
8
  import { Analytics, AnalyticsCategory, AnalyticsEvent } from '../utils/analytics';
9
+ import { formatAuthError } from '../utils/errors';
9
10
  import { authLogger } from '../utils/logger';
10
11
  import { useCfgRouter } from './useCfgRouter';
11
12
 
@@ -114,7 +115,7 @@ export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuth
114
115
  window.location.href = response.authorization_url;
115
116
 
116
117
  } catch (err) {
117
- const errorMessage = err instanceof Error ? err.message : 'Failed to start GitHub authentication';
118
+ const errorMessage = formatAuthError(err, 'Failed to start GitHub authentication');
118
119
  authLogger.error('GitHub OAuth start error:', err);
119
120
  setError(errorMessage);
120
121
  onError?.(errorMessage);
@@ -219,7 +220,7 @@ export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuth
219
220
  return;
220
221
 
221
222
  } catch (err) {
222
- const errorMessage = err instanceof Error ? err.message : 'GitHub authentication failed';
223
+ const errorMessage = formatAuthError(err, 'GitHub authentication failed');
223
224
  authLogger.error('GitHub OAuth callback error:', err);
224
225
  setError(errorMessage);
225
226
  onError?.(errorMessage);
@@ -4,7 +4,8 @@ import { useCallback, useState } from 'react';
4
4
 
5
5
  import { CfgTotpSetup } from '../../_api/generated/sdk.gen';
6
6
  import { authLogger } from '../utils/logger';
7
- import { extractTwoFactorError, isValidTotpCode } from './twoFactor.shared';
7
+ import { formatAuthError, logAuthFailure } from '../utils/errors';
8
+ import { isValidTotpCode } from './twoFactor.shared';
8
9
 
9
10
  export interface TwoFactorSetupData {
10
11
  /** Device ID to use for confirmation */
@@ -105,8 +106,8 @@ export const useTwoFactorSetup = (
105
106
 
106
107
  return data;
107
108
  } catch (err) {
108
- const errorMessage = extractTwoFactorError(err, 'Failed to start 2FA setup');
109
- authLogger.error('2FA setup error:', err);
109
+ const errorMessage = formatAuthError(err, 'Failed to start 2FA setup');
110
+ logAuthFailure(authLogger, '2FA setup failed:', err);
110
111
  setError(errorMessage);
111
112
  setSetupStep('idle');
112
113
  onError?.(errorMessage);
@@ -153,8 +154,8 @@ export const useTwoFactorSetup = (
153
154
  onComplete?.(codes);
154
155
  return codes;
155
156
  } catch (err) {
156
- const errorMessage = extractTwoFactorError(err, 'Invalid code. Please try again.');
157
- authLogger.error('2FA setup confirmation error:', err);
157
+ const errorMessage = formatAuthError(err, 'Invalid code. Please try again.');
158
+ logAuthFailure(authLogger, '2FA setup confirmation failed:', err);
158
159
  setError(errorMessage);
159
160
  setSetupStep('scanning'); // Go back to scanning step
160
161
  onError?.(errorMessage);
@@ -4,7 +4,8 @@ import { useCallback, useState } from 'react';
4
4
 
5
5
  import { CfgTotp } from '../../_api/generated/sdk.gen';
6
6
  import { authLogger } from '../utils/logger';
7
- import { extractTwoFactorError, isValidTotpCode } from './twoFactor.shared';
7
+ import { formatAuthError, logAuthFailure } from '../utils/errors';
8
+ import { isValidTotpCode } from './twoFactor.shared';
8
9
 
9
10
  export interface TwoFactorDevice {
10
11
  id: string;
@@ -70,8 +71,8 @@ export const useTwoFactorStatus = (): UseTwoFactorStatusReturn => {
70
71
 
71
72
  authLogger.info('2FA status:', response.has_2fa_enabled ? 'enabled' : 'disabled');
72
73
  } catch (err) {
73
- const errorMessage = extractTwoFactorError(err, 'Failed to fetch 2FA status');
74
- authLogger.error('Failed to fetch 2FA status:', err);
74
+ const errorMessage = formatAuthError(err, 'Failed to fetch 2FA status');
75
+ logAuthFailure(authLogger, 'Failed to fetch 2FA status:', err);
75
76
  setError(errorMessage);
76
77
  } finally {
77
78
  setIsLoading(false);
@@ -98,8 +99,8 @@ export const useTwoFactorStatus = (): UseTwoFactorStatusReturn => {
98
99
  authLogger.info('2FA disabled successfully');
99
100
  return true;
100
101
  } catch (err: unknown) {
101
- const errorMessage = extractTwoFactorError(err, 'Invalid verification code');
102
- authLogger.error('Failed to disable 2FA:', err);
102
+ const errorMessage = formatAuthError(err, 'Invalid verification code');
103
+ logAuthFailure(authLogger, 'Failed to disable 2FA:', err);
103
104
  setError(errorMessage);
104
105
  return false;
105
106
  } finally {
@@ -7,9 +7,10 @@ import { CfgTotpVerify } from '../../_api/generated/sdk.gen';
7
7
  import type { TotpVerifyUser, VerifyResponse } from '../../_api/generated/types.gen';
8
8
  import { Analytics, AnalyticsCategory, AnalyticsEvent } from '../utils/analytics';
9
9
  import { authLogger } from '../utils/logger';
10
+ import { formatAuthError, logAuthFailure } from '../utils/errors';
10
11
  import { consumeSavedRedirect } from './useAuthRedirect';
11
12
  import { useCfgRouter } from './useCfgRouter';
12
- import { extractAttemptsRemaining, extractTwoFactorError, isValidTotpCode } from './twoFactor.shared';
13
+ import { extractAttemptsRemaining, isValidTotpCode } from './twoFactor.shared';
13
14
 
14
15
  export interface UseTwoFactorVerifyOptions {
15
16
  /** Callback on successful 2FA verification */
@@ -130,8 +131,8 @@ export const useTwoFactorVerify = (
130
131
  handleSuccess(response);
131
132
  return true;
132
133
  } catch (err) {
133
- authLogger.error('2FA TOTP verification error:', err);
134
- const errorMessage = extractTwoFactorError(err, 'Invalid verification code');
134
+ logAuthFailure(authLogger, '2FA TOTP verification failed:', err);
135
+ const errorMessage = formatAuthError(err, 'Invalid verification code');
135
136
  const attempts = extractAttemptsRemaining(err);
136
137
  if (attempts !== null) setAttemptsRemaining(attempts);
137
138
  setError(errorMessage);
@@ -183,8 +184,8 @@ export const useTwoFactorVerify = (
183
184
  handleSuccess(response);
184
185
  return true;
185
186
  } catch (err) {
186
- authLogger.error('2FA backup code verification error:', err);
187
- const errorMessage = extractTwoFactorError(err, 'Invalid backup code');
187
+ logAuthFailure(authLogger, '2FA backup code verification failed:', err);
188
+ const errorMessage = formatAuthError(err, 'Invalid backup code');
188
189
  const attempts = extractAttemptsRemaining(err);
189
190
  if (attempts !== null) setAttemptsRemaining(attempts);
190
191
  setError(errorMessage);