@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 +25 -0
- package/dist/auth.cjs +17 -4
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +17 -2
- package/dist/auth.d.ts +17 -2
- package/dist/auth.mjs +17 -4
- package/dist/auth.mjs.map +1 -1
- package/package.json +2 -2
- package/src/auth/__tests__/guard.test.ts +65 -0
- package/src/auth/context/AccountsContext.tsx +17 -4
- package/src/auth/utils/guard.ts +27 -1
- package/src/auth/utils/index.ts +1 -0
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
|
-
|
|
4645
|
-
|
|
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");
|