@djangocfg/api 2.1.56 → 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.
Files changed (75) hide show
  1. package/README.md +125 -9
  2. package/dist/auth.cjs +1865 -402
  3. package/dist/auth.cjs.map +1 -1
  4. package/dist/auth.d.cts +352 -76
  5. package/dist/auth.d.ts +352 -76
  6. package/dist/auth.mjs +1867 -404
  7. package/dist/auth.mjs.map +1 -1
  8. package/dist/clients.cjs +1637 -137
  9. package/dist/clients.cjs.map +1 -1
  10. package/dist/clients.d.cts +1394 -282
  11. package/dist/clients.d.ts +1394 -282
  12. package/dist/clients.mjs +1637 -137
  13. package/dist/clients.mjs.map +1 -1
  14. package/dist/hooks.cjs +24 -11
  15. package/dist/hooks.cjs.map +1 -1
  16. package/dist/hooks.d.cts +88 -21
  17. package/dist/hooks.d.ts +88 -21
  18. package/dist/hooks.mjs +24 -11
  19. package/dist/hooks.mjs.map +1 -1
  20. package/dist/index.cjs +38 -17
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +94 -21
  23. package/dist/index.d.ts +94 -21
  24. package/dist/index.mjs +38 -17
  25. package/dist/index.mjs.map +1 -1
  26. package/package.json +3 -3
  27. package/src/auth/context/AccountsContext.tsx +8 -1
  28. package/src/auth/context/AuthContext.tsx +31 -8
  29. package/src/auth/context/types.ts +8 -1
  30. package/src/auth/hooks/index.ts +29 -5
  31. package/src/auth/hooks/useAuthForm.ts +292 -226
  32. package/src/auth/hooks/useAuthFormState.ts +60 -0
  33. package/src/auth/hooks/useAuthValidation.ts +77 -0
  34. package/src/auth/hooks/useGithubAuth.ts +26 -5
  35. package/src/auth/hooks/useTwoFactor.ts +239 -0
  36. package/src/auth/hooks/useTwoFactorSetup.ts +213 -0
  37. package/src/auth/index.ts +3 -0
  38. package/src/auth/types/form.ts +194 -0
  39. package/src/auth/types/index.ts +28 -0
  40. package/src/clients.ts +10 -0
  41. package/src/generated/cfg_accounts/_utils/schemas/OAuthTokenResponse.schema.ts +26 -3
  42. package/src/generated/cfg_accounts/_utils/schemas/OTPVerifyResponse.schema.ts +26 -3
  43. package/src/generated/cfg_accounts/accounts/client.ts +4 -1
  44. package/src/generated/cfg_accounts/accounts/models.ts +15 -6
  45. package/src/generated/cfg_accounts/accounts__oauth/models.ts +16 -7
  46. package/src/generated/cfg_accounts/client.ts +5 -2
  47. package/src/generated/cfg_accounts/http.ts +8 -2
  48. package/src/generated/cfg_accounts/schema.json +47 -19
  49. package/src/generated/cfg_centrifugo/client.ts +5 -2
  50. package/src/generated/cfg_centrifugo/http.ts +8 -2
  51. package/src/generated/cfg_totp/CLAUDE.md +12 -12
  52. package/src/generated/cfg_totp/_utils/fetchers/index.ts +3 -3
  53. package/src/generated/cfg_totp/_utils/fetchers/{totp__2fa_management.ts → totp__totp_management.ts} +3 -3
  54. package/src/generated/cfg_totp/_utils/fetchers/{totp__2fa_setup.ts → totp__totp_setup.ts} +3 -3
  55. package/src/generated/cfg_totp/_utils/fetchers/{totp__2fa_verification.ts → totp__totp_verification.ts} +3 -3
  56. package/src/generated/cfg_totp/_utils/hooks/index.ts +3 -3
  57. package/src/generated/cfg_totp/_utils/hooks/{totp__2fa_management.ts → totp__totp_management.ts} +2 -2
  58. package/src/generated/cfg_totp/_utils/hooks/{totp__2fa_setup.ts → totp__totp_setup.ts} +2 -2
  59. package/src/generated/cfg_totp/_utils/hooks/{totp__2fa_verification.ts → totp__totp_verification.ts} +2 -2
  60. package/src/generated/cfg_totp/_utils/schemas/DeviceList.schema.ts +1 -1
  61. package/src/generated/cfg_totp/client.ts +14 -11
  62. package/src/generated/cfg_totp/http.ts +8 -2
  63. package/src/generated/cfg_totp/index.ts +16 -16
  64. package/src/generated/cfg_totp/schema.json +8 -7
  65. package/src/generated/cfg_totp/{totp__2fa_management → totp__totp_management}/client.ts +2 -2
  66. package/src/generated/cfg_totp/{totp__2fa_management → totp__totp_management}/models.ts +1 -1
  67. package/src/generated/cfg_totp/{totp__2fa_setup → totp__totp_setup}/client.ts +4 -4
  68. package/src/generated/cfg_totp/{totp__2fa_verification → totp__totp_verification}/client.ts +2 -2
  69. package/src/generated/cfg_webpush/client.ts +5 -2
  70. package/src/generated/cfg_webpush/http.ts +8 -2
  71. /package/src/generated/cfg_totp/{totp__2fa_management → totp__totp_management}/index.ts +0 -0
  72. /package/src/generated/cfg_totp/{totp__2fa_setup → totp__totp_setup}/index.ts +0 -0
  73. /package/src/generated/cfg_totp/{totp__2fa_setup → totp__totp_setup}/models.ts +0 -0
  74. /package/src/generated/cfg_totp/{totp__2fa_verification → totp__totp_verification}/index.ts +0 -0
  75. /package/src/generated/cfg_totp/{totp__2fa_verification → totp__totp_verification}/models.ts +0 -0
@@ -0,0 +1,60 @@
1
+ "use client"
2
+
3
+ import { useCallback, useState } from 'react';
4
+
5
+ import type { AuthChannel, AuthFormState, AuthFormStateHandlers, AuthStep } from '../types';
6
+
7
+ export interface UseAuthFormStateReturn extends AuthFormState, AuthFormStateHandlers {}
8
+
9
+ /**
10
+ * Hook for auth form state management.
11
+ * Pure state - no side effects, no API calls.
12
+ */
13
+ export const useAuthFormState = (
14
+ initialIdentifier = '',
15
+ initialChannel: AuthChannel = 'email'
16
+ ): UseAuthFormStateReturn => {
17
+ const [identifier, setIdentifier] = useState(initialIdentifier);
18
+ const [channel, setChannel] = useState<AuthChannel>(initialChannel);
19
+ const [otp, setOtp] = useState('');
20
+ const [isLoading, setIsLoading] = useState(false);
21
+ const [acceptedTerms, setAcceptedTerms] = useState(false);
22
+ const [step, setStep] = useState<AuthStep>('identifier');
23
+ const [error, setError] = useState('');
24
+
25
+ // 2FA state
26
+ const [twoFactorSessionId, setTwoFactorSessionId] = useState<string | null>(null);
27
+ const [shouldPrompt2FA, setShouldPrompt2FA] = useState(false);
28
+ const [twoFactorCode, setTwoFactorCode] = useState('');
29
+ const [useBackupCode, setUseBackupCode] = useState(false);
30
+
31
+ const clearError = useCallback(() => setError(''), []);
32
+
33
+ return {
34
+ // State
35
+ identifier,
36
+ channel,
37
+ otp,
38
+ isLoading,
39
+ acceptedTerms,
40
+ step,
41
+ error,
42
+ twoFactorSessionId,
43
+ shouldPrompt2FA,
44
+ twoFactorCode,
45
+ useBackupCode,
46
+ // Handlers
47
+ setIdentifier,
48
+ setChannel,
49
+ setOtp,
50
+ setAcceptedTerms,
51
+ setError,
52
+ clearError,
53
+ setStep,
54
+ setIsLoading,
55
+ setTwoFactorSessionId,
56
+ setShouldPrompt2FA,
57
+ setTwoFactorCode,
58
+ setUseBackupCode,
59
+ };
60
+ };
@@ -0,0 +1,77 @@
1
+ "use client"
2
+
3
+ import { useCallback } from 'react';
4
+
5
+ import type { AuthChannel, AuthFormValidation } from '../types';
6
+
7
+ // Email regex - RFC 5322 simplified
8
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9
+
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
+ 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;
36
+ }, []);
37
+
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
+ };
61
+ };
62
+
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
+ };
@@ -12,7 +12,11 @@ export interface UseGithubAuthOptions {
12
12
  sourceUrl?: string;
13
13
  onSuccess?: (user: any, isNewUser: boolean) => void;
14
14
  onError?: (error: string) => void;
15
+ /** Callback when 2FA is required */
16
+ onRequires2FA?: (sessionId: string, shouldPrompt2FA: boolean) => void;
15
17
  redirectUrl?: string;
18
+ /** Skip automatic redirect after success (caller handles navigation) */
19
+ skipRedirect?: boolean;
16
20
  }
17
21
 
18
22
  export interface UseGithubAuthReturn {
@@ -42,7 +46,7 @@ export interface UseGithubAuthReturn {
42
46
  * ```
43
47
  */
44
48
  export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuthReturn => {
45
- const { sourceUrl, onSuccess, onError, redirectUrl } = options;
49
+ const { sourceUrl, onSuccess, onError, onRequires2FA, redirectUrl, skipRedirect = false } = options;
46
50
  const router = useCfgRouter();
47
51
 
48
52
  const [isLoading, setIsLoading] = useState(false);
@@ -132,6 +136,21 @@ export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuth
132
136
  state,
133
137
  });
134
138
 
139
+ // Check if 2FA is required
140
+ if (response.requires_2fa && response.session_id) {
141
+ authLogger.info('GitHub OAuth requires 2FA, session:', response.session_id);
142
+
143
+ // Track 2FA requirement
144
+ Analytics.event(AnalyticsEvent.AUTH_OAUTH_START, {
145
+ category: AnalyticsCategory.AUTH,
146
+ label: 'github-2fa-required',
147
+ });
148
+
149
+ // Call 2FA callback
150
+ onRequires2FA?.(response.session_id, response.should_prompt_2fa || false);
151
+ return;
152
+ }
153
+
135
154
  if (!response.access || !response.refresh) {
136
155
  throw new Error('Invalid response from OAuth callback');
137
156
  }
@@ -155,10 +174,12 @@ export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuth
155
174
  // Call success callback
156
175
  onSuccess?.(response.user, response.is_new_user || false);
157
176
 
158
- // Redirect to dashboard or specified URL
159
- // Use hardPush for full page reload - ensures all React contexts reinitialize
160
- const finalRedirectUrl = redirectUrl || '/dashboard';
161
- router.hardPush(finalRedirectUrl);
177
+ // Redirect (unless skipRedirect is true)
178
+ if (!skipRedirect) {
179
+ // Use hardPush for full page reload - ensures all React contexts reinitialize
180
+ const finalRedirectUrl = redirectUrl || '/dashboard';
181
+ router.hardPush(finalRedirectUrl);
182
+ }
162
183
 
163
184
  } catch (err) {
164
185
  const errorMessage = err instanceof Error ? err.message : 'GitHub authentication failed';
@@ -0,0 +1,239 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useState } from 'react';
4
+
5
+ import { useCfgRouter } from '@djangocfg/ui-nextjs/hooks';
6
+
7
+ import { apiAccounts, apiTotp } from '../../clients';
8
+ import { Analytics, AnalyticsCategory, AnalyticsEvent } from '../utils/analytics';
9
+ import { authLogger } from '../utils/logger';
10
+
11
+ export interface UseTwoFactorOptions {
12
+ /** Callback on successful 2FA verification */
13
+ onSuccess?: (user: any) => void;
14
+ /** Callback on error */
15
+ onError?: (error: string) => void;
16
+ /** URL to redirect after successful verification */
17
+ redirectUrl?: string;
18
+ /** Skip automatic redirect after success (caller handles navigation) */
19
+ skipRedirect?: boolean;
20
+ }
21
+
22
+ export interface UseTwoFactorReturn {
23
+ /** Loading state */
24
+ isLoading: boolean;
25
+ /** Error message */
26
+ error: string | null;
27
+ /** Warning message (e.g., low backup codes) */
28
+ warning: string | null;
29
+ /** Remaining backup codes (if backup code was used) */
30
+ remainingBackupCodes: number | null;
31
+ /** Verify TOTP code */
32
+ verifyTOTP: (sessionId: string, code: string) => Promise<boolean>;
33
+ /** Verify backup code */
34
+ verifyBackupCode: (sessionId: string, backupCode: string) => Promise<boolean>;
35
+ /** Clear error */
36
+ clearError: () => void;
37
+ }
38
+
39
+ /**
40
+ * Hook for 2FA verification during login.
41
+ *
42
+ * Usage:
43
+ * 1. After OTP/OAuth verification returns requires_2fa=true and session_id
44
+ * 2. Show 2FA form and collect TOTP code from user
45
+ * 3. Call verifyTOTP(sessionId, code) to complete authentication
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * const { isLoading, error, verifyTOTP, verifyBackupCode } = useTwoFactor({
50
+ * onSuccess: (user) => console.log('Logged in:', user),
51
+ * onError: (error) => console.error(error),
52
+ * redirectUrl: '/dashboard',
53
+ * });
54
+ *
55
+ * const handleSubmit = async (code: string) => {
56
+ * if (useBackupCode) {
57
+ * await verifyBackupCode(sessionId, code);
58
+ * } else {
59
+ * await verifyTOTP(sessionId, code);
60
+ * }
61
+ * };
62
+ * ```
63
+ */
64
+ export const useTwoFactor = (options: UseTwoFactorOptions = {}): UseTwoFactorReturn => {
65
+ const { onSuccess, onError, redirectUrl, skipRedirect = false } = options;
66
+ const router = useCfgRouter();
67
+
68
+ const [isLoading, setIsLoading] = useState(false);
69
+ const [error, setError] = useState<string | null>(null);
70
+ const [warning, setWarning] = useState<string | null>(null);
71
+ const [remainingBackupCodes, setRemainingBackupCodes] = useState<number | null>(null);
72
+
73
+ const clearError = useCallback(() => {
74
+ setError(null);
75
+ }, []);
76
+
77
+ /**
78
+ * Handle successful 2FA verification
79
+ */
80
+ const handleSuccess = useCallback((response: {
81
+ access_token: string;
82
+ refresh_token: string;
83
+ user: any;
84
+ warning?: string;
85
+ remaining_backup_codes?: number;
86
+ }) => {
87
+ // Save tokens
88
+ apiAccounts.setToken(response.access_token, response.refresh_token);
89
+
90
+ // Set warning if any
91
+ if (response.warning) {
92
+ setWarning(response.warning);
93
+ }
94
+
95
+ // Set remaining backup codes if provided
96
+ if (response.remaining_backup_codes !== undefined) {
97
+ setRemainingBackupCodes(response.remaining_backup_codes);
98
+ }
99
+
100
+ // Track successful 2FA
101
+ Analytics.event(AnalyticsEvent.AUTH_LOGIN_SUCCESS, {
102
+ category: AnalyticsCategory.AUTH,
103
+ label: '2fa',
104
+ });
105
+
106
+ // Set user ID for tracking
107
+ if (response.user?.id) {
108
+ Analytics.setUser(String(response.user.id));
109
+ }
110
+
111
+ // Call success callback
112
+ onSuccess?.(response.user);
113
+
114
+ // Redirect (unless skipRedirect is true)
115
+ if (!skipRedirect) {
116
+ const finalRedirectUrl = redirectUrl || '/dashboard';
117
+ authLogger.info('2FA successful, redirecting to:', finalRedirectUrl);
118
+ router.hardPush(finalRedirectUrl);
119
+ }
120
+ }, [onSuccess, redirectUrl, router, skipRedirect]);
121
+
122
+ /**
123
+ * Verify TOTP code from authenticator app
124
+ */
125
+ const verifyTOTP = useCallback(async (sessionId: string, code: string): Promise<boolean> => {
126
+ if (!sessionId) {
127
+ const msg = 'Missing 2FA session ID';
128
+ setError(msg);
129
+ onError?.(msg);
130
+ return false;
131
+ }
132
+
133
+ if (!code || code.length !== 6) {
134
+ const msg = 'Please enter a 6-digit code';
135
+ setError(msg);
136
+ onError?.(msg);
137
+ return false;
138
+ }
139
+
140
+ setIsLoading(true);
141
+ setError(null);
142
+
143
+ try {
144
+ authLogger.info('Verifying TOTP code...');
145
+
146
+ const response = await apiTotp.totp_verification.totpVerifyCreate({
147
+ session_id: sessionId,
148
+ code,
149
+ });
150
+
151
+ if (!response.access_token || !response.refresh_token) {
152
+ throw new Error('Invalid response from 2FA verification');
153
+ }
154
+
155
+ handleSuccess(response);
156
+ return true;
157
+
158
+ } catch (err) {
159
+ const errorMessage = err instanceof Error ? err.message : 'Invalid verification code';
160
+ authLogger.error('2FA TOTP verification error:', err);
161
+ setError(errorMessage);
162
+ onError?.(errorMessage);
163
+
164
+ // Track failed 2FA
165
+ Analytics.event(AnalyticsEvent.AUTH_OTP_VERIFY_FAIL, {
166
+ category: AnalyticsCategory.AUTH,
167
+ label: '2fa-totp',
168
+ });
169
+
170
+ return false;
171
+ } finally {
172
+ setIsLoading(false);
173
+ }
174
+ }, [handleSuccess, onError]);
175
+
176
+ /**
177
+ * Verify backup recovery code
178
+ */
179
+ const verifyBackupCode = useCallback(async (sessionId: string, backupCode: string): Promise<boolean> => {
180
+ if (!sessionId) {
181
+ const msg = 'Missing 2FA session ID';
182
+ setError(msg);
183
+ onError?.(msg);
184
+ return false;
185
+ }
186
+
187
+ if (!backupCode || backupCode.length < 8) {
188
+ const msg = 'Please enter your backup code';
189
+ setError(msg);
190
+ onError?.(msg);
191
+ return false;
192
+ }
193
+
194
+ setIsLoading(true);
195
+ setError(null);
196
+
197
+ try {
198
+ authLogger.info('Verifying backup code...');
199
+
200
+ const response = await apiTotp.totp_verification.totpVerifyBackupCreate({
201
+ session_id: sessionId,
202
+ backup_code: backupCode.replace(/\s+/g, ''), // Remove spaces
203
+ });
204
+
205
+ if (!response.access_token || !response.refresh_token) {
206
+ throw new Error('Invalid response from backup code verification');
207
+ }
208
+
209
+ handleSuccess(response);
210
+ return true;
211
+
212
+ } catch (err) {
213
+ const errorMessage = err instanceof Error ? err.message : 'Invalid backup code';
214
+ authLogger.error('2FA backup code verification error:', err);
215
+ setError(errorMessage);
216
+ onError?.(errorMessage);
217
+
218
+ // Track failed 2FA
219
+ Analytics.event(AnalyticsEvent.AUTH_OTP_VERIFY_FAIL, {
220
+ category: AnalyticsCategory.AUTH,
221
+ label: '2fa-backup',
222
+ });
223
+
224
+ return false;
225
+ } finally {
226
+ setIsLoading(false);
227
+ }
228
+ }, [handleSuccess, onError]);
229
+
230
+ return {
231
+ isLoading,
232
+ error,
233
+ warning,
234
+ remainingBackupCodes,
235
+ verifyTOTP,
236
+ verifyBackupCode,
237
+ clearError,
238
+ };
239
+ };
@@ -0,0 +1,213 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useState } from 'react';
4
+
5
+ import { apiTotp } from '../../clients';
6
+ import { authLogger } from '../utils/logger';
7
+
8
+ export interface TwoFactorSetupData {
9
+ /** Device ID to use for confirmation */
10
+ deviceId: string;
11
+ /** Base32-encoded TOTP secret (for manual entry) */
12
+ secret: string;
13
+ /** otpauth:// URI for QR code generation */
14
+ provisioningUri: string;
15
+ /** Base64-encoded QR code image (data URI) */
16
+ qrCodeBase64: string;
17
+ /** Seconds until setup expires */
18
+ expiresIn: number;
19
+ }
20
+
21
+ export interface UseTwoFactorSetupOptions {
22
+ /** Callback when setup is confirmed and backup codes are generated */
23
+ onComplete?: (backupCodes: string[]) => void;
24
+ /** Callback on error */
25
+ onError?: (error: string) => void;
26
+ }
27
+
28
+ export interface UseTwoFactorSetupReturn {
29
+ /** Loading state */
30
+ isLoading: boolean;
31
+ /** Error message */
32
+ error: string | null;
33
+ /** Setup data (QR code, secret, etc.) */
34
+ setupData: TwoFactorSetupData | null;
35
+ /** Backup codes after confirmation */
36
+ backupCodes: string[] | null;
37
+ /** Warning message about backup codes */
38
+ backupCodesWarning: string | null;
39
+ /** Current setup step */
40
+ setupStep: 'idle' | 'scanning' | 'confirming' | 'complete';
41
+ /** Start 2FA setup - returns QR code data */
42
+ startSetup: (deviceName?: string) => Promise<TwoFactorSetupData | null>;
43
+ /** Confirm setup with TOTP code - returns backup codes */
44
+ confirmSetup: (code: string) => Promise<string[] | null>;
45
+ /** Reset setup state */
46
+ resetSetup: () => void;
47
+ /** Clear error */
48
+ clearError: () => void;
49
+ }
50
+
51
+ /**
52
+ * Hook for 2FA setup (enabling TOTP authentication).
53
+ *
54
+ * Flow:
55
+ * 1. Call startSetup() to get QR code and provisioning URI
56
+ * 2. User scans QR code with authenticator app
57
+ * 3. Call confirmSetup(code) with the 6-digit code from app
58
+ * 4. Show backup codes to user (they must save these!)
59
+ *
60
+ * @example
61
+ * ```tsx
62
+ * const {
63
+ * isLoading,
64
+ * error,
65
+ * setupData,
66
+ * backupCodes,
67
+ * setupStep,
68
+ * startSetup,
69
+ * confirmSetup,
70
+ * } = useTwoFactorSetup({
71
+ * onComplete: (codes) => console.log('Backup codes:', codes),
72
+ * onError: (error) => console.error(error),
73
+ * });
74
+ *
75
+ * // Start setup
76
+ * const data = await startSetup('My iPhone');
77
+ *
78
+ * // Show QR code
79
+ * <QRCodeSVG value={data.provisioningUri} />
80
+ *
81
+ * // Confirm with code from app
82
+ * const codes = await confirmSetup('123456');
83
+ * ```
84
+ */
85
+ export const useTwoFactorSetup = (options: UseTwoFactorSetupOptions = {}): UseTwoFactorSetupReturn => {
86
+ const { onComplete, onError } = options;
87
+
88
+ const [isLoading, setIsLoading] = useState(false);
89
+ const [error, setError] = useState<string | null>(null);
90
+ const [setupData, setSetupData] = useState<TwoFactorSetupData | null>(null);
91
+ const [backupCodes, setBackupCodes] = useState<string[] | null>(null);
92
+ const [backupCodesWarning, setBackupCodesWarning] = useState<string | null>(null);
93
+ const [setupStep, setSetupStep] = useState<'idle' | 'scanning' | 'confirming' | 'complete'>('idle');
94
+
95
+ const clearError = useCallback(() => {
96
+ setError(null);
97
+ }, []);
98
+
99
+ const resetSetup = useCallback(() => {
100
+ setSetupData(null);
101
+ setBackupCodes(null);
102
+ setBackupCodesWarning(null);
103
+ setSetupStep('idle');
104
+ setError(null);
105
+ }, []);
106
+
107
+ /**
108
+ * Start 2FA setup - generates QR code and secret
109
+ */
110
+ const startSetup = useCallback(async (deviceName?: string): Promise<TwoFactorSetupData | null> => {
111
+ setIsLoading(true);
112
+ setError(null);
113
+ setSetupStep('scanning');
114
+
115
+ try {
116
+ authLogger.info('Starting 2FA setup...');
117
+
118
+ const response = await apiTotp.totp_setup.create({
119
+ device_name: deviceName,
120
+ });
121
+
122
+ const data: TwoFactorSetupData = {
123
+ deviceId: response.device_id,
124
+ secret: response.secret,
125
+ provisioningUri: response.provisioning_uri,
126
+ qrCodeBase64: response.qr_code_base64,
127
+ expiresIn: response.expires_in,
128
+ };
129
+
130
+ setSetupData(data);
131
+ authLogger.info('2FA setup initiated, expires in:', data.expiresIn, 'seconds');
132
+
133
+ return data;
134
+
135
+ } catch (err) {
136
+ const errorMessage = err instanceof Error ? err.message : 'Failed to start 2FA setup';
137
+ authLogger.error('2FA setup error:', err);
138
+ setError(errorMessage);
139
+ setSetupStep('idle');
140
+ onError?.(errorMessage);
141
+ return null;
142
+ } finally {
143
+ setIsLoading(false);
144
+ }
145
+ }, [onError]);
146
+
147
+ /**
148
+ * Confirm 2FA setup with TOTP code from authenticator app
149
+ */
150
+ const confirmSetup = useCallback(async (code: string): Promise<string[] | null> => {
151
+ if (!setupData) {
152
+ const msg = 'Setup not started. Call startSetup() first.';
153
+ setError(msg);
154
+ onError?.(msg);
155
+ return null;
156
+ }
157
+
158
+ if (!code || code.length !== 6) {
159
+ const msg = 'Please enter a 6-digit code';
160
+ setError(msg);
161
+ onError?.(msg);
162
+ return null;
163
+ }
164
+
165
+ setIsLoading(true);
166
+ setError(null);
167
+ setSetupStep('confirming');
168
+
169
+ try {
170
+ authLogger.info('Confirming 2FA setup...');
171
+
172
+ const response = await apiTotp.totp_setup.confirmCreate({
173
+ device_id: setupData.deviceId,
174
+ code,
175
+ });
176
+
177
+ const codes = response.backup_codes;
178
+ setBackupCodes(codes);
179
+ setBackupCodesWarning(response.backup_codes_warning);
180
+ setSetupStep('complete');
181
+
182
+ authLogger.info('2FA setup confirmed, backup codes generated:', codes.length);
183
+
184
+ // Call completion callback
185
+ onComplete?.(codes);
186
+
187
+ return codes;
188
+
189
+ } catch (err) {
190
+ const errorMessage = err instanceof Error ? err.message : 'Invalid code. Please try again.';
191
+ authLogger.error('2FA setup confirmation error:', err);
192
+ setError(errorMessage);
193
+ setSetupStep('scanning'); // Go back to scanning step
194
+ onError?.(errorMessage);
195
+ return null;
196
+ } finally {
197
+ setIsLoading(false);
198
+ }
199
+ }, [setupData, onComplete, onError]);
200
+
201
+ return {
202
+ isLoading,
203
+ error,
204
+ setupData,
205
+ backupCodes,
206
+ backupCodesWarning,
207
+ setupStep,
208
+ startSetup,
209
+ confirmSetup,
210
+ resetSetup,
211
+ clearError,
212
+ };
213
+ };
package/src/auth/index.ts CHANGED
@@ -30,5 +30,8 @@ export * from './context';
30
30
  // Hooks
31
31
  export * from './hooks';
32
32
 
33
+ // Types (single source of truth)
34
+ export * from './types';
35
+
33
36
  // Utils (validation, errors, analytics)
34
37
  export * from './utils';