@djangocfg/api 2.1.547 → 2.1.549

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 CHANGED
@@ -77,6 +77,31 @@ import {
77
77
  } from '@djangocfg/api/auth';
78
78
  ```
79
79
 
80
+ ### Guard resolvers
81
+
82
+ The "spinner vs redirect vs render" decision is pure and separately testable.
83
+ `useAuthGuard` (and `@djangocfg/layouts`' `PrivateLayout`) just wire React state
84
+ into these:
85
+
86
+ ```typescript
87
+ import {
88
+ resolveGuardIsLoading, // show the preloader?
89
+ shouldRedirectToAuth, // no session → send to the auth form
90
+ isForbidden, // signed in, but lacks the required privilege
91
+ resolveGuardIsAuthenticated,
92
+ type GuardInput,
93
+ } from '@djangocfg/api/auth';
94
+ ```
95
+
96
+ `GuardInput` takes two optional fields for the role gate: `requireStaff` (the
97
+ route needs staff/admin) and `isAdminUser` (from `useAuth()` — the
98
+ `is_staff || is_superuser` fold, never one flag).
99
+
100
+ `isForbidden` is deliberately **not** a redirect condition: the session is
101
+ valid, so the auth form cannot fix it, and redirecting would loop on the saved
102
+ URL. Render a distinct "not authorised" state instead. `resolveGuardIsLoading`
103
+ returns `false` once forbidden, so the preloader never spins on it.
104
+
80
105
  ## Token storage & DPoP
81
106
 
82
107
  The token lives in the shared `auth` store (localStorage by default, switchable
package/dist/auth.cjs CHANGED
@@ -50,6 +50,7 @@ __export(auth_exports, {
50
50
  getCachedProfile: () => getCachedProfile,
51
51
  hasValidCache: () => hasValidCache,
52
52
  isAllowedAuthPath: () => isAllowedAuthPath,
53
+ isForbidden: () => isForbidden,
53
54
  logger: () => logger,
54
55
  normalizePath: () => normalizePath,
55
56
  peekSavedRedirect: () => peekSavedRedirect,
@@ -4603,6 +4604,7 @@ function AccountsProvider({ children }) {
4603
4604
  const [profileError, setProfileError] = (0, import_react15.useState)(null);
4604
4605
  const profileRef = (0, import_react15.useRef)(profile);
4605
4606
  const isLoadingRef = (0, import_react15.useRef)(false);
4607
+ const fetchSeqRef = (0, import_react15.useRef)(0);
4606
4608
  (0, import_react15.useEffect)(() => {
4607
4609
  profileRef.current = profile;
4608
4610
  }, [profile]);
@@ -4621,10 +4623,11 @@ function AccountsProvider({ children }) {
4621
4623
  authLogger.debug(`No API origin configured, skipping profile fetch (caller: ${callerId})`);
4622
4624
  return void 0;
4623
4625
  }
4624
- if (isLoadingRef.current) {
4626
+ if (isLoadingRef.current && !force) {
4625
4627
  authLogger.debug(`Profile loading in progress, skipping (caller: ${callerId})`);
4626
4628
  return profileRef.current;
4627
4629
  }
4630
+ const seq = ++fetchSeqRef.current;
4628
4631
  setIsLoadingProfile(true);
4629
4632
  isLoadingRef.current = true;
4630
4633
  setProfileError(null);
@@ -4632,17 +4635,20 @@ function AccountsProvider({ children }) {
4632
4635
  authLogger.debug(`Fetching profile from API (caller: ${callerId}, force: ${force})`);
4633
4636
  const res = await CfgAccountsProfile.cfgAccountsProfileRetrieve({ throwOnError: true });
4634
4637
  const result = res.data;
4638
+ if (seq !== fetchSeqRef.current) return result;
4635
4639
  setProfile(result);
4636
4640
  profileRef.current = result;
4637
4641
  setCachedProfile(result);
4638
4642
  return result;
4639
4643
  } catch (error) {
4640
4644
  const err = error instanceof Error ? error : new Error("Failed to fetch profile");
4641
- setProfileError(err);
4645
+ if (seq === fetchSeqRef.current) setProfileError(err);
4642
4646
  throw err;
4643
4647
  } finally {
4644
- setIsLoadingProfile(false);
4645
- isLoadingRef.current = false;
4648
+ if (seq === fetchSeqRef.current) {
4649
+ setIsLoadingProfile(false);
4650
+ isLoadingRef.current = false;
4651
+ }
4646
4652
  }
4647
4653
  }, []);
4648
4654
  const updateProfile = (0, import_react15.useCallback)(async (data) => {
@@ -5210,9 +5216,16 @@ var formatAuthError = /* @__PURE__ */ __name((error) => {
5210
5216
  }, "formatAuthError");
5211
5217
 
5212
5218
  // src/auth/utils/guard.ts
5219
+ function isForbidden(s) {
5220
+ return Boolean(
5221
+ s.mounted && s.requireAuth && s.requireStaff && !s.authLoading && s.isAuthenticated && !s.isAdminUser
5222
+ );
5223
+ }
5224
+ __name(isForbidden, "isForbidden");
5213
5225
  function resolveGuardIsLoading(s) {
5214
5226
  if (!s.mounted) return true;
5215
5227
  if (!s.requireAuth) return false;
5228
+ if (isForbidden(s)) return false;
5216
5229
  return s.authLoading || s.isRedirecting || !s.isAuthenticated;
5217
5230
  }
5218
5231
  __name(resolveGuardIsLoading, "resolveGuardIsLoading");