@djangocfg/api 2.1.449 → 2.1.452
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 +147 -25
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +107 -2
- package/dist/auth.d.ts +107 -2
- package/dist/auth.mjs +147 -25
- package/dist/auth.mjs.map +1 -1
- package/package.json +4 -2
- package/src/auth/__tests__/authRedirect.test.ts +101 -0
- package/src/auth/__tests__/base64.test.ts +53 -0
- package/src/auth/__tests__/constants.test.ts +30 -0
- package/src/auth/__tests__/formatAuthError.test.ts +48 -0
- package/src/auth/__tests__/guard.test.ts +144 -0
- package/src/auth/__tests__/jwt.test.ts +119 -0
- package/src/auth/__tests__/onUnauthorized.test.ts +206 -0
- package/src/auth/__tests__/path.test.ts +67 -0
- package/src/auth/__tests__/profileCache.test.ts +138 -0
- package/src/auth/__tests__/sessionBootstrap.test.ts +111 -0
- package/src/auth/__tests__/useAuthForm.2fa.test.ts +28 -18
- package/src/auth/__tests__/useTokenRefresh.dom.test.tsx +167 -0
- package/src/auth/__tests__/useTwoFactorStatus.test.ts +45 -36
- package/src/auth/__tests__/validation.test.ts +54 -0
- package/src/auth/context/AuthContext.tsx +172 -14
- package/src/auth/hooks/useAutoAuth.ts +5 -17
- package/src/auth/hooks/useTokenRefresh.ts +3 -21
- package/src/auth/utils/guard.ts +79 -0
- package/src/auth/utils/index.ts +9 -0
- package/src/auth/utils/jwt.ts +66 -0
- package/src/auth/utils/path.ts +34 -0
package/dist/auth.cjs
CHANGED
|
@@ -46,9 +46,18 @@ __export(auth_exports, {
|
|
|
46
46
|
formatAuthError: () => formatAuthError,
|
|
47
47
|
getCacheMetadata: () => getCacheMetadata,
|
|
48
48
|
getCachedProfile: () => getCachedProfile,
|
|
49
|
+
getTokenExpiry: () => getTokenExpiry,
|
|
49
50
|
hasValidCache: () => hasValidCache,
|
|
51
|
+
isAllowedAuthPath: () => isAllowedAuthPath,
|
|
52
|
+
isTokenExpired: () => isTokenExpired,
|
|
53
|
+
isTokenExpiringSoon: () => isTokenExpiringSoon,
|
|
50
54
|
logger: () => logger,
|
|
55
|
+
nextAuthEvaluationDelay: () => nextAuthEvaluationDelay,
|
|
56
|
+
normalizePath: () => normalizePath,
|
|
57
|
+
resolveGuardIsAuthenticated: () => resolveGuardIsAuthenticated,
|
|
58
|
+
resolveGuardIsLoading: () => resolveGuardIsLoading,
|
|
51
59
|
setCachedProfile: () => setCachedProfile,
|
|
60
|
+
shouldRedirectToAuth: () => shouldRedirectToAuth,
|
|
52
61
|
useAccountsContext: () => useAccountsContext,
|
|
53
62
|
useAuth: () => useAuth,
|
|
54
63
|
useAuthForm: () => useAuthForm,
|
|
@@ -348,21 +357,28 @@ var authLogger = logger.withTag("auth");
|
|
|
348
357
|
// src/auth/hooks/useAutoAuth.ts
|
|
349
358
|
var import_navigation3 = require("next/navigation");
|
|
350
359
|
var import_react5 = require("react");
|
|
360
|
+
|
|
361
|
+
// src/auth/utils/path.ts
|
|
351
362
|
var normalizePath = /* @__PURE__ */ __name((path) => {
|
|
352
363
|
if (!path) return "";
|
|
353
364
|
const withoutLocale = path.replace(/^\/[a-z]{2}(?=\/|$)/, "");
|
|
354
365
|
const trimmed = withoutLocale.replace(/\/+$/, "");
|
|
355
366
|
return trimmed || "/";
|
|
356
367
|
}, "normalizePath");
|
|
368
|
+
var isAllowedAuthPath = /* @__PURE__ */ __name((pathname, allowedPaths) => {
|
|
369
|
+
const normalized = normalizePath(pathname);
|
|
370
|
+
return allowedPaths.some(
|
|
371
|
+
(p) => normalized === p || normalized.startsWith(p + "/")
|
|
372
|
+
);
|
|
373
|
+
}, "isAllowedAuthPath");
|
|
374
|
+
|
|
375
|
+
// src/auth/hooks/useAutoAuth.ts
|
|
357
376
|
var useAutoAuth = /* @__PURE__ */ __name((options = {}) => {
|
|
358
377
|
const { onOTPDetected, cleanupUrl = true, allowedPaths = ["/auth"] } = options;
|
|
359
378
|
const queryParams = useQueryParams();
|
|
360
379
|
const pathname = (0, import_navigation3.usePathname)();
|
|
361
380
|
const router = useCfgRouter();
|
|
362
|
-
const
|
|
363
|
-
const isAllowedPath = allowedPaths.some(
|
|
364
|
-
(path) => normalizedPath === path || normalizedPath.startsWith(path + "/")
|
|
365
|
-
);
|
|
381
|
+
const isAllowedPath = isAllowedAuthPath(pathname, allowedPaths);
|
|
366
382
|
const queryOtp = queryParams.get("otp") || "";
|
|
367
383
|
const queryEmail = queryParams.get("email") || void 0;
|
|
368
384
|
const hasOTP = !!queryOtp;
|
|
@@ -3864,24 +3880,45 @@ function triggerRefresh() {
|
|
|
3864
3880
|
}
|
|
3865
3881
|
__name(triggerRefresh, "triggerRefresh");
|
|
3866
3882
|
|
|
3867
|
-
// src/auth/
|
|
3868
|
-
|
|
3869
|
-
var CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
3870
|
-
function getTokenExpiry(token) {
|
|
3883
|
+
// src/auth/utils/jwt.ts
|
|
3884
|
+
function decodeSegment(segment) {
|
|
3871
3885
|
try {
|
|
3872
|
-
const
|
|
3873
|
-
|
|
3886
|
+
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
3887
|
+
if (typeof atob !== "function") return null;
|
|
3888
|
+
const json = atob(base64);
|
|
3889
|
+
return JSON.parse(json);
|
|
3874
3890
|
} catch {
|
|
3875
3891
|
return null;
|
|
3876
3892
|
}
|
|
3877
3893
|
}
|
|
3894
|
+
__name(decodeSegment, "decodeSegment");
|
|
3895
|
+
function getTokenExpiry(token) {
|
|
3896
|
+
if (!token || typeof token !== "string") return null;
|
|
3897
|
+
const parts = token.split(".");
|
|
3898
|
+
if (parts.length !== 3) return null;
|
|
3899
|
+
const payload = decodeSegment(parts[1]);
|
|
3900
|
+
if (!payload || typeof payload !== "object") return null;
|
|
3901
|
+
const exp = payload.exp;
|
|
3902
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
|
|
3903
|
+
return exp * 1e3;
|
|
3904
|
+
}
|
|
3878
3905
|
__name(getTokenExpiry, "getTokenExpiry");
|
|
3879
|
-
function
|
|
3906
|
+
function isTokenExpired(token, skewMs = 0, now = Date.now()) {
|
|
3907
|
+
const expiry = getTokenExpiry(token);
|
|
3908
|
+
if (expiry === null) return true;
|
|
3909
|
+
return expiry - skewMs <= now;
|
|
3910
|
+
}
|
|
3911
|
+
__name(isTokenExpired, "isTokenExpired");
|
|
3912
|
+
function isTokenExpiringSoon(token, thresholdMs, now = Date.now()) {
|
|
3880
3913
|
const expiry = getTokenExpiry(token);
|
|
3881
|
-
if (
|
|
3882
|
-
return expiry -
|
|
3914
|
+
if (expiry === null) return false;
|
|
3915
|
+
return expiry - now < thresholdMs;
|
|
3883
3916
|
}
|
|
3884
3917
|
__name(isTokenExpiringSoon, "isTokenExpiringSoon");
|
|
3918
|
+
|
|
3919
|
+
// src/auth/hooks/useTokenRefresh.ts
|
|
3920
|
+
var TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1e3;
|
|
3921
|
+
var CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
3885
3922
|
function useTokenRefresh(options = {}) {
|
|
3886
3923
|
const { enabled = true, onRefresh, onRefreshError } = options;
|
|
3887
3924
|
const refreshToken = (0, import_react14.useCallback)(async () => {
|
|
@@ -3981,6 +4018,30 @@ var useDeleteAccount = /* @__PURE__ */ __name(() => {
|
|
|
3981
4018
|
};
|
|
3982
4019
|
}, "useDeleteAccount");
|
|
3983
4020
|
|
|
4021
|
+
// src/auth/utils/guard.ts
|
|
4022
|
+
function resolveGuardIsLoading(s) {
|
|
4023
|
+
if (!s.mounted) return true;
|
|
4024
|
+
if (!s.requireAuth) return false;
|
|
4025
|
+
return s.authLoading || s.isRedirecting || !s.isAuthenticated;
|
|
4026
|
+
}
|
|
4027
|
+
__name(resolveGuardIsLoading, "resolveGuardIsLoading");
|
|
4028
|
+
function shouldRedirectToAuth(s) {
|
|
4029
|
+
return s.mounted && s.requireAuth && !s.authLoading && !s.isAuthenticated && !s.isRedirecting;
|
|
4030
|
+
}
|
|
4031
|
+
__name(shouldRedirectToAuth, "shouldRedirectToAuth");
|
|
4032
|
+
function resolveGuardIsAuthenticated(s) {
|
|
4033
|
+
return !s.requireAuth || s.isAuthenticated;
|
|
4034
|
+
}
|
|
4035
|
+
__name(resolveGuardIsAuthenticated, "resolveGuardIsAuthenticated");
|
|
4036
|
+
function nextAuthEvaluationDelay(accessExp, refreshExp, now) {
|
|
4037
|
+
const future = [accessExp, refreshExp].filter(
|
|
4038
|
+
(d) => d !== null && d > now
|
|
4039
|
+
);
|
|
4040
|
+
if (future.length === 0) return null;
|
|
4041
|
+
return Math.max(0, Math.min(...future) - now) + 1e3;
|
|
4042
|
+
}
|
|
4043
|
+
__name(nextAuthEvaluationDelay, "nextAuthEvaluationDelay");
|
|
4044
|
+
|
|
3984
4045
|
// src/auth/context/AccountsContext.tsx
|
|
3985
4046
|
var import_react16 = require("react");
|
|
3986
4047
|
|
|
@@ -4737,6 +4798,20 @@ var hasValidTokens = /* @__PURE__ */ __name(() => {
|
|
|
4737
4798
|
if (typeof window === "undefined") return false;
|
|
4738
4799
|
return CfgAccountsApi.isAuthenticated();
|
|
4739
4800
|
}, "hasValidTokens");
|
|
4801
|
+
var isSessionDeadOnBootstrap = /* @__PURE__ */ __name((accessToken, refreshToken, now = Date.now()) => {
|
|
4802
|
+
const accessDead = isTokenExpired(accessToken, 0, now);
|
|
4803
|
+
if (!accessDead) return false;
|
|
4804
|
+
const refreshDead = isTokenExpired(refreshToken, 0, now);
|
|
4805
|
+
return refreshDead;
|
|
4806
|
+
}, "isSessionDeadOnBootstrap");
|
|
4807
|
+
var isSessionAlive = /* @__PURE__ */ __name((now = Date.now()) => {
|
|
4808
|
+
if (typeof window === "undefined") return false;
|
|
4809
|
+
return !isSessionDeadOnBootstrap(
|
|
4810
|
+
CfgAccountsApi.getToken(),
|
|
4811
|
+
CfgAccountsApi.getRefreshToken(),
|
|
4812
|
+
now
|
|
4813
|
+
);
|
|
4814
|
+
}, "isSessionAlive");
|
|
4740
4815
|
var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
4741
4816
|
const accounts = useAccountsContext();
|
|
4742
4817
|
ensureRefreshHandler();
|
|
@@ -4752,19 +4827,24 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4752
4827
|
return true;
|
|
4753
4828
|
});
|
|
4754
4829
|
const [initialized, setInitialized] = (0, import_react17.useState)(false);
|
|
4830
|
+
const [authTick, setAuthTick] = (0, import_react17.useState)(0);
|
|
4831
|
+
const bumpAuthTick = (0, import_react17.useCallback)(() => setAuthTick((t) => t + 1), []);
|
|
4832
|
+
(0, import_react17.useEffect)(() => {
|
|
4833
|
+
if (typeof window === "undefined") return;
|
|
4834
|
+
const delay = nextAuthEvaluationDelay(
|
|
4835
|
+
getTokenExpiry(CfgAccountsApi.getToken()),
|
|
4836
|
+
getTokenExpiry(CfgAccountsApi.getRefreshToken()),
|
|
4837
|
+
Date.now()
|
|
4838
|
+
);
|
|
4839
|
+
if (delay === null) return;
|
|
4840
|
+
const timer = setTimeout(bumpAuthTick, Math.min(delay, 2e9));
|
|
4841
|
+
return () => clearTimeout(timer);
|
|
4842
|
+
}, [authTick, bumpAuthTick]);
|
|
4843
|
+
const onUnauthorizedRef = (0, import_react17.useRef)(false);
|
|
4755
4844
|
const router = useCfgRouter();
|
|
4756
4845
|
const pathname = (0, import_navigation4.usePathname)();
|
|
4757
4846
|
const queryParams = useQueryParams();
|
|
4758
4847
|
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
4848
|
const user = accounts.profile;
|
|
4769
4849
|
const userRef = (0, import_react17.useRef)(user);
|
|
4770
4850
|
const configRef = (0, import_react17.useRef)(config);
|
|
@@ -4781,7 +4861,23 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4781
4861
|
clearProfileCache();
|
|
4782
4862
|
setInitialized(true);
|
|
4783
4863
|
setIsLoading(false);
|
|
4784
|
-
|
|
4864
|
+
bumpAuthTick();
|
|
4865
|
+
}, [bumpAuthTick]);
|
|
4866
|
+
const handleProactiveRefreshError = (0, import_react17.useCallback)((error) => {
|
|
4867
|
+
authLogger.warn("Proactive token refresh failed \u2014 session is dead, redirecting to login:", error.message);
|
|
4868
|
+
if (onUnauthorizedRef.current) return;
|
|
4869
|
+
onUnauthorizedRef.current = true;
|
|
4870
|
+
clearAuthState("proactiveRefresh:failed");
|
|
4871
|
+
const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
|
|
4872
|
+
router.hardReplace(authCallbackUrl);
|
|
4873
|
+
}, [clearAuthState, router]);
|
|
4874
|
+
useTokenRefresh({
|
|
4875
|
+
enabled: true,
|
|
4876
|
+
onRefresh: /* @__PURE__ */ __name(() => {
|
|
4877
|
+
authLogger.info("Token auto-refreshed successfully");
|
|
4878
|
+
}, "onRefresh"),
|
|
4879
|
+
onRefreshError: handleProactiveRefreshError
|
|
4880
|
+
});
|
|
4785
4881
|
const handleGlobalAuthError = (0, import_react17.useCallback)((error, context = "API Request") => {
|
|
4786
4882
|
const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
|
|
4787
4883
|
if (isAuthError) {
|
|
@@ -4794,6 +4890,20 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4794
4890
|
}
|
|
4795
4891
|
return false;
|
|
4796
4892
|
}, [clearAuthState]);
|
|
4893
|
+
(0, import_react17.useEffect)(() => {
|
|
4894
|
+
const handler = /* @__PURE__ */ __name(() => {
|
|
4895
|
+
if (onUnauthorizedRef.current) return;
|
|
4896
|
+
onUnauthorizedRef.current = true;
|
|
4897
|
+
authLogger.warn("Unrecoverable 401 (refresh failed) \u2014 clearing session and redirecting to login");
|
|
4898
|
+
clearAuthState("onUnauthorized:401");
|
|
4899
|
+
const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
|
|
4900
|
+
router.hardReplace(authCallbackUrl);
|
|
4901
|
+
}, "handler");
|
|
4902
|
+
CfgAccountsApi.onUnauthorized(handler);
|
|
4903
|
+
return () => {
|
|
4904
|
+
CfgAccountsApi.onUnauthorized(null);
|
|
4905
|
+
};
|
|
4906
|
+
}, [clearAuthState, router]);
|
|
4797
4907
|
const isAutoLoggingOutRef = (0, import_react17.useRef)(false);
|
|
4798
4908
|
const swrOnError = (0, import_react17.useCallback)((error) => {
|
|
4799
4909
|
const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
|
|
@@ -4858,6 +4968,11 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
4858
4968
|
authLogger.info("Token from API:", token ? `${token.substring(0, 20)}...` : "null");
|
|
4859
4969
|
authLogger.info("Refresh token from API:", refreshToken2 ? `${refreshToken2.substring(0, 20)}...` : "null");
|
|
4860
4970
|
authLogger.info("localStorage keys:", Object.keys(localStorage).filter((k) => k.includes("token") || k.includes("auth")));
|
|
4971
|
+
if (!isInIframe && isSessionDeadOnBootstrap(token, refreshToken2)) {
|
|
4972
|
+
authLogger.warn("Bootstrap: dead session in storage (access + refresh both unusable) \u2014 clearing and showing login");
|
|
4973
|
+
clearAuthState("initializeAuth:deadSession");
|
|
4974
|
+
return;
|
|
4975
|
+
}
|
|
4861
4976
|
const hasTokens = hasValidTokens();
|
|
4862
4977
|
authLogger.info("Has tokens:", hasTokens);
|
|
4863
4978
|
if (userRef.current) {
|
|
@@ -5114,8 +5229,13 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
5114
5229
|
() => ({
|
|
5115
5230
|
user,
|
|
5116
5231
|
isLoading,
|
|
5117
|
-
//
|
|
5118
|
-
|
|
5232
|
+
// Authenticated iff the session is locally alive (access valid OR a usable
|
|
5233
|
+
// refresh token) — validated by `exp`, NOT by mere token presence. This is
|
|
5234
|
+
// what makes the guard self-sufficient: an expired session reads as
|
|
5235
|
+
// unauthenticated WITHOUT waiting for a server 401 (which CSP / a hung
|
|
5236
|
+
// request / offline can swallow). `authTick` (in deps) forces recompute
|
|
5237
|
+
// after tokens are cleared/set, since token state lives in storage.
|
|
5238
|
+
isAuthenticated: isSessionAlive(),
|
|
5119
5239
|
isAdminUser,
|
|
5120
5240
|
loadCurrentProfile,
|
|
5121
5241
|
checkAuthAndRedirect,
|
|
@@ -5140,6 +5260,8 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
|
|
|
5140
5260
|
[
|
|
5141
5261
|
user,
|
|
5142
5262
|
isLoading,
|
|
5263
|
+
authTick,
|
|
5264
|
+
// recompute isAuthenticated when tokens are cleared/set
|
|
5143
5265
|
isAdminUser,
|
|
5144
5266
|
loadCurrentProfile,
|
|
5145
5267
|
checkAuthAndRedirect,
|