@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/api",
3
- "version": "2.1.618",
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.618",
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
  }