@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.cjs
CHANGED
|
@@ -3864,24 +3864,45 @@ function triggerRefresh() {
|
|
|
3864
3864
|
}
|
|
3865
3865
|
__name(triggerRefresh, "triggerRefresh");
|
|
3866
3866
|
|
|
3867
|
-
// src/auth/
|
|
3868
|
-
|
|
3869
|
-
var CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
3870
|
-
function getTokenExpiry(token) {
|
|
3867
|
+
// src/auth/utils/jwt.ts
|
|
3868
|
+
function decodeSegment(segment) {
|
|
3871
3869
|
try {
|
|
3872
|
-
const
|
|
3873
|
-
|
|
3870
|
+
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
3871
|
+
if (typeof atob !== "function") return null;
|
|
3872
|
+
const json = atob(base64);
|
|
3873
|
+
return JSON.parse(json);
|
|
3874
3874
|
} catch {
|
|
3875
3875
|
return null;
|
|
3876
3876
|
}
|
|
3877
3877
|
}
|
|
3878
|
+
__name(decodeSegment, "decodeSegment");
|
|
3879
|
+
function getTokenExpiry(token) {
|
|
3880
|
+
if (!token || typeof token !== "string") return null;
|
|
3881
|
+
const parts = token.split(".");
|
|
3882
|
+
if (parts.length !== 3) return null;
|
|
3883
|
+
const payload = decodeSegment(parts[1]);
|
|
3884
|
+
if (!payload || typeof payload !== "object") return null;
|
|
3885
|
+
const exp = payload.exp;
|
|
3886
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
|
|
3887
|
+
return exp * 1e3;
|
|
3888
|
+
}
|
|
3878
3889
|
__name(getTokenExpiry, "getTokenExpiry");
|
|
3879
|
-
function
|
|
3890
|
+
function isTokenExpired(token, skewMs = 0, now = Date.now()) {
|
|
3891
|
+
const expiry = getTokenExpiry(token);
|
|
3892
|
+
if (expiry === null) return true;
|
|
3893
|
+
return expiry - skewMs <= now;
|
|
3894
|
+
}
|
|
3895
|
+
__name(isTokenExpired, "isTokenExpired");
|
|
3896
|
+
function isTokenExpiringSoon(token, thresholdMs, now = Date.now()) {
|
|
3880
3897
|
const expiry = getTokenExpiry(token);
|
|
3881
|
-
if (
|
|
3882
|
-
return expiry -
|
|
3898
|
+
if (expiry === null) return false;
|
|
3899
|
+
return expiry - now < thresholdMs;
|
|
3883
3900
|
}
|
|
3884
3901
|
__name(isTokenExpiringSoon, "isTokenExpiringSoon");
|
|
3902
|
+
|
|
3903
|
+
// src/auth/hooks/useTokenRefresh.ts
|
|
3904
|
+
var TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1e3;
|
|
3905
|
+
var CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
3885
3906
|
function useTokenRefresh(options = {}) {
|
|
3886
3907
|
const { enabled = true, onRefresh, onRefreshError } = options;
|
|
3887
3908
|
const refreshToken = (0, import_react14.useCallback)(async () => {
|
|
@@ -4737,6 +4758,12 @@ var hasValidTokens = /* @__PURE__ */ __name(() => {
|
|
|
4737
4758
|
if (typeof window === "undefined") return false;
|
|
4738
4759
|
return CfgAccountsApi.isAuthenticated();
|
|
4739
4760
|
}, "hasValidTokens");
|
|
4761
|
+
var isSessionDeadOnBootstrap = /* @__PURE__ */ __name((accessToken, refreshToken, now = Date.now()) => {
|
|
4762
|
+
const accessDead = isTokenExpired(accessToken, 0, now);
|
|
4763
|
+
if (!accessDead) return false;
|
|
4764
|
+
const refreshDead = isTokenExpired(refreshToken, 0, now);
|
|
4765
|
+
return refreshDead;
|
|
4766
|
+
}, "isSessionDeadOnBootstrap");
|
|
4740
4767
|
var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
4741
4768
|
const accounts = useAccountsContext();
|
|
4742
4769
|
ensureRefreshHandler();
|
|
@@ -4752,19 +4779,13 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4752
4779
|
return true;
|
|
4753
4780
|
});
|
|
4754
4781
|
const [initialized, setInitialized] = (0, import_react17.useState)(false);
|
|
4782
|
+
const [authTick, setAuthTick] = (0, import_react17.useState)(0);
|
|
4783
|
+
const bumpAuthTick = (0, import_react17.useCallback)(() => setAuthTick((t) => t + 1), []);
|
|
4784
|
+
const onUnauthorizedRef = (0, import_react17.useRef)(false);
|
|
4755
4785
|
const router = useCfgRouter();
|
|
4756
4786
|
const pathname = (0, import_navigation4.usePathname)();
|
|
4757
4787
|
const queryParams = useQueryParams();
|
|
4758
4788
|
const [storedEmail, setStoredEmail, clearStoredEmail] = useLocalStorage(EMAIL_STORAGE_KEY, null);
|
|
4759
|
-
useTokenRefresh({
|
|
4760
|
-
enabled: true,
|
|
4761
|
-
onRefresh: /* @__PURE__ */ __name((newToken) => {
|
|
4762
|
-
authLogger.info("Token auto-refreshed successfully");
|
|
4763
|
-
}, "onRefresh"),
|
|
4764
|
-
onRefreshError: /* @__PURE__ */ __name((error) => {
|
|
4765
|
-
authLogger.warn("Token auto-refresh failed:", error.message);
|
|
4766
|
-
}, "onRefreshError")
|
|
4767
|
-
});
|
|
4768
4789
|
const user = accounts.profile;
|
|
4769
4790
|
const userRef = (0, import_react17.useRef)(user);
|
|
4770
4791
|
const configRef = (0, import_react17.useRef)(config);
|
|
@@ -4781,7 +4802,23 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4781
4802
|
clearProfileCache();
|
|
4782
4803
|
setInitialized(true);
|
|
4783
4804
|
setIsLoading(false);
|
|
4784
|
-
|
|
4805
|
+
bumpAuthTick();
|
|
4806
|
+
}, [bumpAuthTick]);
|
|
4807
|
+
const handleProactiveRefreshError = (0, import_react17.useCallback)((error) => {
|
|
4808
|
+
authLogger.warn("Proactive token refresh failed \u2014 session is dead, redirecting to login:", error.message);
|
|
4809
|
+
if (onUnauthorizedRef.current) return;
|
|
4810
|
+
onUnauthorizedRef.current = true;
|
|
4811
|
+
clearAuthState("proactiveRefresh:failed");
|
|
4812
|
+
const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
|
|
4813
|
+
router.hardReplace(authCallbackUrl);
|
|
4814
|
+
}, [clearAuthState, router]);
|
|
4815
|
+
useTokenRefresh({
|
|
4816
|
+
enabled: true,
|
|
4817
|
+
onRefresh: /* @__PURE__ */ __name(() => {
|
|
4818
|
+
authLogger.info("Token auto-refreshed successfully");
|
|
4819
|
+
}, "onRefresh"),
|
|
4820
|
+
onRefreshError: handleProactiveRefreshError
|
|
4821
|
+
});
|
|
4785
4822
|
const handleGlobalAuthError = (0, import_react17.useCallback)((error, context = "API Request") => {
|
|
4786
4823
|
const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
|
|
4787
4824
|
if (isAuthError) {
|
|
@@ -4794,6 +4831,20 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4794
4831
|
}
|
|
4795
4832
|
return false;
|
|
4796
4833
|
}, [clearAuthState]);
|
|
4834
|
+
(0, import_react17.useEffect)(() => {
|
|
4835
|
+
const handler = /* @__PURE__ */ __name(() => {
|
|
4836
|
+
if (onUnauthorizedRef.current) return;
|
|
4837
|
+
onUnauthorizedRef.current = true;
|
|
4838
|
+
authLogger.warn("Unrecoverable 401 (refresh failed) \u2014 clearing session and redirecting to login");
|
|
4839
|
+
clearAuthState("onUnauthorized:401");
|
|
4840
|
+
const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
|
|
4841
|
+
router.hardReplace(authCallbackUrl);
|
|
4842
|
+
}, "handler");
|
|
4843
|
+
CfgAccountsApi.onUnauthorized(handler);
|
|
4844
|
+
return () => {
|
|
4845
|
+
CfgAccountsApi.onUnauthorized(null);
|
|
4846
|
+
};
|
|
4847
|
+
}, [clearAuthState, router]);
|
|
4797
4848
|
const isAutoLoggingOutRef = (0, import_react17.useRef)(false);
|
|
4798
4849
|
const swrOnError = (0, import_react17.useCallback)((error) => {
|
|
4799
4850
|
const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
|
|
@@ -4858,6 +4909,11 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4858
4909
|
authLogger.info("Token from API:", token ? `${token.substring(0, 20)}...` : "null");
|
|
4859
4910
|
authLogger.info("Refresh token from API:", refreshToken2 ? `${refreshToken2.substring(0, 20)}...` : "null");
|
|
4860
4911
|
authLogger.info("localStorage keys:", Object.keys(localStorage).filter((k) => k.includes("token") || k.includes("auth")));
|
|
4912
|
+
if (!isInIframe && isSessionDeadOnBootstrap(token, refreshToken2)) {
|
|
4913
|
+
authLogger.warn("Bootstrap: dead session in storage (access + refresh both unusable) \u2014 clearing and showing login");
|
|
4914
|
+
clearAuthState("initializeAuth:deadSession");
|
|
4915
|
+
return;
|
|
4916
|
+
}
|
|
4861
4917
|
const hasTokens = hasValidTokens();
|
|
4862
4918
|
authLogger.info("Has tokens:", hasTokens);
|
|
4863
4919
|
if (userRef.current) {
|
|
@@ -5114,7 +5170,9 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
5114
5170
|
() => ({
|
|
5115
5171
|
user,
|
|
5116
5172
|
isLoading,
|
|
5117
|
-
// Consider authenticated if we have valid tokens, even without user profile
|
|
5173
|
+
// Consider authenticated if we have valid tokens, even without user profile.
|
|
5174
|
+
// `authTick` (in deps) forces recompute after tokens are cleared/set, since
|
|
5175
|
+
// token state lives in storage, not React state.
|
|
5118
5176
|
isAuthenticated: CfgAccountsApi.isAuthenticated(),
|
|
5119
5177
|
isAdminUser,
|
|
5120
5178
|
loadCurrentProfile,
|
|
@@ -5140,6 +5198,8 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
5140
5198
|
[
|
|
5141
5199
|
user,
|
|
5142
5200
|
isLoading,
|
|
5201
|
+
authTick,
|
|
5202
|
+
// recompute isAuthenticated when tokens are cleared/set
|
|
5143
5203
|
isAdminUser,
|
|
5144
5204
|
loadCurrentProfile,
|
|
5145
5205
|
checkAuthAndRedirect,
|