@djangocfg/api 2.1.546 → 2.1.547
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/dist/auth.cjs +331 -73
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +90 -1
- package/dist/auth.d.ts +90 -1
- package/dist/auth.mjs +331 -73
- package/dist/auth.mjs.map +1 -1
- package/dist/clients.cjs +18 -0
- package/dist/clients.cjs.map +1 -1
- package/dist/clients.d.cts +96 -0
- package/dist/clients.d.ts +96 -0
- package/dist/clients.mjs +18 -0
- package/dist/clients.mjs.map +1 -1
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.mjs.map +1 -1
- package/dist/index.cjs +18 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +171 -3
- package/dist/index.d.ts +171 -3
- package/dist/index.mjs +18 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/_api/generated/_cfg_accounts/hooks/index.ts +1 -0
- package/src/_api/generated/_cfg_accounts/hooks/useCfgAccountsTokenCreate.ts +64 -0
- package/src/_api/generated/_cfg_accounts/openapi.json +157 -0
- package/src/_api/generated/_cfg_accounts/schemas/PasswordLoginError.ts +13 -0
- package/src/_api/generated/_cfg_accounts/schemas/PasswordLoginRequest.ts +13 -0
- package/src/_api/generated/_cfg_accounts/schemas/PasswordLoginResponse.ts +18 -0
- package/src/_api/generated/_cfg_accounts/schemas/index.ts +3 -0
- package/src/_api/generated/openapi.json +157 -0
- package/src/_api/generated/sdk.gen.ts +20 -1
- package/src/_api/generated/types.gen.ts +149 -0
- package/src/auth/__tests__/passwordLogin.test.tsx +239 -0
- package/src/auth/context/AccountsContext.tsx +32 -7
- package/src/auth/context/AuthContext.tsx +104 -1
- package/src/auth/context/types.ts +10 -1
- package/src/auth/hooks/useAuthForm.ts +84 -0
- package/src/auth/hooks/useAuthFormState.ts +3 -0
- package/src/auth/types/form.ts +41 -1
- package/src/auth/types/index.ts +2 -0
- package/src/auth/utils/analytics.ts +1 -0
|
@@ -43,6 +43,7 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
|
|
|
43
43
|
const {
|
|
44
44
|
requestOTP,
|
|
45
45
|
verifyOTP,
|
|
46
|
+
login,
|
|
46
47
|
getSavedEmail,
|
|
47
48
|
saveEmail,
|
|
48
49
|
} = useAuth();
|
|
@@ -51,6 +52,7 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
|
|
|
51
52
|
const {
|
|
52
53
|
identifier,
|
|
53
54
|
otp,
|
|
55
|
+
password,
|
|
54
56
|
acceptedTerms,
|
|
55
57
|
marketingConsent,
|
|
56
58
|
rememberMe,
|
|
@@ -235,6 +237,87 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
|
|
|
235
237
|
await submitOTP(identifier, otp);
|
|
236
238
|
}, [identifier, otp, submitOTP]);
|
|
237
239
|
|
|
240
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
241
|
+
// Password Submit
|
|
242
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
// The password credential lands on the SAME step machine as the OTP code:
|
|
245
|
+
// both end at '2fa' or 'success', so the layout renders one set of screens.
|
|
246
|
+
const submitPassword = useCallback(async (
|
|
247
|
+
submitIdentifier: string,
|
|
248
|
+
submitPassword_: string,
|
|
249
|
+
): Promise<boolean> => {
|
|
250
|
+
if (!submitIdentifier || !validateIdentifier(submitIdentifier)) {
|
|
251
|
+
const msg = 'Please enter a valid email address';
|
|
252
|
+
setError(msg);
|
|
253
|
+
onError?.(msg);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!submitPassword_) {
|
|
258
|
+
const msg = 'Please enter your password';
|
|
259
|
+
setError(msg);
|
|
260
|
+
onError?.(msg);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setIsLoading(true);
|
|
265
|
+
clearError();
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
// Skip redirect - we'll show success screen first
|
|
269
|
+
const result = await login(submitIdentifier, submitPassword_, {
|
|
270
|
+
redirectUrl,
|
|
271
|
+
skipRedirect: true,
|
|
272
|
+
rememberMe,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (result.requires_2fa && result.session_id) {
|
|
276
|
+
authLogger.info('2FA required after password login');
|
|
277
|
+
setTwoFactorSessionId(result.session_id);
|
|
278
|
+
setShouldPrompt2FA(result.should_prompt_2fa || false);
|
|
279
|
+
setStep('2fa');
|
|
280
|
+
saveIdentifierToStorage(submitIdentifier);
|
|
281
|
+
return true; // Password was accepted, now need 2FA
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (result.success) {
|
|
285
|
+
saveIdentifierToStorage(submitIdentifier);
|
|
286
|
+
authLogger.info('Password login successful, showing success screen');
|
|
287
|
+
setStep('success');
|
|
288
|
+
onOTPSuccess?.();
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// A lockout (429) carries a retry-after; show the countdown instead of a
|
|
293
|
+
// message the user can only re-trigger.
|
|
294
|
+
if (result.retryAfter) {
|
|
295
|
+
startRateLimitCountdown(result.retryAfter);
|
|
296
|
+
clearError();
|
|
297
|
+
} else {
|
|
298
|
+
setError(result.message);
|
|
299
|
+
onError?.(result.message);
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
} catch {
|
|
303
|
+
const msg = 'An unexpected error occurred';
|
|
304
|
+
setError(msg);
|
|
305
|
+
onError?.(msg);
|
|
306
|
+
return false;
|
|
307
|
+
} finally {
|
|
308
|
+
setIsLoading(false);
|
|
309
|
+
}
|
|
310
|
+
}, [
|
|
311
|
+
login, validateIdentifier, saveIdentifierToStorage, setError, setIsLoading,
|
|
312
|
+
clearError, startRateLimitCountdown, onOTPSuccess, onError, redirectUrl,
|
|
313
|
+
rememberMe, setTwoFactorSessionId, setShouldPrompt2FA, setStep,
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
const handlePasswordSubmit = useCallback(async (e: React.FormEvent) => {
|
|
317
|
+
e.preventDefault();
|
|
318
|
+
await submitPassword(identifier, password);
|
|
319
|
+
}, [identifier, password, submitPassword]);
|
|
320
|
+
|
|
238
321
|
const handleResendOTP = useCallback(async () => {
|
|
239
322
|
setIsLoading(true);
|
|
240
323
|
clearError();
|
|
@@ -370,6 +453,7 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
|
|
|
370
453
|
// Submit handlers
|
|
371
454
|
handleIdentifierSubmit,
|
|
372
455
|
handleOTPSubmit,
|
|
456
|
+
handlePasswordSubmit,
|
|
373
457
|
handleResendOTP,
|
|
374
458
|
handleBackToIdentifier,
|
|
375
459
|
forceOTPStep,
|
|
@@ -21,6 +21,7 @@ export const useAuthFormState = (
|
|
|
21
21
|
): UseAuthFormStateReturn => {
|
|
22
22
|
const [identifier, setIdentifier] = useState(initialIdentifier);
|
|
23
23
|
const [otp, setOtp] = useState('');
|
|
24
|
+
const [password, setPassword] = useState('');
|
|
24
25
|
const [isLoading, setIsLoading] = useState(false);
|
|
25
26
|
const [acceptedTerms, setAcceptedTerms] = useState(true);
|
|
26
27
|
// null until the marketing-consent feature (if enabled) resolves its
|
|
@@ -66,6 +67,7 @@ export const useAuthFormState = (
|
|
|
66
67
|
// State
|
|
67
68
|
identifier,
|
|
68
69
|
otp,
|
|
70
|
+
password,
|
|
69
71
|
isLoading,
|
|
70
72
|
acceptedTerms,
|
|
71
73
|
marketingConsent,
|
|
@@ -83,6 +85,7 @@ export const useAuthFormState = (
|
|
|
83
85
|
// Handlers
|
|
84
86
|
setIdentifier,
|
|
85
87
|
setOtp,
|
|
88
|
+
setPassword,
|
|
86
89
|
setAcceptedTerms,
|
|
87
90
|
setMarketingConsent,
|
|
88
91
|
setRememberMe,
|
package/src/auth/types/form.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { MutableRefObject } from 'react';
|
|
9
9
|
|
|
10
|
-
import type { WebmailLink } from '../../_api/generated/types.gen';
|
|
10
|
+
import type { User, WebmailLink } from '../../_api/generated/types.gen';
|
|
11
11
|
|
|
12
12
|
export type { WebmailLink };
|
|
13
13
|
|
|
@@ -41,6 +41,41 @@ export interface OTPRequestConsent {
|
|
|
41
41
|
disclosureVersion: string;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// Password Login Types
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export interface PasswordLoginOptions {
|
|
49
|
+
/** URL to redirect to after a successful login (ignored when skipRedirect). */
|
|
50
|
+
redirectUrl?: string;
|
|
51
|
+
/** Caller owns ALL navigation for this login, including the 2FA continuation. */
|
|
52
|
+
skipRedirect?: boolean;
|
|
53
|
+
/** Ask the server for a 30-day session instead of the default lifetime. */
|
|
54
|
+
rememberMe?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Result of a password login.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately the same shape as the verifyOTP result plus the failure codes
|
|
61
|
+
* the password endpoint adds: a sign-in form branches on `requires_2fa` the
|
|
62
|
+
* same way whichever credential it collected.
|
|
63
|
+
*/
|
|
64
|
+
export interface PasswordLoginResult {
|
|
65
|
+
success: boolean;
|
|
66
|
+
message: string;
|
|
67
|
+
user?: User;
|
|
68
|
+
requires_2fa?: boolean;
|
|
69
|
+
session_id?: string | null;
|
|
70
|
+
should_prompt_2fa?: boolean;
|
|
71
|
+
/** HTTP status of a failure — 401 for bad credentials, 429 when throttled. */
|
|
72
|
+
statusCode?: number;
|
|
73
|
+
/** Machine-readable failure code, e.g. `invalid_credentials`, `locked_out`. */
|
|
74
|
+
errorCode?: string;
|
|
75
|
+
/** Seconds until a throttled caller may retry (429 only). */
|
|
76
|
+
retryAfter?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
44
79
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
80
|
// Form State
|
|
46
81
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -50,6 +85,8 @@ export interface AuthFormState {
|
|
|
50
85
|
identifier: string;
|
|
51
86
|
/** OTP code input */
|
|
52
87
|
otp: string;
|
|
88
|
+
/** Password input (password sign-in; empty for the OTP-only flow) */
|
|
89
|
+
password: string;
|
|
53
90
|
/** Loading state */
|
|
54
91
|
isLoading: boolean;
|
|
55
92
|
/** Terms acceptance state */
|
|
@@ -91,6 +128,7 @@ export interface AuthFormState {
|
|
|
91
128
|
export interface AuthFormStateHandlers {
|
|
92
129
|
setIdentifier: (identifier: string) => void;
|
|
93
130
|
setOtp: (otp: string) => void;
|
|
131
|
+
setPassword: (password: string) => void;
|
|
94
132
|
setAcceptedTerms: (accepted: boolean) => void;
|
|
95
133
|
setMarketingConsent: (consent: boolean | null) => void;
|
|
96
134
|
setRememberMe: (rememberMe: boolean) => void;
|
|
@@ -111,6 +149,8 @@ export interface AuthFormStateHandlers {
|
|
|
111
149
|
export interface AuthFormSubmitHandlers {
|
|
112
150
|
handleIdentifierSubmit: (e: React.FormEvent) => Promise<void>;
|
|
113
151
|
handleOTPSubmit: (e: React.FormEvent) => Promise<void>;
|
|
152
|
+
/** Submit email + password against the password grant */
|
|
153
|
+
handlePasswordSubmit: (e: React.FormEvent) => Promise<void>;
|
|
114
154
|
handleResendOTP: () => Promise<void>;
|
|
115
155
|
handleBackToIdentifier: () => void;
|
|
116
156
|
forceOTPStep: () => void;
|
package/src/auth/types/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export enum AnalyticsEvent {
|
|
|
19
19
|
AUTH_OTP_REQUEST = 'auth_otp_request',
|
|
20
20
|
AUTH_LOGIN_SUCCESS = 'auth_login_success',
|
|
21
21
|
AUTH_OTP_VERIFY_FAIL = 'auth_otp_verify_fail',
|
|
22
|
+
AUTH_PASSWORD_LOGIN_FAIL = 'auth_password_login_fail',
|
|
22
23
|
AUTH_SESSION_EXPIRED = 'auth_session_expired',
|
|
23
24
|
AUTH_TOKEN_REFRESH = 'auth_token_refresh',
|
|
24
25
|
AUTH_TOKEN_REFRESH_FAIL = 'auth_token_refresh_fail',
|