@djangocfg/api 2.1.457 → 2.1.460
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/auth.cjs +54 -31
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +51 -57
- package/dist/auth.d.ts +51 -57
- package/dist/auth.mjs +54 -31
- package/dist/auth.mjs.map +1 -1
- package/package.json +2 -2
- package/src/auth/__tests__/useAuthForm.2fa.test.ts +2 -2
- package/src/auth/hooks/index.ts +11 -7
- package/src/auth/hooks/twoFactor.shared.ts +54 -0
- package/src/auth/hooks/useAuthForm.ts +2 -2
- package/src/auth/hooks/useTwoFactor.ts +55 -241
- package/src/auth/hooks/useTwoFactorSetup.ts +9 -47
- package/src/auth/hooks/useTwoFactorStatus.ts +6 -40
- package/src/auth/hooks/useTwoFactorVerify.ts +214 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { auth } from '../../_api/generated/helpers/auth';
|
|
6
|
+
import { CfgTotpVerify } from '../../_api/generated/sdk.gen';
|
|
7
|
+
import { Analytics, AnalyticsCategory, AnalyticsEvent } from '../utils/analytics';
|
|
8
|
+
import { authLogger } from '../utils/logger';
|
|
9
|
+
import { consumeSavedRedirect } from './useAuthRedirect';
|
|
10
|
+
import { useCfgRouter } from './useCfgRouter';
|
|
11
|
+
import { extractAttemptsRemaining, extractTwoFactorError, isValidTotpCode } from './twoFactor.shared';
|
|
12
|
+
|
|
13
|
+
export interface UseTwoFactorVerifyOptions {
|
|
14
|
+
/** Callback on successful 2FA verification */
|
|
15
|
+
onSuccess?: (user: any) => void;
|
|
16
|
+
/** Callback on error */
|
|
17
|
+
onError?: (error: string) => void;
|
|
18
|
+
/** URL to redirect after successful verification */
|
|
19
|
+
redirectUrl?: string;
|
|
20
|
+
/** Skip automatic redirect after success (caller handles navigation) */
|
|
21
|
+
skipRedirect?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UseTwoFactorVerifyReturn {
|
|
25
|
+
/** Loading state */
|
|
26
|
+
isLoading: boolean;
|
|
27
|
+
/** Error message */
|
|
28
|
+
error: string | null;
|
|
29
|
+
/** Warning message (e.g., low backup codes) */
|
|
30
|
+
warning: string | null;
|
|
31
|
+
/** Remaining backup codes (if backup code was used) */
|
|
32
|
+
remainingBackupCodes: number | null;
|
|
33
|
+
/** Remaining verification attempts before lockout */
|
|
34
|
+
attemptsRemaining: number | null;
|
|
35
|
+
/** Verify TOTP code */
|
|
36
|
+
verifyTOTP: (sessionId: string, code: string) => Promise<boolean>;
|
|
37
|
+
/** Verify backup code */
|
|
38
|
+
verifyBackupCode: (sessionId: string, backupCode: string) => Promise<boolean>;
|
|
39
|
+
/** Clear error */
|
|
40
|
+
clearError: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 2FA verification during login (internal — composed by {@link useTwoFactor}).
|
|
45
|
+
*
|
|
46
|
+
* After OTP/OAuth verification returns requires_2fa=true and a session_id,
|
|
47
|
+
* collect the TOTP (or backup) code and call verifyTOTP/verifyBackupCode to
|
|
48
|
+
* complete authentication.
|
|
49
|
+
*/
|
|
50
|
+
export const useTwoFactorVerify = (
|
|
51
|
+
options: UseTwoFactorVerifyOptions = {},
|
|
52
|
+
): UseTwoFactorVerifyReturn => {
|
|
53
|
+
const { onSuccess, onError, redirectUrl, skipRedirect = false } = options;
|
|
54
|
+
const router = useCfgRouter();
|
|
55
|
+
|
|
56
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
57
|
+
const [error, setError] = useState<string | null>(null);
|
|
58
|
+
const [warning, setWarning] = useState<string | null>(null);
|
|
59
|
+
const [remainingBackupCodes, setRemainingBackupCodes] = useState<number | null>(null);
|
|
60
|
+
const [attemptsRemaining, setAttemptsRemaining] = useState<number | null>(null);
|
|
61
|
+
|
|
62
|
+
const clearError = useCallback(() => {
|
|
63
|
+
setError(null);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const handleSuccess = useCallback((response: {
|
|
67
|
+
access_token: string;
|
|
68
|
+
refresh_token: string;
|
|
69
|
+
user: any;
|
|
70
|
+
warning?: string;
|
|
71
|
+
remaining_backup_codes?: number;
|
|
72
|
+
}) => {
|
|
73
|
+
// Save tokens — the ONE write path (atomic pair + session notification).
|
|
74
|
+
auth.setSession({ access: response.access_token, refresh: response.refresh_token });
|
|
75
|
+
|
|
76
|
+
if (response.warning) {
|
|
77
|
+
setWarning(response.warning);
|
|
78
|
+
}
|
|
79
|
+
if (response.remaining_backup_codes !== undefined) {
|
|
80
|
+
setRemainingBackupCodes(response.remaining_backup_codes);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Analytics.event(AnalyticsEvent.AUTH_LOGIN_SUCCESS, {
|
|
84
|
+
category: AnalyticsCategory.AUTH,
|
|
85
|
+
label: '2fa',
|
|
86
|
+
});
|
|
87
|
+
if (response.user?.id) {
|
|
88
|
+
Analytics.setUser(String(response.user.id));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onSuccess?.(response.user);
|
|
92
|
+
|
|
93
|
+
// Redirect (unless skipRedirect). Priority: saved back-url > option > default.
|
|
94
|
+
if (!skipRedirect) {
|
|
95
|
+
const finalRedirectUrl = consumeSavedRedirect() || redirectUrl || '/';
|
|
96
|
+
authLogger.info('2FA successful, redirecting to:', finalRedirectUrl);
|
|
97
|
+
router.hardPush(finalRedirectUrl);
|
|
98
|
+
}
|
|
99
|
+
}, [onSuccess, redirectUrl, router, skipRedirect]);
|
|
100
|
+
|
|
101
|
+
const verifyTOTP = useCallback(async (sessionId: string, code: string): Promise<boolean> => {
|
|
102
|
+
if (!sessionId) {
|
|
103
|
+
const msg = 'Missing 2FA session ID';
|
|
104
|
+
setError(msg);
|
|
105
|
+
onError?.(msg);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (!isValidTotpCode(code)) {
|
|
109
|
+
const msg = 'Please enter a 6-digit code';
|
|
110
|
+
setError(msg);
|
|
111
|
+
onError?.(msg);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
setIsLoading(true);
|
|
116
|
+
setError(null);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
authLogger.info('Verifying TOTP code...');
|
|
120
|
+
|
|
121
|
+
const result = await CfgTotpVerify.cfgTotpVerifyCreate({
|
|
122
|
+
body: { session_id: sessionId, code },
|
|
123
|
+
throwOnError: true,
|
|
124
|
+
});
|
|
125
|
+
const response = result.data;
|
|
126
|
+
|
|
127
|
+
if (!response.access_token || !response.refresh_token) {
|
|
128
|
+
throw new Error('Invalid response from 2FA verification');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
handleSuccess(response);
|
|
132
|
+
return true;
|
|
133
|
+
} catch (err) {
|
|
134
|
+
authLogger.error('2FA TOTP verification error:', err);
|
|
135
|
+
const errorMessage = extractTwoFactorError(err, 'Invalid verification code');
|
|
136
|
+
const attempts = extractAttemptsRemaining(err);
|
|
137
|
+
if (attempts !== null) setAttemptsRemaining(attempts);
|
|
138
|
+
setError(errorMessage);
|
|
139
|
+
onError?.(errorMessage);
|
|
140
|
+
|
|
141
|
+
Analytics.event(AnalyticsEvent.AUTH_OTP_VERIFY_FAIL, {
|
|
142
|
+
category: AnalyticsCategory.AUTH,
|
|
143
|
+
label: '2fa-totp',
|
|
144
|
+
});
|
|
145
|
+
return false;
|
|
146
|
+
} finally {
|
|
147
|
+
setIsLoading(false);
|
|
148
|
+
}
|
|
149
|
+
}, [handleSuccess, onError]);
|
|
150
|
+
|
|
151
|
+
const verifyBackupCode = useCallback(async (sessionId: string, backupCode: string): Promise<boolean> => {
|
|
152
|
+
if (!sessionId) {
|
|
153
|
+
const msg = 'Missing 2FA session ID';
|
|
154
|
+
setError(msg);
|
|
155
|
+
onError?.(msg);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (!backupCode || backupCode.length < 8) {
|
|
159
|
+
const msg = 'Please enter your backup code';
|
|
160
|
+
setError(msg);
|
|
161
|
+
onError?.(msg);
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
setIsLoading(true);
|
|
166
|
+
setError(null);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
authLogger.info('Verifying backup code...');
|
|
170
|
+
|
|
171
|
+
const result = await CfgTotpVerify.cfgTotpVerifyBackupCreate({
|
|
172
|
+
body: {
|
|
173
|
+
session_id: sessionId,
|
|
174
|
+
backup_code: backupCode.replace(/\s+/g, ''),
|
|
175
|
+
},
|
|
176
|
+
throwOnError: true,
|
|
177
|
+
});
|
|
178
|
+
const response = result.data;
|
|
179
|
+
|
|
180
|
+
if (!response.access_token || !response.refresh_token) {
|
|
181
|
+
throw new Error('Invalid response from backup code verification');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
handleSuccess(response);
|
|
185
|
+
return true;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
authLogger.error('2FA backup code verification error:', err);
|
|
188
|
+
const errorMessage = extractTwoFactorError(err, 'Invalid backup code');
|
|
189
|
+
const attempts = extractAttemptsRemaining(err);
|
|
190
|
+
if (attempts !== null) setAttemptsRemaining(attempts);
|
|
191
|
+
setError(errorMessage);
|
|
192
|
+
onError?.(errorMessage);
|
|
193
|
+
|
|
194
|
+
Analytics.event(AnalyticsEvent.AUTH_OTP_VERIFY_FAIL, {
|
|
195
|
+
category: AnalyticsCategory.AUTH,
|
|
196
|
+
label: '2fa-backup',
|
|
197
|
+
});
|
|
198
|
+
return false;
|
|
199
|
+
} finally {
|
|
200
|
+
setIsLoading(false);
|
|
201
|
+
}
|
|
202
|
+
}, [handleSuccess, onError]);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
isLoading,
|
|
206
|
+
error,
|
|
207
|
+
warning,
|
|
208
|
+
remainingBackupCodes,
|
|
209
|
+
attemptsRemaining,
|
|
210
|
+
verifyTOTP,
|
|
211
|
+
verifyBackupCode,
|
|
212
|
+
clearError,
|
|
213
|
+
};
|
|
214
|
+
};
|