@djangocfg/api 2.1.332 → 2.1.334
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-server.cjs +1148 -1039
- package/dist/auth-server.cjs.map +1 -1
- package/dist/auth-server.mjs +1148 -1039
- package/dist/auth-server.mjs.map +1 -1
- package/dist/auth.cjs +1157 -1048
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.mjs +1157 -1048
- package/dist/auth.mjs.map +1 -1
- package/dist/clients.cjs +60 -835
- package/dist/clients.cjs.map +1 -1
- package/dist/clients.d.cts +21 -0
- package/dist/clients.d.ts +21 -0
- package/dist/clients.mjs +60 -835
- package/dist/clients.mjs.map +1 -1
- package/dist/index.cjs +1180 -1071
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -18
- package/dist/index.d.ts +49 -18
- package/dist/index.mjs +1180 -1071
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/_api/generated/_cfg_accounts/api.ts +12 -0
- package/src/_api/generated/_cfg_centrifugo/api.ts +12 -0
- package/src/_api/generated/_cfg_totp/api.ts +12 -0
- package/src/_api/generated/client.gen.ts +3 -2
- package/src/_api/generated/helpers/auth.ts +161 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/api",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.334",
|
|
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",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@types/node": "^24.7.2",
|
|
81
81
|
"@types/react": "^19.1.0",
|
|
82
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
82
|
+
"@djangocfg/typescript-config": "^2.1.334",
|
|
83
83
|
"next": "^16.2.2",
|
|
84
84
|
"react": "^19.1.0",
|
|
85
85
|
"tsup": "^8.5.0",
|
|
@@ -61,6 +61,18 @@ export class API {
|
|
|
61
61
|
setLocale(locale: string | null): void { auth.setLocale(locale); }
|
|
62
62
|
getApiKey(): string | null { return auth.getApiKey(); }
|
|
63
63
|
setApiKey(key: string | null): void { auth.setApiKey(key); }
|
|
64
|
+
|
|
65
|
+
// ── 401 handling ────────────────────────────────────────────────────────
|
|
66
|
+
/** Fired only on terminal 401 (after refresh+retry path is exhausted). */
|
|
67
|
+
onUnauthorized(cb: ((response: Response) => void) | null): void {
|
|
68
|
+
auth.onUnauthorized(cb);
|
|
69
|
+
}
|
|
70
|
+
/** Provide a refresh strategy. See `auth.setRefreshHandler` for the contract. */
|
|
71
|
+
setRefreshHandler(
|
|
72
|
+
fn: ((refreshToken: string) => Promise<{ access: string; refresh?: string } | null>) | null,
|
|
73
|
+
): void {
|
|
74
|
+
auth.setRefreshHandler(fn);
|
|
75
|
+
}
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
export { };
|
|
@@ -61,6 +61,18 @@ export class API {
|
|
|
61
61
|
setLocale(locale: string | null): void { auth.setLocale(locale); }
|
|
62
62
|
getApiKey(): string | null { return auth.getApiKey(); }
|
|
63
63
|
setApiKey(key: string | null): void { auth.setApiKey(key); }
|
|
64
|
+
|
|
65
|
+
// ── 401 handling ────────────────────────────────────────────────────────
|
|
66
|
+
/** Fired only on terminal 401 (after refresh+retry path is exhausted). */
|
|
67
|
+
onUnauthorized(cb: ((response: Response) => void) | null): void {
|
|
68
|
+
auth.onUnauthorized(cb);
|
|
69
|
+
}
|
|
70
|
+
/** Provide a refresh strategy. See `auth.setRefreshHandler` for the contract. */
|
|
71
|
+
setRefreshHandler(
|
|
72
|
+
fn: ((refreshToken: string) => Promise<{ access: string; refresh?: string } | null>) | null,
|
|
73
|
+
): void {
|
|
74
|
+
auth.setRefreshHandler(fn);
|
|
75
|
+
}
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
export { };
|
|
@@ -61,6 +61,18 @@ export class API {
|
|
|
61
61
|
setLocale(locale: string | null): void { auth.setLocale(locale); }
|
|
62
62
|
getApiKey(): string | null { return auth.getApiKey(); }
|
|
63
63
|
setApiKey(key: string | null): void { auth.setApiKey(key); }
|
|
64
|
+
|
|
65
|
+
// ── 401 handling ────────────────────────────────────────────────────────
|
|
66
|
+
/** Fired only on terminal 401 (after refresh+retry path is exhausted). */
|
|
67
|
+
onUnauthorized(cb: ((response: Response) => void) | null): void {
|
|
68
|
+
auth.onUnauthorized(cb);
|
|
69
|
+
}
|
|
70
|
+
/** Provide a refresh strategy. See `auth.setRefreshHandler` for the contract. */
|
|
71
|
+
setRefreshHandler(
|
|
72
|
+
fn: ((refreshToken: string) => Promise<{ access: string; refresh?: string } | null>) | null,
|
|
73
|
+
): void {
|
|
74
|
+
auth.setRefreshHandler(fn);
|
|
75
|
+
}
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
export { };
|
|
@@ -15,5 +15,6 @@ export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (over
|
|
|
15
15
|
|
|
16
16
|
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: 'http://localhost:8000' }));
|
|
17
17
|
|
|
18
|
-
// auto-init:
|
|
19
|
-
import './helpers/auth';
|
|
18
|
+
// auto-init: install auth on client
|
|
19
|
+
import { installAuthOnClient } from './helpers/auth';
|
|
20
|
+
installAuthOnClient(client);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// AUTO-GENERATED by django_generator / ts_extras.wrapper
|
|
2
|
-
// Global auth store.
|
|
3
|
-
//
|
|
2
|
+
// Global auth store. Wired into the shared `client` from `client.gen.ts`
|
|
3
|
+
// via `installAuthOnClient(client)` — called synchronously by the
|
|
4
|
+
// post-processed bottom of client.gen.ts. No circular import here.
|
|
4
5
|
// DO NOT EDIT — re-run `make gen`.
|
|
5
6
|
|
|
6
|
-
import { client } from '../client.gen';
|
|
7
|
-
|
|
8
7
|
const ACCESS_KEY = 'cfg.access_token';
|
|
9
8
|
const REFRESH_KEY = 'cfg.refresh_token';
|
|
10
9
|
const API_KEY_KEY = 'cfg.api_key';
|
|
@@ -106,6 +105,45 @@ let _baseUrlOverride: string | null = null;
|
|
|
106
105
|
let _withCredentials = true;
|
|
107
106
|
let _onUnauthorized: ((response: Response) => void) | null = null;
|
|
108
107
|
|
|
108
|
+
/**
|
|
109
|
+
* User-supplied refresh handler. Receives the current refresh token,
|
|
110
|
+
* must return a fresh access (and optional refresh) pair or null on failure.
|
|
111
|
+
* Set once at app bootstrap via `auth.setRefreshHandler(...)`.
|
|
112
|
+
*/
|
|
113
|
+
type RefreshResult = { access: string; refresh?: string } | null;
|
|
114
|
+
type RefreshHandler = (refreshToken: string) => Promise<RefreshResult>;
|
|
115
|
+
let _refreshHandler: RefreshHandler | null = null;
|
|
116
|
+
|
|
117
|
+
/** Single-flight: every concurrent 401 awaits the same refresh. */
|
|
118
|
+
let _refreshInflight: Promise<string | null> | null = null;
|
|
119
|
+
|
|
120
|
+
/** Marker header — set on retried requests so we never loop on 401. */
|
|
121
|
+
const RETRY_MARKER = 'X-Auth-Retry';
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Captured reference to the shared Hey API client. Set exactly once by
|
|
125
|
+
* `installAuthOnClient(client)` (called from client.gen.ts). All `auth.set*`
|
|
126
|
+
* methods that mutate transport config (baseUrl / credentials) push through
|
|
127
|
+
* this reference. Until installed, those mutations are silently buffered as
|
|
128
|
+
* in-memory state — the next request after install will pick them up.
|
|
129
|
+
*/
|
|
130
|
+
type HeyClient = {
|
|
131
|
+
setConfig(opts: Record<string, unknown>): void;
|
|
132
|
+
interceptors: {
|
|
133
|
+
request: { use(fn: (req: Request) => Request | Promise<Request>): void };
|
|
134
|
+
response: { use(fn: (res: Response, req: Request) => Response | Promise<Response>): void };
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
let _client: HeyClient | null = null;
|
|
138
|
+
|
|
139
|
+
function pushClientConfig(): void {
|
|
140
|
+
if (!_client) return;
|
|
141
|
+
_client.setConfig({
|
|
142
|
+
baseUrl: auth.getBaseUrl(),
|
|
143
|
+
credentials: _withCredentials ? 'include' : 'same-origin',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
109
147
|
/**
|
|
110
148
|
* Global auth/config store. All getters read live state every call —
|
|
111
149
|
* the interceptor below uses these to attach headers per-request.
|
|
@@ -116,24 +154,13 @@ let _onUnauthorized: ((response: Response) => void) | null = null;
|
|
|
116
154
|
*
|
|
117
155
|
* @example
|
|
118
156
|
* import { auth } from '@your/api';
|
|
119
|
-
*
|
|
120
|
-
* // After login
|
|
121
157
|
* auth.setToken(jwt);
|
|
122
|
-
* auth.setRefreshToken(refresh);
|
|
123
|
-
*
|
|
124
|
-
* // After logout
|
|
125
158
|
* auth.clearTokens();
|
|
126
|
-
*
|
|
127
|
-
* // Switch to cookie storage (call once during app init)
|
|
128
159
|
* auth.setStorageMode('cookie');
|
|
129
160
|
*/
|
|
130
161
|
export const auth = {
|
|
131
162
|
// ── Storage mode ──────────────────────────────────────────────────
|
|
132
163
|
getStorageMode(): StorageMode { return _storageMode; },
|
|
133
|
-
/**
|
|
134
|
-
* Switch the storage backend. Existing values in the *previous*
|
|
135
|
-
* backend are NOT migrated — set fresh values after switching.
|
|
136
|
-
*/
|
|
137
164
|
setStorageMode(mode: StorageMode): void {
|
|
138
165
|
_storageMode = mode;
|
|
139
166
|
_storage = mode === 'cookie' ? cookieBackend : localStorageBackend;
|
|
@@ -148,13 +175,10 @@ export const auth = {
|
|
|
148
175
|
isAuthenticated(): boolean { return _storage.get(ACCESS_KEY) !== null; },
|
|
149
176
|
|
|
150
177
|
// ── API key ───────────────────────────────────────────────────────
|
|
151
|
-
/** In-memory API key. Falls back to storage, then NEXT_PUBLIC_API_KEY. */
|
|
152
178
|
getApiKey(): string | null {
|
|
153
179
|
return _apiKeyOverride ?? _storage.get(API_KEY_KEY) ?? defaultApiKey();
|
|
154
180
|
},
|
|
155
|
-
/** In-memory only (cleared on reload). */
|
|
156
181
|
setApiKey(key: string | null): void { _apiKeyOverride = key; },
|
|
157
|
-
/** Persist to active storage backend (localStorage or cookie). */
|
|
158
182
|
setApiKeyPersist(key: string | null): void {
|
|
159
183
|
_apiKeyOverride = key;
|
|
160
184
|
_storage.set(API_KEY_KEY, key);
|
|
@@ -162,7 +186,6 @@ export const auth = {
|
|
|
162
186
|
clearApiKey(): void { _apiKeyOverride = null; _storage.set(API_KEY_KEY, null); },
|
|
163
187
|
|
|
164
188
|
// ── Locale ────────────────────────────────────────────────────────
|
|
165
|
-
/** Override locale → falls back to NEXT_LOCALE cookie / navigator.language. */
|
|
166
189
|
getLocale(): string | null { return _localeOverride ?? detectLocale(); },
|
|
167
190
|
setLocale(locale: string | null): void { _localeOverride = locale; },
|
|
168
191
|
|
|
@@ -173,51 +196,144 @@ export const auth = {
|
|
|
173
196
|
},
|
|
174
197
|
setBaseUrl(url: string | null): void {
|
|
175
198
|
_baseUrlOverride = url ? url.replace(/\/$/, '') : null;
|
|
176
|
-
|
|
199
|
+
pushClientConfig();
|
|
177
200
|
},
|
|
178
201
|
|
|
179
|
-
// ── Credentials toggle
|
|
202
|
+
// ── Credentials toggle ────────────────────────────────────────────
|
|
180
203
|
getWithCredentials(): boolean { return _withCredentials; },
|
|
181
204
|
setWithCredentials(value: boolean): void {
|
|
182
205
|
_withCredentials = value;
|
|
183
|
-
|
|
206
|
+
pushClientConfig();
|
|
184
207
|
},
|
|
185
208
|
|
|
186
209
|
// ── 401 handler ───────────────────────────────────────────────────
|
|
187
210
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
211
|
+
* Fired when the server returns 401 AND no refresh path recovers it
|
|
212
|
+
* (no refresh token, no refresh handler, refresh failed, or retry
|
|
213
|
+
* still 401). The app should clear local state and redirect to login.
|
|
214
|
+
*
|
|
215
|
+
* NOT fired for 401 that gets transparently recovered by the refresh
|
|
216
|
+
* handler — those are invisible to callers.
|
|
191
217
|
*/
|
|
192
218
|
onUnauthorized(cb: ((response: Response) => void) | null): void {
|
|
193
219
|
_onUnauthorized = cb;
|
|
194
220
|
},
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Register the refresh strategy. The handler receives the current
|
|
224
|
+
* refresh token and must call your refresh endpoint, returning
|
|
225
|
+
* `{ access, refresh? }` on success or `null` on failure.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* auth.setRefreshHandler(async (refresh) => {
|
|
229
|
+
* const { data } = await Auth.tokenRefreshCreate({ body: { refresh } });
|
|
230
|
+
* return data ? { access: data.access, refresh: data.refresh } : null;
|
|
231
|
+
* });
|
|
232
|
+
*/
|
|
233
|
+
setRefreshHandler(fn: RefreshHandler | null): void {
|
|
234
|
+
_refreshHandler = fn;
|
|
235
|
+
},
|
|
195
236
|
};
|
|
196
237
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Run the user-supplied refresh handler under single-flight, persist
|
|
240
|
+
* the new tokens, and return the fresh access token (or null on any
|
|
241
|
+
* failure path). All concurrent 401s share the same in-flight promise.
|
|
242
|
+
*/
|
|
243
|
+
async function tryRefresh(): Promise<string | null> {
|
|
244
|
+
if (_refreshInflight) return _refreshInflight;
|
|
245
|
+
if (!_refreshHandler) return null;
|
|
246
|
+
const refresh = auth.getRefreshToken();
|
|
247
|
+
if (!refresh) return null;
|
|
202
248
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
249
|
+
_refreshInflight = (async () => {
|
|
250
|
+
try {
|
|
251
|
+
const result = await _refreshHandler!(refresh);
|
|
252
|
+
if (!result?.access) return null;
|
|
253
|
+
auth.setToken(result.access);
|
|
254
|
+
if (result.refresh) auth.setRefreshToken(result.refresh);
|
|
255
|
+
return result.access;
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
} finally {
|
|
259
|
+
_refreshInflight = null;
|
|
260
|
+
}
|
|
261
|
+
})();
|
|
262
|
+
|
|
263
|
+
return _refreshInflight;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Wire the shared client to the global auth store. Called exactly
|
|
268
|
+
* once from `client.gen.ts` (post-processed) right after
|
|
269
|
+
* `createClient()`. Synchronous — no microtask, no TDZ races.
|
|
270
|
+
*
|
|
271
|
+
* Safe to call from server / SSR: storage backends short-circuit on
|
|
272
|
+
* non-browser environments, so headers populated by the interceptor
|
|
273
|
+
* are simply absent server-side (which is the correct behaviour
|
|
274
|
+
* unless the caller explicitly sets a server-side token).
|
|
275
|
+
*/
|
|
276
|
+
export function installAuthOnClient(client: HeyClient): void {
|
|
277
|
+
if (_client) return; // idempotent
|
|
278
|
+
_client = client;
|
|
279
|
+
|
|
280
|
+
client.setConfig({
|
|
281
|
+
baseUrl: auth.getBaseUrl(),
|
|
282
|
+
credentials: _withCredentials ? 'include' : 'same-origin',
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
client.interceptors.request.use((request) => {
|
|
286
|
+
const token = auth.getToken();
|
|
287
|
+
if (token) request.headers.set('Authorization', `Bearer ${token}`);
|
|
288
|
+
|
|
289
|
+
const locale = auth.getLocale();
|
|
290
|
+
if (locale) request.headers.set('Accept-Language', locale);
|
|
206
291
|
|
|
207
|
-
|
|
208
|
-
|
|
292
|
+
const apiKey = auth.getApiKey();
|
|
293
|
+
if (apiKey) request.headers.set('X-API-Key', apiKey);
|
|
209
294
|
|
|
210
|
-
|
|
211
|
-
|
|
295
|
+
return request;
|
|
296
|
+
});
|
|
212
297
|
|
|
213
|
-
|
|
214
|
-
|
|
298
|
+
client.interceptors.response.use(async (response, request) => {
|
|
299
|
+
if (response.status !== 401) return response;
|
|
215
300
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
301
|
+
// Already retried once — give up to avoid loops.
|
|
302
|
+
if (request.headers.get(RETRY_MARKER)) {
|
|
303
|
+
if (_onUnauthorized) {
|
|
304
|
+
try { _onUnauthorized(response); } catch {}
|
|
305
|
+
}
|
|
306
|
+
return response;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const newToken = await tryRefresh();
|
|
310
|
+
if (!newToken) {
|
|
311
|
+
if (_onUnauthorized) {
|
|
312
|
+
try { _onUnauthorized(response); } catch {}
|
|
313
|
+
}
|
|
314
|
+
return response;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Retry the original request once with the new token. We mutate a
|
|
318
|
+
// clone so the original Request body (already consumed by the
|
|
319
|
+
// failed call) doesn't trip "body already used".
|
|
320
|
+
const retry = request.clone();
|
|
321
|
+
retry.headers.set('Authorization', `Bearer ${newToken}`);
|
|
322
|
+
retry.headers.set(RETRY_MARKER, '1');
|
|
323
|
+
try {
|
|
324
|
+
const retried = await fetch(retry);
|
|
325
|
+
if (retried.status === 401 && _onUnauthorized) {
|
|
326
|
+
try { _onUnauthorized(retried); } catch {}
|
|
327
|
+
}
|
|
328
|
+
return retried;
|
|
329
|
+
} catch {
|
|
330
|
+
// Network error on retry — surface the original 401.
|
|
331
|
+
if (_onUnauthorized) {
|
|
332
|
+
try { _onUnauthorized(response); } catch {}
|
|
333
|
+
}
|
|
334
|
+
return response;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
222
338
|
|
|
223
339
|
export type Auth = typeof auth;
|