@djangocfg/api 2.1.582 → 2.1.584
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 +18 -0
- package/dist/OAuthTokenResponse-WEDM6IP5.mjs +19 -0
- package/dist/OAuthTokenResponse-WEDM6IP5.mjs.map +1 -0
- package/dist/OTPVerifyResponse-P5LDNM7H.mjs +20 -0
- package/dist/OTPVerifyResponse-P5LDNM7H.mjs.map +1 -0
- package/dist/PasswordLoginResponse-33B576NG.mjs +20 -0
- package/dist/PasswordLoginResponse-33B576NG.mjs.map +1 -0
- package/dist/auth.cjs +279 -103
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +14 -41
- package/dist/auth.d.ts +14 -41
- package/dist/auth.mjs +279 -101
- package/dist/auth.mjs.map +1 -1
- package/dist/clients.cjs +242 -54
- package/dist/clients.cjs.map +1 -1
- package/dist/clients.d.cts +13 -41
- package/dist/clients.d.ts +13 -41
- package/dist/clients.mjs +242 -54
- package/dist/clients.mjs.map +1 -1
- package/dist/hooks.cjs +242 -54
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.mjs +242 -54
- package/dist/hooks.mjs.map +1 -1
- package/dist/index.cjs +247 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -63
- package/dist/index.d.ts +90 -63
- package/dist/index.mjs +247 -54
- package/dist/index.mjs.map +1 -1
- package/dist/storage.cjs +268 -0
- package/dist/storage.cjs.map +1 -0
- package/dist/storage.d.cts +84 -0
- package/dist/storage.d.ts +84 -0
- package/dist/storage.mjs +247 -0
- package/dist/storage.mjs.map +1 -0
- package/package.json +8 -2
- package/src/__tests__/noStorageLiterals.test.ts +27 -0
- package/src/__tests__/storage.test.ts +134 -0
- package/src/__tests__/storageSsr.test.ts +14 -0
- package/src/_api/generated/_cfg_accounts/openapi.json +1 -28
- package/src/_api/generated/_cfg_accounts/schemas/OAuthCallbackRequestRequest.ts +0 -1
- package/src/_api/generated/_cfg_accounts/schemas/OAuthTokenResponse.ts +0 -1
- package/src/_api/generated/_cfg_accounts/schemas/OTPVerifyRequest.ts +0 -1
- package/src/_api/generated/_cfg_accounts/schemas/OTPVerifyResponse.ts +0 -1
- package/src/_api/generated/_cfg_accounts/schemas/PasswordLoginRequest.ts +0 -1
- package/src/_api/generated/_cfg_accounts/schemas/PasswordLoginResponse.ts +0 -1
- package/src/_api/generated/_cfg_totp/openapi.json +0 -5
- package/src/_api/generated/_cfg_totp/schemas/VerifyResponse.ts +0 -1
- package/src/_api/generated/helpers/auth.ts +28 -65
- package/src/_api/generated/helpers/index.ts +12 -0
- package/src/_api/generated/helpers/keys.ts +327 -0
- package/src/_api/generated/openapi.json +1 -33
- package/src/_api/generated/types.gen.ts +4 -59
- package/src/auth/__tests__/authRedirect.test.ts +1 -1
- package/src/auth/__tests__/passwordLogin.test.tsx +1 -3
- package/src/auth/__tests__/profileCache.test.ts +1 -1
- package/src/auth/context/AccountsContext.tsx +6 -8
- package/src/auth/context/AuthContext.tsx +3 -5
- package/src/auth/context/types.ts +1 -1
- package/src/auth/hooks/useAuthForm.ts +3 -5
- package/src/auth/hooks/useAuthFormState.ts +0 -4
- package/src/auth/hooks/useAuthRedirect.ts +3 -2
- package/src/auth/hooks/useGithubAuth.ts +14 -16
- package/src/auth/hooks/useLocalStorage.ts +7 -7
- package/src/auth/hooks/useProfileCache.ts +2 -1
- package/src/auth/hooks/useSessionStorage.ts +7 -7
- package/src/auth/hooks/useTwoFactorVerify.ts +2 -2
- package/src/auth/types/form.ts +0 -5
- package/src/storage.ts +21 -0
|
@@ -321,10 +321,6 @@ export type OAuthCallbackRequestRequest = {
|
|
|
321
321
|
* Same redirect_uri used in authorize request. If not provided, uses config's site_url + callback_path
|
|
322
322
|
*/
|
|
323
323
|
redirect_uri?: string;
|
|
324
|
-
/**
|
|
325
|
-
* Keep this browser signed in for 30 days
|
|
326
|
-
*/
|
|
327
|
-
remember_me?: boolean;
|
|
328
324
|
};
|
|
329
325
|
|
|
330
326
|
/**
|
|
@@ -454,10 +450,6 @@ export type OAuthTokenResponse = {
|
|
|
454
450
|
* True if user should be prompted to enable 2FA
|
|
455
451
|
*/
|
|
456
452
|
should_prompt_2fa?: boolean;
|
|
457
|
-
/**
|
|
458
|
-
* Whether this login should persist across browser restarts
|
|
459
|
-
*/
|
|
460
|
-
persistent_session?: boolean;
|
|
461
453
|
};
|
|
462
454
|
|
|
463
455
|
/**
|
|
@@ -545,10 +537,6 @@ export type OtpVerifyRequest = {
|
|
|
545
537
|
* Source URL for tracking login (e.g., https://my.djangocfg.com)
|
|
546
538
|
*/
|
|
547
539
|
source_url?: string;
|
|
548
|
-
/**
|
|
549
|
-
* Keep this browser signed in for 30 days
|
|
550
|
-
*/
|
|
551
|
-
remember_me?: boolean;
|
|
552
540
|
};
|
|
553
541
|
|
|
554
542
|
/**
|
|
@@ -589,10 +577,6 @@ export type OtpVerifyResponse = {
|
|
|
589
577
|
* Whether user should be prompted to enable 2FA
|
|
590
578
|
*/
|
|
591
579
|
should_prompt_2fa?: boolean;
|
|
592
|
-
/**
|
|
593
|
-
* Whether this login should persist across browser restarts
|
|
594
|
-
*/
|
|
595
|
-
persistent_session?: boolean;
|
|
596
580
|
};
|
|
597
581
|
|
|
598
582
|
/**
|
|
@@ -623,14 +607,12 @@ export type PasswordLoginError = {
|
|
|
623
607
|
*
|
|
624
608
|
* Subclasses SimpleJWT so the field/validation contract stays theirs, but
|
|
625
609
|
* replaces ``validate()`` — the token pair itself is minted by the view via
|
|
626
|
-
* ``mint_tokens_for_request`` so the DPoP binding and
|
|
610
|
+
* ``mint_tokens_for_request`` so the DPoP binding and token
|
|
627
611
|
* lifetime match OTP, OAuth and TOTP exactly.
|
|
628
612
|
*/
|
|
629
613
|
export type PasswordLoginRequest = {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
*/
|
|
633
|
-
remember_me?: boolean;
|
|
614
|
+
email: string;
|
|
615
|
+
password: string;
|
|
634
616
|
};
|
|
635
617
|
|
|
636
618
|
/**
|
|
@@ -664,10 +646,6 @@ export type PasswordLoginResponse = {
|
|
|
664
646
|
* Whether user should be prompted to enable 2FA
|
|
665
647
|
*/
|
|
666
648
|
should_prompt_2fa?: boolean;
|
|
667
|
-
/**
|
|
668
|
-
* Whether this login should persist across browser restarts
|
|
669
|
-
*/
|
|
670
|
-
persistent_session?: boolean;
|
|
671
649
|
};
|
|
672
650
|
|
|
673
651
|
/**
|
|
@@ -868,10 +846,6 @@ export type VerifyResponse = {
|
|
|
868
846
|
* User profile data
|
|
869
847
|
*/
|
|
870
848
|
user: TotpVerifyUser;
|
|
871
|
-
/**
|
|
872
|
-
* Whether this login should persist across browser restarts
|
|
873
|
-
*/
|
|
874
|
-
persistent_session: boolean;
|
|
875
849
|
/**
|
|
876
850
|
* Number of remaining backup codes (if backup code was used)
|
|
877
851
|
*/
|
|
@@ -1064,27 +1038,6 @@ export type OtpVerifyResponseWritable = {
|
|
|
1064
1038
|
* Whether user should be prompted to enable 2FA
|
|
1065
1039
|
*/
|
|
1066
1040
|
should_prompt_2fa?: boolean;
|
|
1067
|
-
/**
|
|
1068
|
-
* Whether this login should persist across browser restarts
|
|
1069
|
-
*/
|
|
1070
|
-
persistent_session?: boolean;
|
|
1071
|
-
};
|
|
1072
|
-
|
|
1073
|
-
/**
|
|
1074
|
-
* Validate email+password against live accounts only.
|
|
1075
|
-
*
|
|
1076
|
-
* Subclasses SimpleJWT so the field/validation contract stays theirs, but
|
|
1077
|
-
* replaces ``validate()`` — the token pair itself is minted by the view via
|
|
1078
|
-
* ``mint_tokens_for_request`` so the DPoP binding and remembered-session
|
|
1079
|
-
* lifetime match OTP, OAuth and TOTP exactly.
|
|
1080
|
-
*/
|
|
1081
|
-
export type PasswordLoginRequestWritable = {
|
|
1082
|
-
/**
|
|
1083
|
-
* Keep this browser signed in for 30 days
|
|
1084
|
-
*/
|
|
1085
|
-
remember_me?: boolean;
|
|
1086
|
-
email: string;
|
|
1087
|
-
password: string;
|
|
1088
1041
|
};
|
|
1089
1042
|
|
|
1090
1043
|
/**
|
|
@@ -1118,10 +1071,6 @@ export type PasswordLoginResponseWritable = {
|
|
|
1118
1071
|
* Whether user should be prompted to enable 2FA
|
|
1119
1072
|
*/
|
|
1120
1073
|
should_prompt_2fa?: boolean;
|
|
1121
|
-
/**
|
|
1122
|
-
* Whether this login should persist across browser restarts
|
|
1123
|
-
*/
|
|
1124
|
-
persistent_session?: boolean;
|
|
1125
1074
|
};
|
|
1126
1075
|
|
|
1127
1076
|
export type TokenRefreshWritable = {
|
|
@@ -1171,10 +1120,6 @@ export type VerifyResponseWritable = {
|
|
|
1171
1120
|
* User profile data
|
|
1172
1121
|
*/
|
|
1173
1122
|
user: TotpVerifyUserWritable;
|
|
1174
|
-
/**
|
|
1175
|
-
* Whether this login should persist across browser restarts
|
|
1176
|
-
*/
|
|
1177
|
-
persistent_session: boolean;
|
|
1178
1123
|
/**
|
|
1179
1124
|
* Number of remaining backup codes (if backup code was used)
|
|
1180
1125
|
*/
|
|
@@ -1566,7 +1511,7 @@ export type CfgAccountsProfileUpdateUpdateResponses = {
|
|
|
1566
1511
|
export type CfgAccountsProfileUpdateUpdateResponse = CfgAccountsProfileUpdateUpdateResponses[keyof CfgAccountsProfileUpdateUpdateResponses];
|
|
1567
1512
|
|
|
1568
1513
|
export type CfgAccountsTokenCreateData = {
|
|
1569
|
-
body:
|
|
1514
|
+
body: PasswordLoginRequest;
|
|
1570
1515
|
path?: never;
|
|
1571
1516
|
query?: never;
|
|
1572
1517
|
url: '/cfg/accounts/token/';
|
|
@@ -136,21 +136,19 @@ describe('AuthContext.login (password grant)', () => {
|
|
|
136
136
|
expect(tokens).toEqual({ access: 'access-1', refresh: 'refresh-1' });
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
it('sends email
|
|
139
|
+
it('sends email and password to the password endpoint', async () => {
|
|
140
140
|
respondWith({ requires_2fa: false, access: 'access-1', refresh: 'refresh-1', user: USER });
|
|
141
141
|
const { result } = await renderAuth();
|
|
142
142
|
|
|
143
143
|
await act(async () => {
|
|
144
144
|
await result.current.login('jane@example.com', 'correct horse', {
|
|
145
145
|
skipRedirect: true,
|
|
146
|
-
rememberMe: true,
|
|
147
146
|
});
|
|
148
147
|
});
|
|
149
148
|
|
|
150
149
|
expect(loginWithPassword).toHaveBeenCalledWith({
|
|
151
150
|
email: 'jane@example.com',
|
|
152
151
|
password: 'correct horse',
|
|
153
|
-
remember_me: true,
|
|
154
152
|
});
|
|
155
153
|
});
|
|
156
154
|
|
|
@@ -57,7 +57,7 @@ import type {
|
|
|
57
57
|
OtpVerifyRequest,
|
|
58
58
|
OtpRequestResponse,
|
|
59
59
|
OtpVerifyResponse,
|
|
60
|
-
|
|
60
|
+
PasswordLoginRequest,
|
|
61
61
|
PasswordLoginResponse,
|
|
62
62
|
} from '../../_api/generated/_cfg_accounts/types.gen';
|
|
63
63
|
|
|
@@ -79,7 +79,7 @@ export interface AccountsContextValue {
|
|
|
79
79
|
|
|
80
80
|
requestOTP: (data: OtpRequestRequest) => Promise<OtpRequestResponse>;
|
|
81
81
|
verifyOTP: (data: OtpVerifyRequest) => Promise<OtpVerifyResponse>;
|
|
82
|
-
loginWithPassword: (data:
|
|
82
|
+
loginWithPassword: (data: PasswordLoginRequest) => Promise<PasswordLoginResponse>;
|
|
83
83
|
logout: () => void;
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -207,11 +207,9 @@ export function AccountsProvider({ children }: AccountsProviderProps) {
|
|
|
207
207
|
|
|
208
208
|
if (result.access && result.refresh) {
|
|
209
209
|
// The ONE write path — atomic pair, notifies every session subscriber.
|
|
210
|
-
// Storage is always localStorage:
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
// to strand the tokens — the module reinitializes to localStorage on the
|
|
214
|
-
// next page load and would never look at sessionStorage again.
|
|
210
|
+
// Storage is always localStorage: session length is the token lifetime
|
|
211
|
+
// the server chose. A sessionStorage switch would strand the tokens — the
|
|
212
|
+
// module reinitializes to localStorage on the next page load.
|
|
215
213
|
auth.setSession({ access: result.access, refresh: result.refresh });
|
|
216
214
|
try {
|
|
217
215
|
await refreshProfile({ callerId, force: true });
|
|
@@ -228,7 +226,7 @@ export function AccountsProvider({ children }: AccountsProviderProps) {
|
|
|
228
226
|
}, [triggerOtpVerify, settleLoginEnvelope]);
|
|
229
227
|
|
|
230
228
|
const loginWithPassword = useCallback(async (
|
|
231
|
-
data:
|
|
229
|
+
data: PasswordLoginRequest,
|
|
232
230
|
): Promise<PasswordLoginResponse> => {
|
|
233
231
|
// Errors propagate: the SWR trigger throws APIError for 401/429 and the
|
|
234
232
|
// caller maps them. Nothing is written to the session on a failure.
|
|
@@ -30,6 +30,7 @@ import React, {
|
|
|
30
30
|
|
|
31
31
|
import { auth } from '../../_api/generated/helpers/auth';
|
|
32
32
|
import { APIError } from '../../_api/generated/helpers';
|
|
33
|
+
import { CFG_KEYS } from '../../_api/generated/helpers/keys';
|
|
33
34
|
import { applyRoleLogPolicy } from '../../log-control';
|
|
34
35
|
import { useCfgRouter, useLocalStorage, useQueryParams, useSession } from '../hooks';
|
|
35
36
|
import { clearProfileCache } from '../hooks/useProfileCache';
|
|
@@ -68,7 +69,7 @@ export const resolveAuthRoutes = (
|
|
|
68
69
|
defaultAuthCallback: routes?.defaultAuthCallback || routes?.auth || defaultRoutes.defaultAuthCallback,
|
|
69
70
|
});
|
|
70
71
|
|
|
71
|
-
const EMAIL_STORAGE_KEY =
|
|
72
|
+
const EMAIL_STORAGE_KEY = CFG_KEYS.email.key;
|
|
72
73
|
|
|
73
74
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
74
75
|
|
|
@@ -287,7 +288,6 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
287
288
|
sourceUrl?: string,
|
|
288
289
|
redirectUrl?: string,
|
|
289
290
|
skipRedirect?: boolean,
|
|
290
|
-
rememberMe = false,
|
|
291
291
|
): Promise<{
|
|
292
292
|
success: boolean;
|
|
293
293
|
message: string;
|
|
@@ -305,7 +305,6 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
305
305
|
identifier,
|
|
306
306
|
otp: otpCode,
|
|
307
307
|
source_url: sourceUrl,
|
|
308
|
-
remember_me: rememberMe,
|
|
309
308
|
});
|
|
310
309
|
|
|
311
310
|
if (result.requires_2fa && result.session_id) {
|
|
@@ -378,7 +377,7 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
378
377
|
password: string,
|
|
379
378
|
options?: PasswordLoginOptions,
|
|
380
379
|
): Promise<PasswordLoginResult> => {
|
|
381
|
-
const { redirectUrl, skipRedirect
|
|
380
|
+
const { redirectUrl, skipRedirect } = options || {};
|
|
382
381
|
|
|
383
382
|
try {
|
|
384
383
|
// The caller opting out of redirect commits to handling ALL navigation
|
|
@@ -388,7 +387,6 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
388
387
|
const result = await accountsRef.current.loginWithPassword({
|
|
389
388
|
email,
|
|
390
389
|
password,
|
|
391
|
-
remember_me: rememberMe,
|
|
392
390
|
});
|
|
393
391
|
|
|
394
392
|
if (result.requires_2fa && result.session_id) {
|
|
@@ -55,7 +55,7 @@ export interface AuthContextType {
|
|
|
55
55
|
|
|
56
56
|
// OTP Methods - email only
|
|
57
57
|
requestOTP: (identifier: string, sourceUrl?: string, consent?: OTPRequestConsent) => Promise<OTPRequestResult>;
|
|
58
|
-
verifyOTP: (identifier: string, otpCode: string, sourceUrl?: string, redirectUrl?: string, skipRedirect?: boolean
|
|
58
|
+
verifyOTP: (identifier: string, otpCode: string, sourceUrl?: string, redirectUrl?: string, skipRedirect?: boolean) => Promise<{
|
|
59
59
|
success: boolean;
|
|
60
60
|
message: string;
|
|
61
61
|
user?: UserProfile;
|
|
@@ -55,7 +55,6 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
|
|
|
55
55
|
password,
|
|
56
56
|
acceptedTerms,
|
|
57
57
|
marketingConsent,
|
|
58
|
-
rememberMe,
|
|
59
58
|
setIdentifier,
|
|
60
59
|
setOtp,
|
|
61
60
|
setStep,
|
|
@@ -176,7 +175,7 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
|
|
|
176
175
|
|
|
177
176
|
try {
|
|
178
177
|
// Skip redirect - we'll show success screen first
|
|
179
|
-
const result = await verifyOTP(submitIdentifier, submitOtp, sourceUrl, redirectUrl, true
|
|
178
|
+
const result = await verifyOTP(submitIdentifier, submitOtp, sourceUrl, redirectUrl, true);
|
|
180
179
|
|
|
181
180
|
if (result.requires_2fa && result.session_id) {
|
|
182
181
|
authLogger.info('2FA required after OTP verification');
|
|
@@ -205,7 +204,7 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
|
|
|
205
204
|
} finally {
|
|
206
205
|
setIsLoading(false);
|
|
207
206
|
}
|
|
208
|
-
}, [verifyOTP, saveIdentifierToStorage, setError, setIsLoading, clearError, onOTPSuccess, onError, sourceUrl, redirectUrl,
|
|
207
|
+
}, [verifyOTP, saveIdentifierToStorage, setError, setIsLoading, clearError, onOTPSuccess, onError, sourceUrl, redirectUrl, setStep]);
|
|
209
208
|
|
|
210
209
|
const handleOTPSubmit = useCallback(async (e: React.FormEvent) => {
|
|
211
210
|
e.preventDefault();
|
|
@@ -244,7 +243,6 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
|
|
|
244
243
|
const result = await login(submitIdentifier, submitPassword_, {
|
|
245
244
|
redirectUrl,
|
|
246
245
|
skipRedirect: true,
|
|
247
|
-
rememberMe,
|
|
248
246
|
});
|
|
249
247
|
|
|
250
248
|
if (result.requires_2fa && result.session_id) {
|
|
@@ -283,7 +281,7 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
|
|
|
283
281
|
}, [
|
|
284
282
|
login, validateIdentifier, saveIdentifierToStorage, setError, setIsLoading,
|
|
285
283
|
clearError, startRateLimitCountdown, onOTPSuccess, onError, redirectUrl,
|
|
286
|
-
|
|
284
|
+
setStep,
|
|
287
285
|
]);
|
|
288
286
|
|
|
289
287
|
const handlePasswordSubmit = useCallback(async (e: React.FormEvent) => {
|
|
@@ -53,8 +53,6 @@ export const useAuthFormState = (
|
|
|
53
53
|
// null until the marketing-consent feature (if enabled) resolves its
|
|
54
54
|
// jurisdiction-aware default — the layout owns that decision.
|
|
55
55
|
const [marketingConsent, setMarketingConsent] = useState<boolean | null>(null);
|
|
56
|
-
// Signed in for 30 days unless an app opts into showing the choice.
|
|
57
|
-
const [rememberMe, setRememberMe] = useState(true);
|
|
58
56
|
const [step, setStep] = useState<AuthStep>('identifier');
|
|
59
57
|
const [error, setError] = useState('');
|
|
60
58
|
const [webmail, setWebmail] = useState<WebmailLink | null>(null);
|
|
@@ -72,7 +70,6 @@ export const useAuthFormState = (
|
|
|
72
70
|
isLoading,
|
|
73
71
|
acceptedTerms,
|
|
74
72
|
marketingConsent,
|
|
75
|
-
rememberMe,
|
|
76
73
|
step,
|
|
77
74
|
error,
|
|
78
75
|
rateLimitSeconds: rateLimit.seconds,
|
|
@@ -87,7 +84,6 @@ export const useAuthFormState = (
|
|
|
87
84
|
setPassword,
|
|
88
85
|
setAcceptedTerms,
|
|
89
86
|
setMarketingConsent,
|
|
90
|
-
setRememberMe,
|
|
91
87
|
setError,
|
|
92
88
|
clearError,
|
|
93
89
|
setStep,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { CFG_KEYS } from '../../_api/generated/helpers/keys';
|
|
1
2
|
import { useSessionStorage } from './useSessionStorage';
|
|
2
3
|
|
|
3
|
-
const AUTH_REDIRECT_KEY =
|
|
4
|
+
const AUTH_REDIRECT_KEY = CFG_KEYS.redirectUrl.key;
|
|
4
5
|
|
|
5
6
|
export interface AuthRedirectOptions {
|
|
6
7
|
fallbackUrl?: string;
|
|
@@ -28,7 +29,7 @@ const clearRedirectFromStorage = (): void => {
|
|
|
28
29
|
if (typeof window === 'undefined') return;
|
|
29
30
|
try {
|
|
30
31
|
window.sessionStorage.removeItem(AUTH_REDIRECT_KEY);
|
|
31
|
-
window.sessionStorage.removeItem(`${AUTH_REDIRECT_KEY}
|
|
32
|
+
window.sessionStorage.removeItem(`${AUTH_REDIRECT_KEY}_ts`);
|
|
32
33
|
} catch {
|
|
33
34
|
// Ignore errors
|
|
34
35
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useCallback, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
import { auth } from '../../_api/generated/helpers/auth';
|
|
6
|
+
import { CFG_KEYS } from '../../_api/generated/helpers/keys';
|
|
6
7
|
import { CfgAccountsOauth } from '../../_api/generated/sdk.gen';
|
|
7
8
|
import type { OAuthTokenResponse } from '../../_api/generated/types.gen';
|
|
8
9
|
import { Analytics, AnalyticsCategory, AnalyticsEvent } from '../utils/analytics';
|
|
@@ -10,6 +11,9 @@ import { formatAuthError } from '../utils/errors';
|
|
|
10
11
|
import { authLogger } from '../utils/logger';
|
|
11
12
|
import { useCfgRouter } from './useCfgRouter';
|
|
12
13
|
|
|
14
|
+
const OAUTH_STATE_KEY = CFG_KEYS.oauthState.key;
|
|
15
|
+
const OAUTH_PROVIDER_KEY = CFG_KEYS.oauthProvider.key;
|
|
16
|
+
|
|
13
17
|
/**
|
|
14
18
|
* The user payload as the OAuth endpoint declares it.
|
|
15
19
|
*
|
|
@@ -22,8 +26,6 @@ export type OAuthUser = NonNullable<OAuthTokenResponse['user']>;
|
|
|
22
26
|
|
|
23
27
|
export interface UseGithubAuthOptions {
|
|
24
28
|
sourceUrl?: string;
|
|
25
|
-
/** Keep the completed browser session for 30 days. */
|
|
26
|
-
rememberMe?: boolean;
|
|
27
29
|
/**
|
|
28
30
|
* The OAuth endpoint's OpenAPI schema types `user` as a free-form object, so
|
|
29
31
|
* this is what the server actually promises — not a `User`. Narrow it at the
|
|
@@ -65,7 +67,7 @@ export interface UseGithubAuthReturn {
|
|
|
65
67
|
* ```
|
|
66
68
|
*/
|
|
67
69
|
export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuthReturn => {
|
|
68
|
-
const { sourceUrl,
|
|
70
|
+
const { sourceUrl, onSuccess, onError, onRequires2FA, redirectUrl, skipRedirect = false } = options;
|
|
69
71
|
const router = useCfgRouter();
|
|
70
72
|
|
|
71
73
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -107,9 +109,8 @@ export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuth
|
|
|
107
109
|
authLogger.info('Redirecting to GitHub...', response.authorization_url);
|
|
108
110
|
|
|
109
111
|
if (typeof window !== 'undefined') {
|
|
110
|
-
sessionStorage.setItem(
|
|
111
|
-
sessionStorage.setItem(
|
|
112
|
-
sessionStorage.setItem('cfg.oauth.remember_me', String(rememberMe));
|
|
112
|
+
sessionStorage.setItem(OAUTH_STATE_KEY, response.state);
|
|
113
|
+
sessionStorage.setItem(OAUTH_PROVIDER_KEY, 'github');
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
window.location.href = response.authorization_url;
|
|
@@ -128,7 +129,7 @@ export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuth
|
|
|
128
129
|
} finally {
|
|
129
130
|
setIsLoading(false);
|
|
130
131
|
}
|
|
131
|
-
}, [sourceUrl,
|
|
132
|
+
}, [sourceUrl, onError]);
|
|
132
133
|
|
|
133
134
|
/**
|
|
134
135
|
* Handle GitHub OAuth callback - exchanges code for JWT tokens.
|
|
@@ -144,22 +145,19 @@ export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuth
|
|
|
144
145
|
authLogger.info('Processing GitHub OAuth callback...');
|
|
145
146
|
|
|
146
147
|
// Verify state matches what we stored
|
|
147
|
-
let rememberMeForSession = false;
|
|
148
148
|
if (typeof window !== 'undefined') {
|
|
149
|
-
const storedState = sessionStorage.getItem(
|
|
149
|
+
const storedState = sessionStorage.getItem(OAUTH_STATE_KEY);
|
|
150
150
|
if (storedState && storedState !== state) {
|
|
151
151
|
throw new Error('Invalid OAuth state - possible CSRF attack');
|
|
152
152
|
}
|
|
153
|
-
rememberMeForSession = sessionStorage.getItem('cfg.oauth.remember_me') === 'true';
|
|
154
153
|
// Clear stored state
|
|
155
|
-
sessionStorage.removeItem(
|
|
156
|
-
sessionStorage.removeItem(
|
|
157
|
-
sessionStorage.removeItem('cfg.oauth.remember_me');
|
|
154
|
+
sessionStorage.removeItem(OAUTH_STATE_KEY);
|
|
155
|
+
sessionStorage.removeItem(OAUTH_PROVIDER_KEY);
|
|
158
156
|
|
|
159
157
|
}
|
|
160
158
|
|
|
161
159
|
const result = await CfgAccountsOauth.cfgAccountsOauthGithubCallbackCreate({
|
|
162
|
-
body: { code, state
|
|
160
|
+
body: { code, state },
|
|
163
161
|
throwOnError: true,
|
|
164
162
|
});
|
|
165
163
|
const response = result.data;
|
|
@@ -186,8 +184,8 @@ export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuth
|
|
|
186
184
|
authLogger.info('GitHub OAuth successful, user:', response.user);
|
|
187
185
|
|
|
188
186
|
// The ONE write path — atomic pair + session notification. Storage is
|
|
189
|
-
// always localStorage; session length is the server's call
|
|
190
|
-
//
|
|
187
|
+
// always localStorage; session length is the server's call.
|
|
188
|
+
// See AccountsContext.verifyOTP.
|
|
191
189
|
auth.setSession({ access: response.access, refresh: response.refresh });
|
|
192
190
|
|
|
193
191
|
// Track successful OAuth
|
|
@@ -49,7 +49,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
49
49
|
const key = keys[i];
|
|
50
50
|
if (key) {
|
|
51
51
|
localStorage.removeItem(key);
|
|
52
|
-
localStorage.removeItem(`${key}
|
|
52
|
+
localStorage.removeItem(`${key}_ts`);
|
|
53
53
|
}
|
|
54
54
|
} catch {
|
|
55
55
|
// Ignore errors when removing items
|
|
@@ -88,7 +88,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
88
88
|
// Remove the key if data is too large
|
|
89
89
|
try {
|
|
90
90
|
window.localStorage.removeItem(key);
|
|
91
|
-
window.localStorage.removeItem(`${key}
|
|
91
|
+
window.localStorage.removeItem(`${key}_ts`);
|
|
92
92
|
} catch {
|
|
93
93
|
// Ignore errors when removing
|
|
94
94
|
}
|
|
@@ -109,7 +109,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
109
109
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
110
110
|
}
|
|
111
111
|
// Add timestamp for cleanup
|
|
112
|
-
window.localStorage.setItem(`${key}
|
|
112
|
+
window.localStorage.setItem(`${key}_ts`, Date.now().toString());
|
|
113
113
|
} catch (storageError: unknown) {
|
|
114
114
|
// If quota exceeded, clear old data and try again
|
|
115
115
|
if (isQuotaExceededError(storageError)) {
|
|
@@ -124,7 +124,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
124
124
|
} else {
|
|
125
125
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
126
126
|
}
|
|
127
|
-
window.localStorage.setItem(`${key}
|
|
127
|
+
window.localStorage.setItem(`${key}_ts`, Date.now().toString());
|
|
128
128
|
} catch (retryError) {
|
|
129
129
|
authLogger.error(`Failed to set localStorage key "${key}" after clearing old data:`, retryError);
|
|
130
130
|
// If still fails, force clear all and try one more time
|
|
@@ -136,7 +136,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
136
136
|
} else {
|
|
137
137
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
138
138
|
}
|
|
139
|
-
window.localStorage.setItem(`${key}
|
|
139
|
+
window.localStorage.setItem(`${key}_ts`, Date.now().toString());
|
|
140
140
|
} catch (finalError) {
|
|
141
141
|
authLogger.error(`Failed to set localStorage key "${key}" after force clearing:`, finalError);
|
|
142
142
|
// If still fails, just update the state without localStorage
|
|
@@ -163,7 +163,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
163
163
|
if (typeof window !== 'undefined') {
|
|
164
164
|
try {
|
|
165
165
|
window.localStorage.removeItem(key);
|
|
166
|
-
window.localStorage.removeItem(`${key}
|
|
166
|
+
window.localStorage.removeItem(`${key}_ts`);
|
|
167
167
|
} catch (removeError: unknown) {
|
|
168
168
|
// If removal fails due to quota, try to clear some data first
|
|
169
169
|
if (isQuotaExceededError(removeError)) {
|
|
@@ -172,7 +172,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
172
172
|
|
|
173
173
|
try {
|
|
174
174
|
window.localStorage.removeItem(key);
|
|
175
|
-
window.localStorage.removeItem(`${key}
|
|
175
|
+
window.localStorage.removeItem(`${key}_ts`);
|
|
176
176
|
} catch (retryError) {
|
|
177
177
|
authLogger.error(`Failed to remove localStorage key "${key}" after clearing:`, retryError);
|
|
178
178
|
// If still fails, force clear all
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
import type { User } from '../../';
|
|
13
13
|
import { authLogger as profileLogger } from '../utils/logger';
|
|
14
|
+
import { CFG_KEYS } from '../../_api/generated/helpers/keys';
|
|
14
15
|
import { decodeBase64, encodeBase64 } from './useBase64';
|
|
15
16
|
|
|
16
17
|
// Cache configuration
|
|
17
|
-
const CACHE_KEY =
|
|
18
|
+
const CACHE_KEY = CFG_KEYS.profileCache.key;
|
|
18
19
|
const CACHE_VERSION = 1;
|
|
19
20
|
const DEFAULT_TTL = 14400000; // 4 hours in milliseconds (reduced API calls, more resilient)
|
|
20
21
|
|
|
@@ -39,7 +39,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
39
39
|
const key = keys[i];
|
|
40
40
|
if (key) {
|
|
41
41
|
sessionStorage.removeItem(key);
|
|
42
|
-
sessionStorage.removeItem(`${key}
|
|
42
|
+
sessionStorage.removeItem(`${key}_ts`);
|
|
43
43
|
}
|
|
44
44
|
} catch {
|
|
45
45
|
// Ignore errors when removing items
|
|
@@ -78,7 +78,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
78
78
|
// Remove the key if data is too large
|
|
79
79
|
try {
|
|
80
80
|
window.sessionStorage.removeItem(key);
|
|
81
|
-
window.sessionStorage.removeItem(`${key}
|
|
81
|
+
window.sessionStorage.removeItem(`${key}_ts`);
|
|
82
82
|
} catch {
|
|
83
83
|
// Ignore errors when removing
|
|
84
84
|
}
|
|
@@ -94,7 +94,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
94
94
|
try {
|
|
95
95
|
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
96
96
|
// Add timestamp for cleanup
|
|
97
|
-
window.sessionStorage.setItem(`${key}
|
|
97
|
+
window.sessionStorage.setItem(`${key}_ts`, Date.now().toString());
|
|
98
98
|
} catch (storageError: unknown) {
|
|
99
99
|
// If quota exceeded, clear old data and try again
|
|
100
100
|
if (isQuotaExceededError(storageError)) {
|
|
@@ -104,14 +104,14 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
104
104
|
// Try again after clearing
|
|
105
105
|
try {
|
|
106
106
|
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
107
|
-
window.sessionStorage.setItem(`${key}
|
|
107
|
+
window.sessionStorage.setItem(`${key}_ts`, Date.now().toString());
|
|
108
108
|
} catch (retryError) {
|
|
109
109
|
authLogger.error(`Failed to set sessionStorage key "${key}" after clearing old data:`, retryError);
|
|
110
110
|
// If still fails, force clear all and try one more time
|
|
111
111
|
try {
|
|
112
112
|
forceClearAll();
|
|
113
113
|
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
114
|
-
window.sessionStorage.setItem(`${key}
|
|
114
|
+
window.sessionStorage.setItem(`${key}_ts`, Date.now().toString());
|
|
115
115
|
} catch (finalError) {
|
|
116
116
|
authLogger.error(`Failed to set sessionStorage key "${key}" after force clearing:`, finalError);
|
|
117
117
|
// If still fails, just update the state without sessionStorage
|
|
@@ -138,7 +138,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
138
138
|
if (typeof window !== 'undefined') {
|
|
139
139
|
try {
|
|
140
140
|
window.sessionStorage.removeItem(key);
|
|
141
|
-
window.sessionStorage.removeItem(`${key}
|
|
141
|
+
window.sessionStorage.removeItem(`${key}_ts`);
|
|
142
142
|
} catch (removeError: unknown) {
|
|
143
143
|
// If removal fails due to quota, try to clear some data first
|
|
144
144
|
if (isQuotaExceededError(removeError)) {
|
|
@@ -147,7 +147,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
147
147
|
|
|
148
148
|
try {
|
|
149
149
|
window.sessionStorage.removeItem(key);
|
|
150
|
-
window.sessionStorage.removeItem(`${key}
|
|
150
|
+
window.sessionStorage.removeItem(`${key}_ts`);
|
|
151
151
|
} catch (retryError) {
|
|
152
152
|
authLogger.error(`Failed to remove sessionStorage key "${key}" after clearing:`, retryError);
|
|
153
153
|
// If still fails, force clear all
|
|
@@ -69,8 +69,8 @@ export const useTwoFactorVerify = (
|
|
|
69
69
|
// copy it replaces cannot drift from the server contract any more.
|
|
70
70
|
const handleSuccess = useCallback((response: VerifyResponse) => {
|
|
71
71
|
// Save tokens — the ONE write path (atomic pair + session notification).
|
|
72
|
-
// Storage is always localStorage; session length is the server's call
|
|
73
|
-
//
|
|
72
|
+
// Storage is always localStorage; session length is the server's call.
|
|
73
|
+
// See AccountsContext.verifyOTP.
|
|
74
74
|
auth.setSession({ access: response.access_token, refresh: response.refresh_token });
|
|
75
75
|
|
|
76
76
|
if (response.warning) {
|
package/src/auth/types/form.ts
CHANGED
|
@@ -52,8 +52,6 @@ export interface PasswordLoginOptions {
|
|
|
52
52
|
redirectUrl?: string;
|
|
53
53
|
/** Caller owns ALL navigation for this login, including the 2FA continuation. */
|
|
54
54
|
skipRedirect?: boolean;
|
|
55
|
-
/** Ask the server for a 30-day session instead of the default lifetime. */
|
|
56
|
-
rememberMe?: boolean;
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
/**
|
|
@@ -99,8 +97,6 @@ export interface AuthFormState {
|
|
|
99
97
|
* jurisdiction-aware default on mount.
|
|
100
98
|
*/
|
|
101
99
|
marketingConsent: boolean | null;
|
|
102
|
-
/** Keep this browser signed in for 30 days after successful verification. */
|
|
103
|
-
rememberMe: boolean;
|
|
104
100
|
/** Current form step */
|
|
105
101
|
step: AuthStep;
|
|
106
102
|
/** Error message */
|
|
@@ -129,7 +125,6 @@ export interface AuthFormStateHandlers {
|
|
|
129
125
|
setPassword: (password: string) => void;
|
|
130
126
|
setAcceptedTerms: (accepted: boolean) => void;
|
|
131
127
|
setMarketingConsent: (consent: boolean | null) => void;
|
|
132
|
-
setRememberMe: (rememberMe: boolean) => void;
|
|
133
128
|
setError: (error: string) => void;
|
|
134
129
|
clearError: () => void;
|
|
135
130
|
setStep: (step: AuthStep) => void;
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@djangocfg/api/storage` — the browser-storage key registry on its own.
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies and no client: packages that only need a key, a guarded
|
|
5
|
+
* read/write or `createStorage('<owner>')` import this, not the generated SDK.
|
|
6
|
+
* The source is the generated `helpers/keys.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export {
|
|
9
|
+
CFG_FAMILIES,
|
|
10
|
+
CFG_KEYS,
|
|
11
|
+
cfgStorage,
|
|
12
|
+
createStorage,
|
|
13
|
+
normalizeSegment,
|
|
14
|
+
} from './_api/generated/helpers/keys';
|
|
15
|
+
export type {
|
|
16
|
+
FamilySpec,
|
|
17
|
+
GuardedStorage,
|
|
18
|
+
KeySpec,
|
|
19
|
+
KeyStore,
|
|
20
|
+
StorageOptions,
|
|
21
|
+
} from './_api/generated/helpers/keys';
|