@djangocfg/api 2.1.581 → 2.1.583
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 +18 -0
- package/dist/auth.cjs +272 -80
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.mjs +272 -80
- package/dist/auth.mjs.map +1 -1
- package/dist/clients.cjs +243 -54
- package/dist/clients.cjs.map +1 -1
- package/dist/clients.mjs +243 -54
- package/dist/clients.mjs.map +1 -1
- package/dist/hooks.cjs +243 -54
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.mjs +243 -54
- package/dist/hooks.mjs.map +1 -1
- package/dist/index.cjs +248 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -1
- package/dist/index.d.ts +85 -1
- package/dist/index.mjs +248 -54
- package/dist/index.mjs.map +1 -1
- package/dist/storage.cjs +269 -0
- package/dist/storage.cjs.map +1 -0
- package/dist/storage.d.cts +85 -0
- package/dist/storage.d.ts +85 -0
- package/dist/storage.mjs +248 -0
- package/dist/storage.mjs.map +1 -0
- package/package.json +8 -2
- package/src/__tests__/noStorageLiterals.test.ts +27 -0
- package/src/__tests__/storage.test.ts +134 -0
- package/src/__tests__/storageSsr.test.ts +14 -0
- package/src/_api/generated/helpers/auth.ts +27 -64
- package/src/_api/generated/helpers/index.ts +12 -0
- package/src/_api/generated/helpers/keys.ts +328 -0
- package/src/auth/__tests__/authRedirect.test.ts +1 -1
- package/src/auth/__tests__/profileCache.test.ts +1 -1
- package/src/auth/context/AuthContext.tsx +2 -1
- package/src/auth/hooks/useAuthRedirect.ts +3 -2
- package/src/auth/hooks/useGithubAuth.ts +13 -8
- package/src/auth/hooks/useLocalStorage.ts +7 -7
- package/src/auth/hooks/useProfileCache.ts +2 -1
- package/src/auth/hooks/useSessionStorage.ts +7 -7
- package/src/storage.ts +21 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// AUTO-GENERATED by django_generator / ts_extras.wrapper
|
|
2
|
+
// Browser-storage key registry: one scheme, guarded access.
|
|
3
|
+
// DO NOT EDIT — re-run `make gen`.
|
|
4
|
+
//
|
|
5
|
+
// Rules for agents and apps: @docs/STORAGE_KEYS.md in @djangocfg/api.
|
|
6
|
+
//
|
|
7
|
+
// Scheme: `<owner>.<area>.<name>` — lower case, dots between levels,
|
|
8
|
+
// `snake_case` inside a level. The framework's owner is `cfg`; an app picks its
|
|
9
|
+
// own with `createStorage('<owner>')`. Third-party keys (a library's own
|
|
10
|
+
// prefix, `NEXT_LOCALE`) are not ours to rename and stay as they are.
|
|
11
|
+
|
|
12
|
+
export type KeyStore = 'local' | 'session' | 'cookie';
|
|
13
|
+
|
|
14
|
+
export interface KeySpec {
|
|
15
|
+
/** The canonical key. */
|
|
16
|
+
readonly key: string;
|
|
17
|
+
readonly store: KeyStore;
|
|
18
|
+
/** Removed by `clearAll()` (sign-out). Preferences are kept. */
|
|
19
|
+
readonly personal: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FamilySpec {
|
|
23
|
+
/** Canonical prefix ending in a dot; the id is appended in normalised form. */
|
|
24
|
+
readonly prefix: string;
|
|
25
|
+
readonly store: 'local' | 'session';
|
|
26
|
+
readonly personal: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const k = (
|
|
30
|
+
key: string,
|
|
31
|
+
store: KeyStore,
|
|
32
|
+
personal: boolean,
|
|
33
|
+
): KeySpec => ({ key, store, personal });
|
|
34
|
+
|
|
35
|
+
const f = (
|
|
36
|
+
prefix: string,
|
|
37
|
+
store: 'local' | 'session',
|
|
38
|
+
personal: boolean,
|
|
39
|
+
): FamilySpec => ({ prefix, store, personal });
|
|
40
|
+
|
|
41
|
+
/** Every key the framework writes. No legacy spellings are read: old keys are simply ignored. */
|
|
42
|
+
export const CFG_KEYS = {
|
|
43
|
+
accessToken: k('cfg.auth.access_token', 'local', true),
|
|
44
|
+
refreshToken: k('cfg.auth.refresh_token', 'local', true),
|
|
45
|
+
apiKey: k('cfg.auth.api_key', 'local', true),
|
|
46
|
+
email: k('cfg.auth.email', 'local', false),
|
|
47
|
+
emailTs: k('cfg.auth.email_ts', 'local', false),
|
|
48
|
+
redirectUrl: k('cfg.auth.redirect_url', 'session', true),
|
|
49
|
+
redirectUrlTs: k('cfg.auth.redirect_url_ts', 'session', true),
|
|
50
|
+
oauthState: k('cfg.auth.oauth_state', 'session', true),
|
|
51
|
+
oauthProvider: k('cfg.auth.oauth_provider', 'session', true),
|
|
52
|
+
oauthRememberMe: k('cfg.auth.oauth_remember_me', 'session', true),
|
|
53
|
+
profileCache: k('cfg.profile.cache', 'local', true),
|
|
54
|
+
monitorSessionId: k('cfg.monitor.session_id', 'local', false),
|
|
55
|
+
monitorSessionCookie: k('cfg.monitor.session_id', 'cookie', false),
|
|
56
|
+
monitorDebug: k('cfg.monitor.debug', 'local', false),
|
|
57
|
+
uiTabId: k('cfg.ui.tab_id', 'session', false),
|
|
58
|
+
uiRouterSerial: k('cfg.ui.router_serial', 'session', false),
|
|
59
|
+
uiState: k('cfg.ui.state', 'local', false),
|
|
60
|
+
chatSpeechLanguage: k('cfg.chat.speech_language', 'local', false),
|
|
61
|
+
chatRecentTools: k('cfg.chat.recent_tools', 'local', false),
|
|
62
|
+
formsSpeechDebug: k('cfg.forms.speech_debug', 'local', false),
|
|
63
|
+
formsSpeechPrefs: k('cfg.forms.speech_prefs', 'local', false),
|
|
64
|
+
agentPlaygroundApiKeyId: k('cfg.agent.playground_api_key_id', 'session', true),
|
|
65
|
+
agentPlaygroundBearer: k('cfg.agent.playground_bearer', 'session', true),
|
|
66
|
+
agentEndpointDoc: k('cfg.agent.endpoint_doc', 'session', false),
|
|
67
|
+
} as const satisfies Record<string, KeySpec>;
|
|
68
|
+
|
|
69
|
+
/** Keys with a caller-supplied id part: one per room, tree, endpoint. */
|
|
70
|
+
export const CFG_FAMILIES = {
|
|
71
|
+
chatDraft: f('cfg.chat.draft.', 'session', true),
|
|
72
|
+
dataTree: f('cfg.data.tree.', 'local', false),
|
|
73
|
+
agentEndpointDraft: f('cfg.agent.endpoint_draft.', 'local', false),
|
|
74
|
+
} as const satisfies Record<string, FamilySpec>;
|
|
75
|
+
|
|
76
|
+
const OWNER_RE = /^[a-z][a-z0-9_]*$/;
|
|
77
|
+
|
|
78
|
+
/** One segment of a key: lower case, `[a-z0-9_]`, anything else folded to `_`. */
|
|
79
|
+
export function normalizeSegment(raw: string): string {
|
|
80
|
+
return raw
|
|
81
|
+
.trim()
|
|
82
|
+
.toLowerCase()
|
|
83
|
+
.replace(/[^a-z0-9_]+/g, '_')
|
|
84
|
+
.replace(/^_+|_+$/g, '');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface StorageOptions {
|
|
88
|
+
/** Named keys of this owner. */
|
|
89
|
+
readonly keys?: Readonly<Record<string, KeySpec>>;
|
|
90
|
+
readonly families?: Readonly<Record<string, FamilySpec>>;
|
|
91
|
+
/** Cookie lifetime for the `cookie` store. Default 30 days. */
|
|
92
|
+
readonly cookieMaxAgeSeconds?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface GuardedStorage {
|
|
96
|
+
readonly owner: string;
|
|
97
|
+
/** Builds `<owner>.<part>.<part>` from free-form parts; throws if a part is empty. */
|
|
98
|
+
key(...parts: string[]): string;
|
|
99
|
+
/** Builds a family member key: the prefix plus the normalised id. */
|
|
100
|
+
familyKey(family: FamilySpec, id: string): string;
|
|
101
|
+
/** `^(cfg|<owner>)(\.[a-z0-9_]+)+$`. */
|
|
102
|
+
isValid(key: string): boolean;
|
|
103
|
+
read(key: string, store?: KeyStore): string | null;
|
|
104
|
+
/** True when the value reached the browser's storage; false means it is held in memory for this page only. */
|
|
105
|
+
write(key: string, value: string, store?: KeyStore): boolean;
|
|
106
|
+
/** Removes the key. */
|
|
107
|
+
remove(key: string, store?: KeyStore): void;
|
|
108
|
+
readJSON<T>(key: string, store?: KeyStore): T | null;
|
|
109
|
+
writeJSON(key: string, value: unknown, store?: KeyStore): boolean;
|
|
110
|
+
/** Keys this owner holds in `store`. */
|
|
111
|
+
keys(store?: Exclude<KeyStore, 'cookie'>): string[];
|
|
112
|
+
/** Sign-out: removes the `personal` keys. `{ all: true }` removes everything under this owner. */
|
|
113
|
+
clearAll(options?: { all?: boolean }): void;
|
|
114
|
+
/** Cross-tab: calls `listener` with the new value when ANOTHER tab changes `key`. Cookies fire no events. */
|
|
115
|
+
onChange(key: string, listener: (value: string | null) => void, store?: Exclude<KeyStore, 'cookie'>): () => void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const hasWindow = () => typeof window !== 'undefined';
|
|
119
|
+
|
|
120
|
+
function nativeStorage(store: 'local' | 'session'): Storage | null {
|
|
121
|
+
if (!hasWindow()) return null;
|
|
122
|
+
try {
|
|
123
|
+
return store === 'session' ? window.sessionStorage : window.localStorage;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function cookieNames(): string[] {
|
|
130
|
+
if (!hasWindow()) return [];
|
|
131
|
+
try {
|
|
132
|
+
return document.cookie
|
|
133
|
+
.split('; ')
|
|
134
|
+
.map((part) => decodeURIComponent(part.split('=')[0] ?? ''))
|
|
135
|
+
.filter(Boolean);
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function createStorage(owner: string, options: StorageOptions = {}): GuardedStorage {
|
|
142
|
+
if (!OWNER_RE.test(owner)) {
|
|
143
|
+
throw new TypeError(`createStorage: owner "${owner}" must match ${OWNER_RE}`);
|
|
144
|
+
}
|
|
145
|
+
const prefix = `${owner}.`;
|
|
146
|
+
const pattern = owner === 'cfg' ? /^cfg(\.[a-z0-9_]+)+$/ : new RegExp(`^(cfg|${owner})(\\.[a-z0-9_]+)+$`);
|
|
147
|
+
const cookieMaxAge = options.cookieMaxAgeSeconds ?? 60 * 60 * 24 * 30;
|
|
148
|
+
const specs = Object.values(options.keys ?? {});
|
|
149
|
+
const families = Object.values(options.families ?? {});
|
|
150
|
+
const specByKey = new Map<string, KeySpec[]>();
|
|
151
|
+
for (const spec of specs) specByKey.set(spec.key, [...(specByKey.get(spec.key) ?? []), spec]);
|
|
152
|
+
// Held in memory when the browser refuses a write (quota, private mode), so the page still works until reload.
|
|
153
|
+
const memory: Record<KeyStore, Map<string, string>> = {
|
|
154
|
+
local: new Map(),
|
|
155
|
+
session: new Map(),
|
|
156
|
+
cookie: new Map(),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const isValid = (key: string) => pattern.test(key);
|
|
160
|
+
const assertValid = (key: string) => {
|
|
161
|
+
if (!isValid(key)) throw new TypeError(`storage key "${key}" must match ${pattern}`);
|
|
162
|
+
};
|
|
163
|
+
const assertOwned = (key: string) => {
|
|
164
|
+
assertValid(key);
|
|
165
|
+
if (!key.startsWith(prefix)) throw new TypeError(`storage key "${key}" is not owned by "${owner}"`);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
function rawGet(store: KeyStore, key: string): string | null {
|
|
169
|
+
if (!hasWindow()) return null;
|
|
170
|
+
if (store === 'cookie') {
|
|
171
|
+
try {
|
|
172
|
+
const escaped = encodeURIComponent(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
173
|
+
const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${escaped}=([^;]*)`));
|
|
174
|
+
if (match?.[1] !== undefined) return decodeURIComponent(match[1]);
|
|
175
|
+
} catch {
|
|
176
|
+
/* fall through to memory */
|
|
177
|
+
}
|
|
178
|
+
return memory.cookie.get(key) ?? null;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const value = nativeStorage(store)?.getItem(key) ?? null;
|
|
182
|
+
if (value !== null) return value;
|
|
183
|
+
} catch {
|
|
184
|
+
/* fall through to memory */
|
|
185
|
+
}
|
|
186
|
+
return memory[store].get(key) ?? null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function rawSet(store: KeyStore, key: string, value: string): boolean {
|
|
190
|
+
if (!hasWindow()) return false;
|
|
191
|
+
try {
|
|
192
|
+
if (store === 'cookie') {
|
|
193
|
+
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
|
|
194
|
+
document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(value)}; Path=/; Max-Age=${cookieMaxAge}; SameSite=Lax${secure}`;
|
|
195
|
+
if (rawGet('cookie', key) === value && !memory.cookie.has(key)) return true;
|
|
196
|
+
memory.cookie.set(key, value);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
const native = nativeStorage(store);
|
|
200
|
+
if (!native) throw new Error('storage unavailable');
|
|
201
|
+
native.setItem(key, value);
|
|
202
|
+
memory[store].delete(key);
|
|
203
|
+
return true;
|
|
204
|
+
} catch {
|
|
205
|
+
memory[store].set(key, value);
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function rawRemove(store: KeyStore, key: string): void {
|
|
211
|
+
memory[store].delete(key);
|
|
212
|
+
if (!hasWindow()) return;
|
|
213
|
+
try {
|
|
214
|
+
if (store === 'cookie') {
|
|
215
|
+
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
|
|
216
|
+
document.cookie = `${encodeURIComponent(key)}=; Path=/; Max-Age=0; SameSite=Lax${secure}`;
|
|
217
|
+
} else {
|
|
218
|
+
nativeStorage(store)?.removeItem(key);
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
/* ignore */
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function resolveStore(key: string, store?: KeyStore): KeyStore {
|
|
226
|
+
return store ?? specByKey.get(key)?.[0]?.store ?? 'local';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function listKeys(store: 'local' | 'session', startsWith: string): string[] {
|
|
230
|
+
const out: string[] = [];
|
|
231
|
+
try {
|
|
232
|
+
const native = nativeStorage(store);
|
|
233
|
+
if (native) {
|
|
234
|
+
for (let i = 0; i < native.length; i += 1) {
|
|
235
|
+
const name = native.key(i);
|
|
236
|
+
if (name?.startsWith(startsWith)) out.push(name);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
/* ignore */
|
|
241
|
+
}
|
|
242
|
+
for (const name of memory[store].keys()) if (name.startsWith(startsWith) && !out.includes(name)) out.push(name);
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const familyKey = (family: FamilySpec, id: string) => {
|
|
247
|
+
const part = normalizeSegment(id);
|
|
248
|
+
if (!part) throw new TypeError('storage family id is empty after normalisation');
|
|
249
|
+
return family.prefix + part;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const api: GuardedStorage = {
|
|
253
|
+
owner,
|
|
254
|
+
key(...parts) {
|
|
255
|
+
const segments = parts.map(normalizeSegment);
|
|
256
|
+
if (segments.length === 0 || segments.some((segment) => !segment)) {
|
|
257
|
+
throw new TypeError(`storage key parts must be non-empty: ${JSON.stringify(parts)}`);
|
|
258
|
+
}
|
|
259
|
+
return `${prefix}${segments.join('.')}`;
|
|
260
|
+
},
|
|
261
|
+
familyKey,
|
|
262
|
+
isValid,
|
|
263
|
+
read(key, store) {
|
|
264
|
+
assertValid(key);
|
|
265
|
+
const target = resolveStore(key, store);
|
|
266
|
+
return rawGet(target, key);
|
|
267
|
+
},
|
|
268
|
+
write(key, value, store) {
|
|
269
|
+
assertOwned(key);
|
|
270
|
+
const target = resolveStore(key, store);
|
|
271
|
+
return rawSet(target, key, value);
|
|
272
|
+
},
|
|
273
|
+
remove(key, store) {
|
|
274
|
+
assertOwned(key);
|
|
275
|
+
const target = resolveStore(key, store);
|
|
276
|
+
rawRemove(target, key);
|
|
277
|
+
},
|
|
278
|
+
readJSON<T>(key: string, store?: KeyStore): T | null {
|
|
279
|
+
const raw = api.read(key, store);
|
|
280
|
+
if (raw === null) return null;
|
|
281
|
+
try {
|
|
282
|
+
return JSON.parse(raw) as T;
|
|
283
|
+
} catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
writeJSON(key, value, store) {
|
|
288
|
+
return api.write(key, JSON.stringify(value), store);
|
|
289
|
+
},
|
|
290
|
+
keys(store = 'local') {
|
|
291
|
+
return listKeys(store, prefix);
|
|
292
|
+
},
|
|
293
|
+
clearAll({ all = false } = {}) {
|
|
294
|
+
for (const spec of specs) {
|
|
295
|
+
if (!all && !spec.personal) continue;
|
|
296
|
+
rawRemove(spec.store, spec.key);
|
|
297
|
+
}
|
|
298
|
+
for (const family of families) {
|
|
299
|
+
if (!all && !family.personal) continue;
|
|
300
|
+
for (const name of listKeys(family.store, family.prefix)) rawRemove(family.store, name);
|
|
301
|
+
}
|
|
302
|
+
if (all) {
|
|
303
|
+
for (const store of ['local', 'session'] as const) {
|
|
304
|
+
for (const name of listKeys(store, prefix)) rawRemove(store, name);
|
|
305
|
+
}
|
|
306
|
+
for (const name of cookieNames()) if (name.startsWith(prefix)) rawRemove('cookie', name);
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
onChange(key, listener, store = 'local') {
|
|
310
|
+
assertValid(key);
|
|
311
|
+
if (!hasWindow()) return () => {};
|
|
312
|
+
const handler = (event: StorageEvent) => {
|
|
313
|
+
if (event.storageArea !== nativeStorage(store)) return;
|
|
314
|
+
if (event.key === null || event.key === key) listener(event.key === null ? null : event.newValue);
|
|
315
|
+
};
|
|
316
|
+
window.addEventListener('storage', handler);
|
|
317
|
+
return () => window.removeEventListener('storage', handler);
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
return api;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The framework's own storage: every `cfg.*` key. */
|
|
324
|
+
export const cfgStorage: GuardedStorage = createStorage('cfg', {
|
|
325
|
+
keys: CFG_KEYS,
|
|
326
|
+
families: CFG_FAMILIES,
|
|
327
|
+
});
|
|
328
|
+
|
|
@@ -30,6 +30,7 @@ import React, {
|
|
|
30
30
|
|
|
31
31
|
import { auth } from '../../_api/generated/helpers/auth';
|
|
32
32
|
import { APIError } from '../../_api/generated/helpers';
|
|
33
|
+
import { CFG_KEYS } from '../../_api/generated/helpers/keys';
|
|
33
34
|
import { applyRoleLogPolicy } from '../../log-control';
|
|
34
35
|
import { useCfgRouter, useLocalStorage, useQueryParams, useSession } from '../hooks';
|
|
35
36
|
import { clearProfileCache } from '../hooks/useProfileCache';
|
|
@@ -68,7 +69,7 @@ export const resolveAuthRoutes = (
|
|
|
68
69
|
defaultAuthCallback: routes?.defaultAuthCallback || routes?.auth || defaultRoutes.defaultAuthCallback,
|
|
69
70
|
});
|
|
70
71
|
|
|
71
|
-
const EMAIL_STORAGE_KEY =
|
|
72
|
+
const EMAIL_STORAGE_KEY = CFG_KEYS.email.key;
|
|
72
73
|
|
|
73
74
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
74
75
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { CFG_KEYS } from '../../_api/generated/helpers/keys';
|
|
1
2
|
import { useSessionStorage } from './useSessionStorage';
|
|
2
3
|
|
|
3
|
-
const AUTH_REDIRECT_KEY =
|
|
4
|
+
const AUTH_REDIRECT_KEY = CFG_KEYS.redirectUrl.key;
|
|
4
5
|
|
|
5
6
|
export interface AuthRedirectOptions {
|
|
6
7
|
fallbackUrl?: string;
|
|
@@ -28,7 +29,7 @@ const clearRedirectFromStorage = (): void => {
|
|
|
28
29
|
if (typeof window === 'undefined') return;
|
|
29
30
|
try {
|
|
30
31
|
window.sessionStorage.removeItem(AUTH_REDIRECT_KEY);
|
|
31
|
-
window.sessionStorage.removeItem(`${AUTH_REDIRECT_KEY}
|
|
32
|
+
window.sessionStorage.removeItem(`${AUTH_REDIRECT_KEY}_ts`);
|
|
32
33
|
} catch {
|
|
33
34
|
// Ignore errors
|
|
34
35
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useCallback, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
import { auth } from '../../_api/generated/helpers/auth';
|
|
6
|
+
import { CFG_KEYS } from '../../_api/generated/helpers/keys';
|
|
6
7
|
import { CfgAccountsOauth } from '../../_api/generated/sdk.gen';
|
|
7
8
|
import type { OAuthTokenResponse } from '../../_api/generated/types.gen';
|
|
8
9
|
import { Analytics, AnalyticsCategory, AnalyticsEvent } from '../utils/analytics';
|
|
@@ -10,6 +11,10 @@ import { formatAuthError } from '../utils/errors';
|
|
|
10
11
|
import { authLogger } from '../utils/logger';
|
|
11
12
|
import { useCfgRouter } from './useCfgRouter';
|
|
12
13
|
|
|
14
|
+
const OAUTH_STATE_KEY = CFG_KEYS.oauthState.key;
|
|
15
|
+
const OAUTH_PROVIDER_KEY = CFG_KEYS.oauthProvider.key;
|
|
16
|
+
const OAUTH_REMEMBER_KEY = CFG_KEYS.oauthRememberMe.key;
|
|
17
|
+
|
|
13
18
|
/**
|
|
14
19
|
* The user payload as the OAuth endpoint declares it.
|
|
15
20
|
*
|
|
@@ -107,9 +112,9 @@ export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuth
|
|
|
107
112
|
authLogger.info('Redirecting to GitHub...', response.authorization_url);
|
|
108
113
|
|
|
109
114
|
if (typeof window !== 'undefined') {
|
|
110
|
-
sessionStorage.setItem(
|
|
111
|
-
sessionStorage.setItem(
|
|
112
|
-
sessionStorage.setItem(
|
|
115
|
+
sessionStorage.setItem(OAUTH_STATE_KEY, response.state);
|
|
116
|
+
sessionStorage.setItem(OAUTH_PROVIDER_KEY, 'github');
|
|
117
|
+
sessionStorage.setItem(OAUTH_REMEMBER_KEY, String(rememberMe));
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
window.location.href = response.authorization_url;
|
|
@@ -146,15 +151,15 @@ export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuth
|
|
|
146
151
|
// Verify state matches what we stored
|
|
147
152
|
let rememberMeForSession = false;
|
|
148
153
|
if (typeof window !== 'undefined') {
|
|
149
|
-
const storedState = sessionStorage.getItem(
|
|
154
|
+
const storedState = sessionStorage.getItem(OAUTH_STATE_KEY);
|
|
150
155
|
if (storedState && storedState !== state) {
|
|
151
156
|
throw new Error('Invalid OAuth state - possible CSRF attack');
|
|
152
157
|
}
|
|
153
|
-
rememberMeForSession = sessionStorage.getItem(
|
|
158
|
+
rememberMeForSession = sessionStorage.getItem(OAUTH_REMEMBER_KEY) === 'true';
|
|
154
159
|
// Clear stored state
|
|
155
|
-
sessionStorage.removeItem(
|
|
156
|
-
sessionStorage.removeItem(
|
|
157
|
-
sessionStorage.removeItem(
|
|
160
|
+
sessionStorage.removeItem(OAUTH_STATE_KEY);
|
|
161
|
+
sessionStorage.removeItem(OAUTH_PROVIDER_KEY);
|
|
162
|
+
sessionStorage.removeItem(OAUTH_REMEMBER_KEY);
|
|
158
163
|
|
|
159
164
|
}
|
|
160
165
|
|
|
@@ -49,7 +49,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
49
49
|
const key = keys[i];
|
|
50
50
|
if (key) {
|
|
51
51
|
localStorage.removeItem(key);
|
|
52
|
-
localStorage.removeItem(`${key}
|
|
52
|
+
localStorage.removeItem(`${key}_ts`);
|
|
53
53
|
}
|
|
54
54
|
} catch {
|
|
55
55
|
// Ignore errors when removing items
|
|
@@ -88,7 +88,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
88
88
|
// Remove the key if data is too large
|
|
89
89
|
try {
|
|
90
90
|
window.localStorage.removeItem(key);
|
|
91
|
-
window.localStorage.removeItem(`${key}
|
|
91
|
+
window.localStorage.removeItem(`${key}_ts`);
|
|
92
92
|
} catch {
|
|
93
93
|
// Ignore errors when removing
|
|
94
94
|
}
|
|
@@ -109,7 +109,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
109
109
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
110
110
|
}
|
|
111
111
|
// Add timestamp for cleanup
|
|
112
|
-
window.localStorage.setItem(`${key}
|
|
112
|
+
window.localStorage.setItem(`${key}_ts`, Date.now().toString());
|
|
113
113
|
} catch (storageError: unknown) {
|
|
114
114
|
// If quota exceeded, clear old data and try again
|
|
115
115
|
if (isQuotaExceededError(storageError)) {
|
|
@@ -124,7 +124,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
124
124
|
} else {
|
|
125
125
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
126
126
|
}
|
|
127
|
-
window.localStorage.setItem(`${key}
|
|
127
|
+
window.localStorage.setItem(`${key}_ts`, Date.now().toString());
|
|
128
128
|
} catch (retryError) {
|
|
129
129
|
authLogger.error(`Failed to set localStorage key "${key}" after clearing old data:`, retryError);
|
|
130
130
|
// If still fails, force clear all and try one more time
|
|
@@ -136,7 +136,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
136
136
|
} else {
|
|
137
137
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
138
138
|
}
|
|
139
|
-
window.localStorage.setItem(`${key}
|
|
139
|
+
window.localStorage.setItem(`${key}_ts`, Date.now().toString());
|
|
140
140
|
} catch (finalError) {
|
|
141
141
|
authLogger.error(`Failed to set localStorage key "${key}" after force clearing:`, finalError);
|
|
142
142
|
// If still fails, just update the state without localStorage
|
|
@@ -163,7 +163,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
163
163
|
if (typeof window !== 'undefined') {
|
|
164
164
|
try {
|
|
165
165
|
window.localStorage.removeItem(key);
|
|
166
|
-
window.localStorage.removeItem(`${key}
|
|
166
|
+
window.localStorage.removeItem(`${key}_ts`);
|
|
167
167
|
} catch (removeError: unknown) {
|
|
168
168
|
// If removal fails due to quota, try to clear some data first
|
|
169
169
|
if (isQuotaExceededError(removeError)) {
|
|
@@ -172,7 +172,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
172
172
|
|
|
173
173
|
try {
|
|
174
174
|
window.localStorage.removeItem(key);
|
|
175
|
-
window.localStorage.removeItem(`${key}
|
|
175
|
+
window.localStorage.removeItem(`${key}_ts`);
|
|
176
176
|
} catch (retryError) {
|
|
177
177
|
authLogger.error(`Failed to remove localStorage key "${key}" after clearing:`, retryError);
|
|
178
178
|
// If still fails, force clear all
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
import type { User } from '../../';
|
|
13
13
|
import { authLogger as profileLogger } from '../utils/logger';
|
|
14
|
+
import { CFG_KEYS } from '../../_api/generated/helpers/keys';
|
|
14
15
|
import { decodeBase64, encodeBase64 } from './useBase64';
|
|
15
16
|
|
|
16
17
|
// Cache configuration
|
|
17
|
-
const CACHE_KEY =
|
|
18
|
+
const CACHE_KEY = CFG_KEYS.profileCache.key;
|
|
18
19
|
const CACHE_VERSION = 1;
|
|
19
20
|
const DEFAULT_TTL = 14400000; // 4 hours in milliseconds (reduced API calls, more resilient)
|
|
20
21
|
|
|
@@ -39,7 +39,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
39
39
|
const key = keys[i];
|
|
40
40
|
if (key) {
|
|
41
41
|
sessionStorage.removeItem(key);
|
|
42
|
-
sessionStorage.removeItem(`${key}
|
|
42
|
+
sessionStorage.removeItem(`${key}_ts`);
|
|
43
43
|
}
|
|
44
44
|
} catch {
|
|
45
45
|
// Ignore errors when removing items
|
|
@@ -78,7 +78,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
78
78
|
// Remove the key if data is too large
|
|
79
79
|
try {
|
|
80
80
|
window.sessionStorage.removeItem(key);
|
|
81
|
-
window.sessionStorage.removeItem(`${key}
|
|
81
|
+
window.sessionStorage.removeItem(`${key}_ts`);
|
|
82
82
|
} catch {
|
|
83
83
|
// Ignore errors when removing
|
|
84
84
|
}
|
|
@@ -94,7 +94,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
94
94
|
try {
|
|
95
95
|
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
96
96
|
// Add timestamp for cleanup
|
|
97
|
-
window.sessionStorage.setItem(`${key}
|
|
97
|
+
window.sessionStorage.setItem(`${key}_ts`, Date.now().toString());
|
|
98
98
|
} catch (storageError: unknown) {
|
|
99
99
|
// If quota exceeded, clear old data and try again
|
|
100
100
|
if (isQuotaExceededError(storageError)) {
|
|
@@ -104,14 +104,14 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
104
104
|
// Try again after clearing
|
|
105
105
|
try {
|
|
106
106
|
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
107
|
-
window.sessionStorage.setItem(`${key}
|
|
107
|
+
window.sessionStorage.setItem(`${key}_ts`, Date.now().toString());
|
|
108
108
|
} catch (retryError) {
|
|
109
109
|
authLogger.error(`Failed to set sessionStorage key "${key}" after clearing old data:`, retryError);
|
|
110
110
|
// If still fails, force clear all and try one more time
|
|
111
111
|
try {
|
|
112
112
|
forceClearAll();
|
|
113
113
|
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
114
|
-
window.sessionStorage.setItem(`${key}
|
|
114
|
+
window.sessionStorage.setItem(`${key}_ts`, Date.now().toString());
|
|
115
115
|
} catch (finalError) {
|
|
116
116
|
authLogger.error(`Failed to set sessionStorage key "${key}" after force clearing:`, finalError);
|
|
117
117
|
// If still fails, just update the state without sessionStorage
|
|
@@ -138,7 +138,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
138
138
|
if (typeof window !== 'undefined') {
|
|
139
139
|
try {
|
|
140
140
|
window.sessionStorage.removeItem(key);
|
|
141
|
-
window.sessionStorage.removeItem(`${key}
|
|
141
|
+
window.sessionStorage.removeItem(`${key}_ts`);
|
|
142
142
|
} catch (removeError: unknown) {
|
|
143
143
|
// If removal fails due to quota, try to clear some data first
|
|
144
144
|
if (isQuotaExceededError(removeError)) {
|
|
@@ -147,7 +147,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
147
147
|
|
|
148
148
|
try {
|
|
149
149
|
window.sessionStorage.removeItem(key);
|
|
150
|
-
window.sessionStorage.removeItem(`${key}
|
|
150
|
+
window.sessionStorage.removeItem(`${key}_ts`);
|
|
151
151
|
} catch (retryError) {
|
|
152
152
|
authLogger.error(`Failed to remove sessionStorage key "${key}" after clearing:`, retryError);
|
|
153
153
|
// If still fails, force clear all
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@djangocfg/api/storage` — the browser-storage key registry on its own.
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies and no client: packages that only need a key, a guarded
|
|
5
|
+
* read/write or `createStorage('<owner>')` import this, not the generated SDK.
|
|
6
|
+
* The source is the generated `helpers/keys.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export {
|
|
9
|
+
CFG_FAMILIES,
|
|
10
|
+
CFG_KEYS,
|
|
11
|
+
cfgStorage,
|
|
12
|
+
createStorage,
|
|
13
|
+
normalizeSegment,
|
|
14
|
+
} from './_api/generated/helpers/keys';
|
|
15
|
+
export type {
|
|
16
|
+
FamilySpec,
|
|
17
|
+
GuardedStorage,
|
|
18
|
+
KeySpec,
|
|
19
|
+
KeyStore,
|
|
20
|
+
StorageOptions,
|
|
21
|
+
} from './_api/generated/helpers/keys';
|