@djangocfg/api 2.1.455 → 2.1.456

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.
Files changed (87) hide show
  1. package/dist/auth.cjs +2654 -2920
  2. package/dist/auth.cjs.map +1 -1
  3. package/dist/auth.d.cts +18 -82
  4. package/dist/auth.d.ts +18 -82
  5. package/dist/auth.mjs +385 -2861
  6. package/dist/auth.mjs.map +1 -1
  7. package/dist/chunk-2G67QRNU.mjs +2096 -0
  8. package/dist/chunk-2G67QRNU.mjs.map +1 -0
  9. package/dist/chunk-32SRQGAC.mjs +2109 -0
  10. package/dist/chunk-32SRQGAC.mjs.map +1 -0
  11. package/dist/chunk-4BPRCONN.mjs +2098 -0
  12. package/dist/chunk-4BPRCONN.mjs.map +1 -0
  13. package/dist/chunk-5UZ2Z323.mjs +2021 -0
  14. package/dist/chunk-5UZ2Z323.mjs.map +1 -0
  15. package/dist/chunk-7KFTJXNM.mjs +2097 -0
  16. package/dist/chunk-7KFTJXNM.mjs.map +1 -0
  17. package/dist/chunk-BK4K5CVT.mjs +2019 -0
  18. package/dist/chunk-BK4K5CVT.mjs.map +1 -0
  19. package/dist/chunk-JLPJZ6WB.mjs +2020 -0
  20. package/dist/chunk-JLPJZ6WB.mjs.map +1 -0
  21. package/dist/chunk-TVU6PYJH.mjs +2032 -0
  22. package/dist/chunk-TVU6PYJH.mjs.map +1 -0
  23. package/dist/clients.cjs +2210 -1847
  24. package/dist/clients.cjs.map +1 -1
  25. package/dist/clients.d.cts +23 -0
  26. package/dist/clients.d.ts +23 -0
  27. package/dist/clients.mjs +14 -1845
  28. package/dist/clients.mjs.map +1 -1
  29. package/dist/hooks.cjs +1776 -962
  30. package/dist/hooks.cjs.map +1 -1
  31. package/dist/hooks.mjs +4 -1375
  32. package/dist/hooks.mjs.map +1 -1
  33. package/dist/index.cjs +2245 -1877
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +81 -4
  36. package/dist/index.d.ts +81 -4
  37. package/dist/index.mjs +20 -1860
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/sdk.gen-2SQOPTWJ.mjs +26 -0
  40. package/dist/sdk.gen-2SQOPTWJ.mjs.map +1 -0
  41. package/dist/sdk.gen-HSGK4C5S.mjs +26 -0
  42. package/dist/sdk.gen-HSGK4C5S.mjs.map +1 -0
  43. package/dist/sdk.gen-NUK2VGHO.mjs +25 -0
  44. package/dist/sdk.gen-NUK2VGHO.mjs.map +1 -0
  45. package/dist/sdk.gen-O4KAQUZB.mjs +26 -0
  46. package/dist/sdk.gen-O4KAQUZB.mjs.map +1 -0
  47. package/dist/sdk.gen-PPAVSBNT.mjs +26 -0
  48. package/dist/sdk.gen-PPAVSBNT.mjs.map +1 -0
  49. package/dist/sdk.gen-XLHLOCJ2.mjs +25 -0
  50. package/dist/sdk.gen-XLHLOCJ2.mjs.map +1 -0
  51. package/dist/sdk.gen-ZOE6NQAL.mjs +26 -0
  52. package/dist/sdk.gen-ZOE6NQAL.mjs.map +1 -0
  53. package/dist/sdk.gen-ZT7LGJVO.mjs +26 -0
  54. package/dist/sdk.gen-ZT7LGJVO.mjs.map +1 -0
  55. package/package.json +2 -2
  56. package/src/_api/generated/_cfg_accounts/hooks/index.ts +1 -0
  57. package/src/_api/generated/_cfg_accounts/hooks/useCfgAccountsTokenBlacklistCreate.ts +28 -0
  58. package/src/_api/generated/_cfg_accounts/openapi.json +99 -0
  59. package/src/_api/generated/_cfg_accounts/schemas/TokenBlacklistRequest.ts +11 -0
  60. package/src/_api/generated/_cfg_accounts/schemas/index.ts +1 -0
  61. package/src/_api/generated/_cfg_centrifugo/openapi.json +9 -0
  62. package/src/_api/generated/_cfg_totp/openapi.json +33 -0
  63. package/src/_api/generated/helpers/auth.ts +276 -31
  64. package/src/_api/generated/openapi.json +129 -0
  65. package/src/_api/generated/sdk.gen.ts +94 -16
  66. package/src/_api/generated/types.gen.ts +18 -0
  67. package/src/auth/__tests__/guard.test.ts +0 -31
  68. package/src/auth/constants.ts +6 -0
  69. package/src/auth/context/AccountsContext.tsx +14 -24
  70. package/src/auth/context/AuthContext.tsx +226 -609
  71. package/src/auth/hooks/index.ts +3 -4
  72. package/src/auth/hooks/useGithubAuth.ts +3 -3
  73. package/src/auth/hooks/useSession.ts +22 -0
  74. package/src/auth/hooks/useTwoFactor.ts +4 -4
  75. package/src/auth/middlewares/index.ts +5 -6
  76. package/src/auth/utils/guard.ts +0 -22
  77. package/src/auth/utils/index.ts +0 -2
  78. package/src/_api/generated/_cfg_accounts/events.ts +0 -198
  79. package/src/_api/generated/_cfg_centrifugo/events.ts +0 -198
  80. package/src/_api/generated/_cfg_totp/events.ts +0 -198
  81. package/src/auth/__tests__/jwt.test.ts +0 -119
  82. package/src/auth/__tests__/sessionBootstrap.test.ts +0 -111
  83. package/src/auth/__tests__/useTokenRefresh.dom.test.tsx +0 -167
  84. package/src/auth/hooks/useAuthGuard.ts +0 -35
  85. package/src/auth/hooks/useTokenRefresh.ts +0 -131
  86. package/src/auth/refreshHandler.ts +0 -79
  87. package/src/auth/utils/jwt.ts +0 -66
@@ -1,131 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * Token Refresh Hook (proactive trigger only)
5
- *
6
- * Refreshes the access token BEFORE it expires, and on window focus / network
7
- * reconnect. This hook deliberately owns NO refresh logic of its own — it only
8
- * decides WHEN to refresh and delegates HOW to the canonical path
9
- * (`triggerRefresh` → `api.refreshNow` → the store's single-flight
10
- * `tryRefresh`). That path handles token rotation, concurrent de-duplication,
11
- * and persisting the rotated refresh token.
12
- *
13
- * Historical note: this hook (and a now-removed fetch middleware) used to run
14
- * their own refresh calls and persisted the OLD refresh token instead of the
15
- * rotated one the server returned. With Django's default
16
- * ROTATE_REFRESH_TOKENS + BLACKLIST_AFTER_ROTATION that produced a
17
- * "Token is blacklisted" 401 on the following refresh and logged users out.
18
- * Funnelling every refresh through one implementation removes that whole class
19
- * of bug — see auth/refreshHandler.ts.
20
- */
21
-
22
- import { useCallback, useEffect } from 'react';
23
-
24
- import { api as apiAccounts } from '../../';
25
- import { triggerRefresh } from '../refreshHandler';
26
- import { authLogger } from '../utils/logger';
27
- import { isTokenExpiringSoon } from '../utils/jwt';
28
-
29
- // Configuration
30
- const TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes before expiry
31
- const CHECK_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes
32
-
33
- interface UseTokenRefreshOptions {
34
- /** Enable automatic token refresh (default: true) */
35
- enabled?: boolean;
36
- /** Callback when token is refreshed */
37
- onRefresh?: (newToken: string) => void;
38
- /** Callback when refresh fails */
39
- onRefreshError?: (error: Error) => void;
40
- }
41
-
42
- /**
43
- * Hook for proactive automatic token refresh.
44
- * (JWT expiry helpers live in ../utils/jwt so they can be unit-tested and
45
- * reused by the auth-bootstrap validation path.)
46
- */
47
- export function useTokenRefresh(options: UseTokenRefreshOptions = {}) {
48
- const { enabled = true, onRefresh, onRefreshError } = options;
49
-
50
- /**
51
- * Refresh the token via the canonical single-flight path. Returns true on
52
- * success. No local in-flight guard needed — `triggerRefresh` shares the
53
- * store's single-flight promise, so overlapping calls coalesce.
54
- */
55
- const refreshToken = useCallback(async (): Promise<boolean> => {
56
- if (!apiAccounts.getRefreshToken()) {
57
- authLogger.warn('No refresh token available');
58
- return false;
59
- }
60
-
61
- authLogger.info('Refreshing token...');
62
- try {
63
- const newAccessToken = await triggerRefresh();
64
- if (!newAccessToken) {
65
- throw new Error('Token refresh failed');
66
- }
67
- authLogger.info('Token refreshed successfully');
68
- onRefresh?.(newAccessToken);
69
- return true;
70
- } catch (error) {
71
- authLogger.error('Token refresh error:', error);
72
- onRefreshError?.(error instanceof Error ? error : new Error(String(error)));
73
- return false;
74
- }
75
- }, [onRefresh, onRefreshError]);
76
-
77
- /**
78
- * Check and refresh if the current token is expiring soon.
79
- */
80
- const checkAndRefresh = useCallback(async () => {
81
- const token = apiAccounts.getToken();
82
- if (!token) return;
83
-
84
- if (isTokenExpiringSoon(token, TOKEN_REFRESH_THRESHOLD_MS)) {
85
- authLogger.info('Token expiring soon, refreshing proactively');
86
- await refreshToken();
87
- }
88
- }, [refreshToken]);
89
-
90
- // Periodic check
91
- useEffect(() => {
92
- if (!enabled) return;
93
-
94
- checkAndRefresh();
95
- const intervalId = setInterval(checkAndRefresh, CHECK_INTERVAL_MS);
96
- return () => clearInterval(intervalId);
97
- }, [enabled, checkAndRefresh]);
98
-
99
- // Refresh on window focus
100
- useEffect(() => {
101
- if (!enabled) return;
102
-
103
- const handleFocus = () => {
104
- authLogger.debug('Window focused, checking token...');
105
- checkAndRefresh();
106
- };
107
-
108
- window.addEventListener('focus', handleFocus);
109
- return () => window.removeEventListener('focus', handleFocus);
110
- }, [enabled, checkAndRefresh]);
111
-
112
- // Refresh on network reconnect
113
- useEffect(() => {
114
- if (!enabled) return;
115
-
116
- const handleOnline = () => {
117
- authLogger.info('Network reconnected, checking token...');
118
- checkAndRefresh();
119
- };
120
-
121
- window.addEventListener('online', handleOnline);
122
- return () => window.removeEventListener('online', handleOnline);
123
- }, [enabled, checkAndRefresh]);
124
-
125
- return {
126
- refreshToken,
127
- checkAndRefresh,
128
- };
129
- }
130
-
131
- export default useTokenRefresh;
@@ -1,79 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * Canonical refresh strategy — registered ONCE, used by every refresh path.
5
- *
6
- * The generated client (`installAuthOnClient` in helpers/auth.ts) already owns
7
- * the hard parts of token refresh: single-flight de-duplication of concurrent
8
- * 401s, one automatic retry of the failed request, and — crucially —
9
- * persisting the ROTATED refresh token the server returns. It only needs a
10
- * strategy telling it HOW to call the refresh endpoint. That strategy is here.
11
- *
12
- * Why this matters: Django (SimpleJWT via django-cfg) ships with
13
- * `ROTATE_REFRESH_TOKENS=True` + `BLACKLIST_AFTER_ROTATION=True` by default.
14
- * Every successful refresh returns a NEW refresh token and blacklists the old
15
- * one. The handler MUST return `{ access, refresh }` so the store persists the
16
- * rotated token; reusing the old one yields a "Token is blacklisted" 401 on the
17
- * next refresh. The canonical store (auth.tryRefresh) does the persistence — we
18
- * only surface the rotated token to it.
19
- *
20
- * This is the single source of truth. Proactive refresh (useTokenRefresh) and
21
- * reactive 401 recovery (the response interceptor) both funnel through it via
22
- * `api.refreshNow()` / `tryRefresh()`. Do NOT re-implement refresh anywhere
23
- * else.
24
- */
25
-
26
- import { api as apiAccounts } from '../';
27
- import { CfgAccountsAuth } from '../_api/generated/sdk.gen';
28
- import { authLogger } from './utils/logger';
29
-
30
- let _registered = false;
31
-
32
- /**
33
- * Register the canonical refresh handler on the shared auth store. Idempotent:
34
- * safe to call from every AuthProvider mount; only the first call wires it.
35
- */
36
- export function ensureRefreshHandler(): void {
37
- if (_registered) return;
38
- _registered = true;
39
-
40
- apiAccounts.setRefreshHandler(async (refresh: string) => {
41
- const result = await CfgAccountsAuth.cfgAccountsTokenRefreshCreate({
42
- body: { refresh },
43
- throwOnError: true,
44
- });
45
-
46
- const access = result.data?.access;
47
- if (!access) {
48
- authLogger.error('Refresh response missing access token');
49
- return null;
50
- }
51
-
52
- // Return the ROTATED refresh token so the store persists it. Fall back to
53
- // the current one only if the server omits it (rotation disabled).
54
- return { access, refresh: result.data?.refresh ?? refresh };
55
- });
56
-
57
- authLogger.debug('Canonical refresh handler registered');
58
- }
59
-
60
- /**
61
- * Proactively run the canonical refresh now, sharing the SAME single-flight as
62
- * 401 recovery (rotation + dedup + rotated-token persistence handled by the
63
- * store). Ensures the handler is registered first. Returns the fresh access
64
- * token, or null if refresh failed / no refresh token / no handler.
65
- *
66
- * Use this for expiry-timer / focus / reconnect triggers — never re-implement
67
- * the refresh call itself.
68
- */
69
- export function triggerRefresh(): Promise<string | null> {
70
- ensureRefreshHandler();
71
- // `refreshNow` is provided by the generated auth facade (helpers/auth.ts +
72
- // per-group api.ts). Guarded so this compiles even before regeneration.
73
- const maybe = apiAccounts as unknown as { refreshNow?: () => Promise<string | null> };
74
- if (typeof maybe.refreshNow === 'function') {
75
- return maybe.refreshNow();
76
- }
77
- authLogger.warn('api.refreshNow() unavailable — run `make generate` to regenerate the client');
78
- return Promise.resolve(null);
79
- }
@@ -1,66 +0,0 @@
1
- /**
2
- * JWT inspection helpers (browser-safe, dependency-free).
3
- *
4
- * These decode ONLY the `exp` claim to reason about token freshness on the
5
- * client. They do NOT verify the signature — that is the server's job. The
6
- * point is purely to avoid firing doomed requests with a token we can already
7
- * see is malformed or expired, and to let auth bootstrap collapse a dead
8
- * session to the login form instead of hanging on a preloader.
9
- *
10
- * A JWT that cannot be decoded is treated as EXPIRED/invalid (fail-closed):
11
- * better to re-authenticate than to loop on 401s with garbage in storage.
12
- */
13
-
14
- /** Base64url-decode a JWT segment. Returns null on any malformation. */
15
- function decodeSegment(segment: string): unknown {
16
- try {
17
- const base64 = segment.replace(/-/g, '+').replace(/_/g, '/');
18
- // atob is defined in browsers and modern Node (global). Guard anyway.
19
- if (typeof atob !== 'function') return null;
20
- const json = atob(base64);
21
- return JSON.parse(json);
22
- } catch {
23
- return null;
24
- }
25
- }
26
-
27
- /**
28
- * Extract the `exp` claim (epoch ms), or null if the token is undecodable or
29
- * has no numeric `exp`.
30
- */
31
- export function getTokenExpiry(token: string | null | undefined): number | null {
32
- if (!token || typeof token !== 'string') return null;
33
- const parts = token.split('.');
34
- if (parts.length !== 3) return null; // not a well-formed JWT
35
- const payload = decodeSegment(parts[1]);
36
- if (!payload || typeof payload !== 'object') return null;
37
- const exp = (payload as { exp?: unknown }).exp;
38
- if (typeof exp !== 'number' || !Number.isFinite(exp)) return null;
39
- return exp * 1000;
40
- }
41
-
42
- /**
43
- * True when the token is missing, malformed, or already past its `exp`.
44
- * `skewMs` treats a token expiring within the skew window as already expired
45
- * (default 0). Fail-closed: an undecodable token is considered expired.
46
- */
47
- export function isTokenExpired(token: string | null | undefined, skewMs = 0, now = Date.now()): boolean {
48
- const expiry = getTokenExpiry(token);
49
- if (expiry === null) return true; // undecodable / no exp → treat as dead
50
- return expiry - skewMs <= now;
51
- }
52
-
53
- /**
54
- * True when a valid token exists but expires within `thresholdMs` (used by the
55
- * proactive refresher). A missing/undecodable token returns false here — that
56
- * case is handled by the "expired" path, not the "expiring soon" path.
57
- */
58
- export function isTokenExpiringSoon(
59
- token: string | null | undefined,
60
- thresholdMs: number,
61
- now = Date.now(),
62
- ): boolean {
63
- const expiry = getTokenExpiry(token);
64
- if (expiry === null) return false;
65
- return expiry - now < thresholdMs;
66
- }