@djangocfg/api 2.1.449 → 2.1.450
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 +80 -20
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +2 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.mjs +80 -20
- package/dist/auth.mjs.map +1 -1
- package/package.json +2 -2
- package/src/auth/__tests__/jwt.test.ts +119 -0
- package/src/auth/__tests__/onUnauthorized.test.ts +206 -0
- package/src/auth/__tests__/sessionBootstrap.test.ts +76 -0
- package/src/auth/context/AuthContext.tsx +121 -13
- package/src/auth/hooks/useTokenRefresh.ts +3 -21
- package/src/auth/utils/jwt.ts +66 -0
package/dist/auth.d.cts
CHANGED
|
@@ -951,6 +951,8 @@ interface UseTokenRefreshOptions {
|
|
|
951
951
|
}
|
|
952
952
|
/**
|
|
953
953
|
* Hook for proactive automatic token refresh.
|
|
954
|
+
* (JWT expiry helpers live in ../utils/jwt so they can be unit-tested and
|
|
955
|
+
* reused by the auth-bootstrap validation path.)
|
|
954
956
|
*/
|
|
955
957
|
declare function useTokenRefresh(options?: UseTokenRefreshOptions): {
|
|
956
958
|
refreshToken: () => Promise<boolean>;
|
package/dist/auth.d.ts
CHANGED
|
@@ -951,6 +951,8 @@ interface UseTokenRefreshOptions {
|
|
|
951
951
|
}
|
|
952
952
|
/**
|
|
953
953
|
* Hook for proactive automatic token refresh.
|
|
954
|
+
* (JWT expiry helpers live in ../utils/jwt so they can be unit-tested and
|
|
955
|
+
* reused by the auth-bootstrap validation path.)
|
|
954
956
|
*/
|
|
955
957
|
declare function useTokenRefresh(options?: UseTokenRefreshOptions): {
|
|
956
958
|
refreshToken: () => Promise<boolean>;
|
package/dist/auth.mjs
CHANGED
|
@@ -3802,24 +3802,45 @@ function triggerRefresh() {
|
|
|
3802
3802
|
}
|
|
3803
3803
|
__name(triggerRefresh, "triggerRefresh");
|
|
3804
3804
|
|
|
3805
|
-
// src/auth/
|
|
3806
|
-
|
|
3807
|
-
var CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
3808
|
-
function getTokenExpiry(token) {
|
|
3805
|
+
// src/auth/utils/jwt.ts
|
|
3806
|
+
function decodeSegment(segment) {
|
|
3809
3807
|
try {
|
|
3810
|
-
const
|
|
3811
|
-
|
|
3808
|
+
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
3809
|
+
if (typeof atob !== "function") return null;
|
|
3810
|
+
const json = atob(base64);
|
|
3811
|
+
return JSON.parse(json);
|
|
3812
3812
|
} catch {
|
|
3813
3813
|
return null;
|
|
3814
3814
|
}
|
|
3815
3815
|
}
|
|
3816
|
+
__name(decodeSegment, "decodeSegment");
|
|
3817
|
+
function getTokenExpiry(token) {
|
|
3818
|
+
if (!token || typeof token !== "string") return null;
|
|
3819
|
+
const parts = token.split(".");
|
|
3820
|
+
if (parts.length !== 3) return null;
|
|
3821
|
+
const payload = decodeSegment(parts[1]);
|
|
3822
|
+
if (!payload || typeof payload !== "object") return null;
|
|
3823
|
+
const exp = payload.exp;
|
|
3824
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
|
|
3825
|
+
return exp * 1e3;
|
|
3826
|
+
}
|
|
3816
3827
|
__name(getTokenExpiry, "getTokenExpiry");
|
|
3817
|
-
function
|
|
3828
|
+
function isTokenExpired(token, skewMs = 0, now = Date.now()) {
|
|
3829
|
+
const expiry = getTokenExpiry(token);
|
|
3830
|
+
if (expiry === null) return true;
|
|
3831
|
+
return expiry - skewMs <= now;
|
|
3832
|
+
}
|
|
3833
|
+
__name(isTokenExpired, "isTokenExpired");
|
|
3834
|
+
function isTokenExpiringSoon(token, thresholdMs, now = Date.now()) {
|
|
3818
3835
|
const expiry = getTokenExpiry(token);
|
|
3819
|
-
if (
|
|
3820
|
-
return expiry -
|
|
3836
|
+
if (expiry === null) return false;
|
|
3837
|
+
return expiry - now < thresholdMs;
|
|
3821
3838
|
}
|
|
3822
3839
|
__name(isTokenExpiringSoon, "isTokenExpiringSoon");
|
|
3840
|
+
|
|
3841
|
+
// src/auth/hooks/useTokenRefresh.ts
|
|
3842
|
+
var TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1e3;
|
|
3843
|
+
var CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
3823
3844
|
function useTokenRefresh(options = {}) {
|
|
3824
3845
|
const { enabled = true, onRefresh, onRefreshError } = options;
|
|
3825
3846
|
const refreshToken = useCallback9(async () => {
|
|
@@ -4683,6 +4704,12 @@ var hasValidTokens = /* @__PURE__ */ __name(() => {
|
|
|
4683
4704
|
if (typeof window === "undefined") return false;
|
|
4684
4705
|
return CfgAccountsApi.isAuthenticated();
|
|
4685
4706
|
}, "hasValidTokens");
|
|
4707
|
+
var isSessionDeadOnBootstrap = /* @__PURE__ */ __name((accessToken, refreshToken, now = Date.now()) => {
|
|
4708
|
+
const accessDead = isTokenExpired(accessToken, 0, now);
|
|
4709
|
+
if (!accessDead) return false;
|
|
4710
|
+
const refreshDead = isTokenExpired(refreshToken, 0, now);
|
|
4711
|
+
return refreshDead;
|
|
4712
|
+
}, "isSessionDeadOnBootstrap");
|
|
4686
4713
|
var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
4687
4714
|
const accounts = useAccountsContext();
|
|
4688
4715
|
ensureRefreshHandler();
|
|
@@ -4698,19 +4725,13 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4698
4725
|
return true;
|
|
4699
4726
|
});
|
|
4700
4727
|
const [initialized, setInitialized] = useState12(false);
|
|
4728
|
+
const [authTick, setAuthTick] = useState12(0);
|
|
4729
|
+
const bumpAuthTick = useCallback12(() => setAuthTick((t) => t + 1), []);
|
|
4730
|
+
const onUnauthorizedRef = useRef6(false);
|
|
4701
4731
|
const router = useCfgRouter();
|
|
4702
4732
|
const pathname = usePathname3();
|
|
4703
4733
|
const queryParams = useQueryParams();
|
|
4704
4734
|
const [storedEmail, setStoredEmail, clearStoredEmail] = useLocalStorage(EMAIL_STORAGE_KEY, null);
|
|
4705
|
-
useTokenRefresh({
|
|
4706
|
-
enabled: true,
|
|
4707
|
-
onRefresh: /* @__PURE__ */ __name((newToken) => {
|
|
4708
|
-
authLogger.info("Token auto-refreshed successfully");
|
|
4709
|
-
}, "onRefresh"),
|
|
4710
|
-
onRefreshError: /* @__PURE__ */ __name((error) => {
|
|
4711
|
-
authLogger.warn("Token auto-refresh failed:", error.message);
|
|
4712
|
-
}, "onRefreshError")
|
|
4713
|
-
});
|
|
4714
4735
|
const user = accounts.profile;
|
|
4715
4736
|
const userRef = useRef6(user);
|
|
4716
4737
|
const configRef = useRef6(config);
|
|
@@ -4727,7 +4748,23 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4727
4748
|
clearProfileCache();
|
|
4728
4749
|
setInitialized(true);
|
|
4729
4750
|
setIsLoading(false);
|
|
4730
|
-
|
|
4751
|
+
bumpAuthTick();
|
|
4752
|
+
}, [bumpAuthTick]);
|
|
4753
|
+
const handleProactiveRefreshError = useCallback12((error) => {
|
|
4754
|
+
authLogger.warn("Proactive token refresh failed \u2014 session is dead, redirecting to login:", error.message);
|
|
4755
|
+
if (onUnauthorizedRef.current) return;
|
|
4756
|
+
onUnauthorizedRef.current = true;
|
|
4757
|
+
clearAuthState("proactiveRefresh:failed");
|
|
4758
|
+
const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
|
|
4759
|
+
router.hardReplace(authCallbackUrl);
|
|
4760
|
+
}, [clearAuthState, router]);
|
|
4761
|
+
useTokenRefresh({
|
|
4762
|
+
enabled: true,
|
|
4763
|
+
onRefresh: /* @__PURE__ */ __name(() => {
|
|
4764
|
+
authLogger.info("Token auto-refreshed successfully");
|
|
4765
|
+
}, "onRefresh"),
|
|
4766
|
+
onRefreshError: handleProactiveRefreshError
|
|
4767
|
+
});
|
|
4731
4768
|
const handleGlobalAuthError = useCallback12((error, context = "API Request") => {
|
|
4732
4769
|
const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
|
|
4733
4770
|
if (isAuthError) {
|
|
@@ -4740,6 +4777,20 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4740
4777
|
}
|
|
4741
4778
|
return false;
|
|
4742
4779
|
}, [clearAuthState]);
|
|
4780
|
+
useEffect9(() => {
|
|
4781
|
+
const handler = /* @__PURE__ */ __name(() => {
|
|
4782
|
+
if (onUnauthorizedRef.current) return;
|
|
4783
|
+
onUnauthorizedRef.current = true;
|
|
4784
|
+
authLogger.warn("Unrecoverable 401 (refresh failed) \u2014 clearing session and redirecting to login");
|
|
4785
|
+
clearAuthState("onUnauthorized:401");
|
|
4786
|
+
const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
|
|
4787
|
+
router.hardReplace(authCallbackUrl);
|
|
4788
|
+
}, "handler");
|
|
4789
|
+
CfgAccountsApi.onUnauthorized(handler);
|
|
4790
|
+
return () => {
|
|
4791
|
+
CfgAccountsApi.onUnauthorized(null);
|
|
4792
|
+
};
|
|
4793
|
+
}, [clearAuthState, router]);
|
|
4743
4794
|
const isAutoLoggingOutRef = useRef6(false);
|
|
4744
4795
|
const swrOnError = useCallback12((error) => {
|
|
4745
4796
|
const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
|
|
@@ -4804,6 +4855,11 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4804
4855
|
authLogger.info("Token from API:", token ? `${token.substring(0, 20)}...` : "null");
|
|
4805
4856
|
authLogger.info("Refresh token from API:", refreshToken2 ? `${refreshToken2.substring(0, 20)}...` : "null");
|
|
4806
4857
|
authLogger.info("localStorage keys:", Object.keys(localStorage).filter((k) => k.includes("token") || k.includes("auth")));
|
|
4858
|
+
if (!isInIframe && isSessionDeadOnBootstrap(token, refreshToken2)) {
|
|
4859
|
+
authLogger.warn("Bootstrap: dead session in storage (access + refresh both unusable) \u2014 clearing and showing login");
|
|
4860
|
+
clearAuthState("initializeAuth:deadSession");
|
|
4861
|
+
return;
|
|
4862
|
+
}
|
|
4807
4863
|
const hasTokens = hasValidTokens();
|
|
4808
4864
|
authLogger.info("Has tokens:", hasTokens);
|
|
4809
4865
|
if (userRef.current) {
|
|
@@ -5060,7 +5116,9 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
5060
5116
|
() => ({
|
|
5061
5117
|
user,
|
|
5062
5118
|
isLoading,
|
|
5063
|
-
// Consider authenticated if we have valid tokens, even without user profile
|
|
5119
|
+
// Consider authenticated if we have valid tokens, even without user profile.
|
|
5120
|
+
// `authTick` (in deps) forces recompute after tokens are cleared/set, since
|
|
5121
|
+
// token state lives in storage, not React state.
|
|
5064
5122
|
isAuthenticated: CfgAccountsApi.isAuthenticated(),
|
|
5065
5123
|
isAdminUser,
|
|
5066
5124
|
loadCurrentProfile,
|
|
@@ -5086,6 +5144,8 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
5086
5144
|
[
|
|
5087
5145
|
user,
|
|
5088
5146
|
isLoading,
|
|
5147
|
+
authTick,
|
|
5148
|
+
// recompute isAuthenticated when tokens are cleared/set
|
|
5089
5149
|
isAdminUser,
|
|
5090
5150
|
loadCurrentProfile,
|
|
5091
5151
|
checkAuthAndRedirect,
|