@djangocfg/api 2.1.618 → 2.1.619
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 +4 -0
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.mjs +4 -0
- package/dist/auth.mjs.map +1 -1
- package/dist/clients.cjs +3 -0
- package/dist/clients.cjs.map +1 -1
- package/dist/clients.mjs +3 -0
- package/dist/clients.mjs.map +1 -1
- package/dist/hooks.cjs +3 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.mjs +3 -0
- package/dist/hooks.mjs.map +1 -1
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/_api/generated/helpers/auth.ts +6 -0
- package/src/auth/__tests__/signInRefusal.test.ts +52 -0
- package/src/auth/context/AuthContext.tsx +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/api",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.619",
|
|
4
4
|
"description": "Auto-generated TypeScript API client with React hooks, SWR integration, and Zod validation for Django REST Framework backends",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"django",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"zod": "^4.3.6"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
94
|
+
"@djangocfg/typescript-config": "^2.1.619",
|
|
95
95
|
"@testing-library/react": "^16.3.3",
|
|
96
96
|
"@types/node": "^25.9.5",
|
|
97
97
|
"@types/react": "19.3.0",
|
|
@@ -849,6 +849,12 @@ export function installAuthOnClient(client: HeyClient): void {
|
|
|
849
849
|
client.interceptors.response.use(async (response, request) => {
|
|
850
850
|
if (response.status !== 401) return response;
|
|
851
851
|
|
|
852
|
+
// A refusal from the sign-in endpoints themselves (wrong OTP / password, bad OAuth code) is a credential error, not
|
|
853
|
+
// a dead session: expiring here reloads the sign-in page and wipes the error the user is reading.
|
|
854
|
+
if (/\/accounts\/(otp|oauth|login|register|token)\b/.test(request.url)) {
|
|
855
|
+
return response;
|
|
856
|
+
}
|
|
857
|
+
|
|
852
858
|
// Already retried once — give up to avoid loops.
|
|
853
859
|
if (request.headers.get(RETRY_MARKER)) {
|
|
854
860
|
expireSession(response);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A 401 from a sign-in endpoint (wrong OTP / password, bad OAuth code) is not a dead session:
|
|
3
|
+
* it must not fire `onSessionExpired`, which hard-reloads the sign-in page and wipes the error the user is reading.
|
|
4
|
+
*/
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
function installBrowserStorage() {
|
|
8
|
+
const map = new Map<string, string>();
|
|
9
|
+
const storage = {
|
|
10
|
+
getItem: (k: string) => (map.has(k) ? map.get(k)! : null),
|
|
11
|
+
setItem: (k: string, v: string) => void map.set(k, v),
|
|
12
|
+
removeItem: (k: string) => void map.delete(k),
|
|
13
|
+
clear: () => map.clear(),
|
|
14
|
+
};
|
|
15
|
+
vi.stubGlobal('window', { localStorage: storage, location: { protocol: 'http:' } });
|
|
16
|
+
vi.stubGlobal('localStorage', storage);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function setup() {
|
|
20
|
+
installBrowserStorage();
|
|
21
|
+
const { auth, installAuthOnClient } = await import('../../_api/generated/helpers/auth');
|
|
22
|
+
const handlers: Array<(res: Response, req: Request) => Promise<Response>> = [];
|
|
23
|
+
const noop = { use: () => 0 };
|
|
24
|
+
installAuthOnClient({
|
|
25
|
+
setConfig: () => undefined,
|
|
26
|
+
interceptors: { request: noop, error: noop, response: { use: (fn: never) => handlers.push(fn) } },
|
|
27
|
+
} as never);
|
|
28
|
+
const expired = vi.fn();
|
|
29
|
+
auth.onSessionExpired(expired);
|
|
30
|
+
return { handler: handlers[handlers.length - 1]!, expired };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('401 that is not a session expiry', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.unstubAllGlobals();
|
|
36
|
+
vi.resetModules();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('wrong OTP: no session-expired event', async () => {
|
|
40
|
+
const { handler, expired } = await setup();
|
|
41
|
+
const req = new Request('https://api.example.com/cfg/accounts/otp/verify/', { method: 'POST', headers: { Authorization: 'Bearer stale' } });
|
|
42
|
+
await handler(new Response('{}', { status: 401 }), req);
|
|
43
|
+
expect(expired).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('signed-in request that cannot be refreshed still expires the session', async () => {
|
|
47
|
+
const { handler, expired } = await setup();
|
|
48
|
+
const req = new Request('https://api.example.com/apix/crm/me/');
|
|
49
|
+
await handler(new Response('{}', { status: 401 }), req);
|
|
50
|
+
expect(expired).toHaveBeenCalledTimes(1);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -152,6 +152,10 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
152
152
|
purgeLegacyTokenStorage();
|
|
153
153
|
if (accountsRef.current.profile) accountsRef.current.logout();
|
|
154
154
|
}
|
|
155
|
+
// The hydration commit serves the SERVER snapshot ('anonymous') even when the store holds a live session. Settling
|
|
156
|
+
// now would let the page guard read "loaded and signed out" and bounce to /auth; the snapshot flips on the next
|
|
157
|
+
// render and this effect runs again with the real value.
|
|
158
|
+
if (auth.isAuthenticated()) return;
|
|
155
159
|
setProfileSettled(true);
|
|
156
160
|
return;
|
|
157
161
|
}
|