@djangocfg/api 2.1.420 → 2.1.422

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 (45) hide show
  1. package/dist/auth-server.cjs +45 -27
  2. package/dist/auth-server.cjs.map +1 -1
  3. package/dist/auth-server.mjs +45 -27
  4. package/dist/auth-server.mjs.map +1 -1
  5. package/dist/auth.cjs +61 -33
  6. package/dist/auth.cjs.map +1 -1
  7. package/dist/auth.d.cts +2 -1
  8. package/dist/auth.d.ts +2 -1
  9. package/dist/auth.mjs +61 -33
  10. package/dist/auth.mjs.map +1 -1
  11. package/dist/clients.cjs +45 -27
  12. package/dist/clients.cjs.map +1 -1
  13. package/dist/clients.d.cts +1 -1
  14. package/dist/clients.d.ts +1 -1
  15. package/dist/clients.mjs +45 -27
  16. package/dist/clients.mjs.map +1 -1
  17. package/dist/hooks.cjs +48 -29
  18. package/dist/hooks.cjs.map +1 -1
  19. package/dist/hooks.mjs +48 -29
  20. package/dist/hooks.mjs.map +1 -1
  21. package/dist/index.cjs +45 -27
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +2 -2
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.mjs +45 -27
  26. package/dist/index.mjs.map +1 -1
  27. package/package.json +2 -2
  28. package/src/_api/generated/_cfg_accounts/hooks/useCfgAccountsApiKeyRetrieve.ts +12 -3
  29. package/src/_api/generated/_cfg_accounts/hooks/useCfgAccountsOauthConnectionsList.ts +12 -3
  30. package/src/_api/generated/_cfg_accounts/hooks/useCfgAccountsOauthProvidersRetrieve.ts +12 -3
  31. package/src/_api/generated/_cfg_accounts/hooks/useCfgAccountsProfileRetrieve.ts +12 -3
  32. package/src/_api/generated/_cfg_accounts/openapi.json +9 -0
  33. package/src/_api/generated/_cfg_centrifugo/hooks/useCfgCentrifugoAuthTokenRetrieve.ts +12 -3
  34. package/src/_api/generated/_cfg_totp/hooks/useCfgTotpBackupCodesRetrieve.ts +12 -3
  35. package/src/_api/generated/_cfg_totp/hooks/useCfgTotpDevicesRetrieve.ts +12 -3
  36. package/src/_api/generated/client/client.gen.ts +1 -4
  37. package/src/_api/generated/client/types.gen.ts +3 -2
  38. package/src/_api/generated/client/utils.gen.ts +6 -8
  39. package/src/_api/generated/core/params.gen.ts +4 -4
  40. package/src/_api/generated/helpers/auth.ts +58 -9
  41. package/src/_api/generated/index.ts +6 -0
  42. package/src/_api/generated/openapi.json +9 -0
  43. package/src/_api/generated/sdk.gen.ts +18 -6
  44. package/src/auth/hooks/useAuthForm.ts +5 -4
  45. package/src/auth/hooks/useAutoAuth.ts +23 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/api",
3
- "version": "2.1.420",
3
+ "version": "2.1.422",
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",
@@ -84,7 +84,7 @@
84
84
  "devDependencies": {
85
85
  "@types/node": "^25.2.3",
86
86
  "@types/react": "^19.2.15",
87
- "@djangocfg/typescript-config": "^2.1.420",
87
+ "@djangocfg/typescript-config": "^2.1.422",
88
88
  "next": "^16.2.2",
89
89
  "react": "^19.2.4",
90
90
  "tsup": "^8.5.0",
@@ -12,11 +12,20 @@ import { APIKeySchema } from "../schemas/APIKey";
12
12
 
13
13
  type Result = CfgAccountsApiKeyRetrieveResponses[keyof CfgAccountsApiKeyRetrieveResponses];
14
14
 
15
+ /**
16
+ * Optional `enabled` flag — when `false`, the SWR key becomes `null` and
17
+ * no request is made. Use for gated endpoints (auth-required, feature
18
+ * flags, "fetch only when X is selected"). The flag is stripped before
19
+ * being forwarded to `useSWR` so the underlying SWR config stays pristine.
20
+ */
21
+ type HookConfig = SWRConfiguration<Result> & { enabled?: boolean };
22
+
15
23
  export function useCfgAccountsApiKeyRetrieve(
16
24
  args?: Omit<CfgAccountsApiKeyRetrieveData, "url" | "body">,
17
- config?: SWRConfiguration<Result>,
25
+ config?: HookConfig,
18
26
  ) {
19
- const key = ["cfg_accounts_api_key_retrieve", args ?? {}] as const;
27
+ const { enabled = true, ...swrConfig } = config ?? {};
28
+ const key = enabled ? ["cfg_accounts_api_key_retrieve", args ?? {}] as const : null;
20
29
  return useSWR<Result>(
21
30
  key,
22
31
  async () => {
@@ -60,6 +69,6 @@ export function useCfgAccountsApiKeyRetrieve(
60
69
  }
61
70
  return parsed.data as unknown as Result;
62
71
  },
63
- config,
72
+ swrConfig,
64
73
  );
65
74
  }
@@ -12,11 +12,20 @@ import { cfg_accounts_oauth_connections_response_200_AutoRefSchema } from "../sc
12
12
 
13
13
  type Result = CfgAccountsOauthConnectionsListResponses[keyof CfgAccountsOauthConnectionsListResponses];
14
14
 
15
+ /**
16
+ * Optional `enabled` flag — when `false`, the SWR key becomes `null` and
17
+ * no request is made. Use for gated endpoints (auth-required, feature
18
+ * flags, "fetch only when X is selected"). The flag is stripped before
19
+ * being forwarded to `useSWR` so the underlying SWR config stays pristine.
20
+ */
21
+ type HookConfig = SWRConfiguration<Result> & { enabled?: boolean };
22
+
15
23
  export function useCfgAccountsOauthConnectionsList(
16
24
  args?: Omit<CfgAccountsOauthConnectionsListData, "url" | "body">,
17
- config?: SWRConfiguration<Result>,
25
+ config?: HookConfig,
18
26
  ) {
19
- const key = ["cfg_accounts_oauth_connections_list", args ?? {}] as const;
27
+ const { enabled = true, ...swrConfig } = config ?? {};
28
+ const key = enabled ? ["cfg_accounts_oauth_connections_list", args ?? {}] as const : null;
20
29
  return useSWR<Result>(
21
30
  key,
22
31
  async () => {
@@ -60,6 +69,6 @@ export function useCfgAccountsOauthConnectionsList(
60
69
  }
61
70
  return parsed.data as unknown as Result;
62
71
  },
63
- config,
72
+ swrConfig,
64
73
  );
65
74
  }
@@ -12,11 +12,20 @@ import { OAuthProvidersResponseSchema } from "../schemas/OAuthProvidersResponse"
12
12
 
13
13
  type Result = CfgAccountsOauthProvidersRetrieveResponses[keyof CfgAccountsOauthProvidersRetrieveResponses];
14
14
 
15
+ /**
16
+ * Optional `enabled` flag — when `false`, the SWR key becomes `null` and
17
+ * no request is made. Use for gated endpoints (auth-required, feature
18
+ * flags, "fetch only when X is selected"). The flag is stripped before
19
+ * being forwarded to `useSWR` so the underlying SWR config stays pristine.
20
+ */
21
+ type HookConfig = SWRConfiguration<Result> & { enabled?: boolean };
22
+
15
23
  export function useCfgAccountsOauthProvidersRetrieve(
16
24
  args?: Omit<CfgAccountsOauthProvidersRetrieveData, "url" | "body">,
17
- config?: SWRConfiguration<Result>,
25
+ config?: HookConfig,
18
26
  ) {
19
- const key = ["cfg_accounts_oauth_providers_retrieve", args ?? {}] as const;
27
+ const { enabled = true, ...swrConfig } = config ?? {};
28
+ const key = enabled ? ["cfg_accounts_oauth_providers_retrieve", args ?? {}] as const : null;
20
29
  return useSWR<Result>(
21
30
  key,
22
31
  async () => {
@@ -60,6 +69,6 @@ export function useCfgAccountsOauthProvidersRetrieve(
60
69
  }
61
70
  return parsed.data as unknown as Result;
62
71
  },
63
- config,
72
+ swrConfig,
64
73
  );
65
74
  }
@@ -12,11 +12,20 @@ import { UserSchema } from "../schemas/User";
12
12
 
13
13
  type Result = CfgAccountsProfileRetrieveResponses[keyof CfgAccountsProfileRetrieveResponses];
14
14
 
15
+ /**
16
+ * Optional `enabled` flag — when `false`, the SWR key becomes `null` and
17
+ * no request is made. Use for gated endpoints (auth-required, feature
18
+ * flags, "fetch only when X is selected"). The flag is stripped before
19
+ * being forwarded to `useSWR` so the underlying SWR config stays pristine.
20
+ */
21
+ type HookConfig = SWRConfiguration<Result> & { enabled?: boolean };
22
+
15
23
  export function useCfgAccountsProfileRetrieve(
16
24
  args?: Omit<CfgAccountsProfileRetrieveData, "url" | "body">,
17
- config?: SWRConfiguration<Result>,
25
+ config?: HookConfig,
18
26
  ) {
19
- const key = ["cfg_accounts_profile_retrieve", args ?? {}] as const;
27
+ const { enabled = true, ...swrConfig } = config ?? {};
28
+ const key = enabled ? ["cfg_accounts_profile_retrieve", args ?? {}] as const : null;
20
29
  return useSWR<Result>(
21
30
  key,
22
31
  async () => {
@@ -60,6 +69,6 @@ export function useCfgAccountsProfileRetrieve(
60
69
  }
61
70
  return parsed.data as unknown as Result;
62
71
  },
63
- config,
72
+ swrConfig,
64
73
  );
65
74
  }
@@ -20,6 +20,9 @@
20
20
  },
21
21
  {
22
22
  "cookieAuth": []
23
+ },
24
+ {
25
+ "apiKeyAuth": []
23
26
  }
24
27
  ],
25
28
  "responses": {
@@ -71,6 +74,9 @@
71
74
  },
72
75
  {
73
76
  "cookieAuth": []
77
+ },
78
+ {
79
+ "apiKeyAuth": []
74
80
  }
75
81
  ],
76
82
  "responses": {
@@ -131,6 +137,9 @@
131
137
  },
132
138
  {
133
139
  "cookieAuth": []
140
+ },
141
+ {
142
+ "apiKeyAuth": []
134
143
  }
135
144
  ],
136
145
  "responses": {
@@ -12,11 +12,20 @@ import { ConnectionTokenResponseSchema } from "../schemas/ConnectionTokenRespons
12
12
 
13
13
  type Result = CfgCentrifugoAuthTokenRetrieveResponses[keyof CfgCentrifugoAuthTokenRetrieveResponses];
14
14
 
15
+ /**
16
+ * Optional `enabled` flag — when `false`, the SWR key becomes `null` and
17
+ * no request is made. Use for gated endpoints (auth-required, feature
18
+ * flags, "fetch only when X is selected"). The flag is stripped before
19
+ * being forwarded to `useSWR` so the underlying SWR config stays pristine.
20
+ */
21
+ type HookConfig = SWRConfiguration<Result> & { enabled?: boolean };
22
+
15
23
  export function useCfgCentrifugoAuthTokenRetrieve(
16
24
  args?: Omit<CfgCentrifugoAuthTokenRetrieveData, "url" | "body">,
17
- config?: SWRConfiguration<Result>,
25
+ config?: HookConfig,
18
26
  ) {
19
- const key = ["cfg_centrifugo_auth_token_retrieve", args ?? {}] as const;
27
+ const { enabled = true, ...swrConfig } = config ?? {};
28
+ const key = enabled ? ["cfg_centrifugo_auth_token_retrieve", args ?? {}] as const : null;
20
29
  return useSWR<Result>(
21
30
  key,
22
31
  async () => {
@@ -60,6 +69,6 @@ export function useCfgCentrifugoAuthTokenRetrieve(
60
69
  }
61
70
  return parsed.data as unknown as Result;
62
71
  },
63
- config,
72
+ swrConfig,
64
73
  );
65
74
  }
@@ -12,11 +12,20 @@ import { BackupCodesStatusSchema } from "../schemas/BackupCodesStatus";
12
12
 
13
13
  type Result = CfgTotpBackupCodesRetrieveResponses[keyof CfgTotpBackupCodesRetrieveResponses];
14
14
 
15
+ /**
16
+ * Optional `enabled` flag — when `false`, the SWR key becomes `null` and
17
+ * no request is made. Use for gated endpoints (auth-required, feature
18
+ * flags, "fetch only when X is selected"). The flag is stripped before
19
+ * being forwarded to `useSWR` so the underlying SWR config stays pristine.
20
+ */
21
+ type HookConfig = SWRConfiguration<Result> & { enabled?: boolean };
22
+
15
23
  export function useCfgTotpBackupCodesRetrieve(
16
24
  args?: Omit<CfgTotpBackupCodesRetrieveData, "url" | "body">,
17
- config?: SWRConfiguration<Result>,
25
+ config?: HookConfig,
18
26
  ) {
19
- const key = ["cfg_totp_backup_codes_retrieve", args ?? {}] as const;
27
+ const { enabled = true, ...swrConfig } = config ?? {};
28
+ const key = enabled ? ["cfg_totp_backup_codes_retrieve", args ?? {}] as const : null;
20
29
  return useSWR<Result>(
21
30
  key,
22
31
  async () => {
@@ -60,6 +69,6 @@ export function useCfgTotpBackupCodesRetrieve(
60
69
  }
61
70
  return parsed.data as unknown as Result;
62
71
  },
63
- config,
72
+ swrConfig,
64
73
  );
65
74
  }
@@ -12,11 +12,20 @@ import { DeviceListResponseSchema } from "../schemas/DeviceListResponse";
12
12
 
13
13
  type Result = CfgTotpDevicesRetrieveResponses[keyof CfgTotpDevicesRetrieveResponses];
14
14
 
15
+ /**
16
+ * Optional `enabled` flag — when `false`, the SWR key becomes `null` and
17
+ * no request is made. Use for gated endpoints (auth-required, feature
18
+ * flags, "fetch only when X is selected"). The flag is stripped before
19
+ * being forwarded to `useSWR` so the underlying SWR config stays pristine.
20
+ */
21
+ type HookConfig = SWRConfiguration<Result> & { enabled?: boolean };
22
+
15
23
  export function useCfgTotpDevicesRetrieve(
16
24
  args?: Omit<CfgTotpDevicesRetrieveData, "url" | "body">,
17
- config?: SWRConfiguration<Result>,
25
+ config?: HookConfig,
18
26
  ) {
19
- const key = ["cfg_totp_devices_retrieve", args ?? {}] as const;
27
+ const { enabled = true, ...swrConfig } = config ?? {};
28
+ const key = enabled ? ["cfg_totp_devices_retrieve", args ?? {}] as const : null;
20
29
  return useSWR<Result>(
21
30
  key,
22
31
  async () => {
@@ -60,6 +69,6 @@ export function useCfgTotpDevicesRetrieve(
60
69
  }
61
70
  return parsed.data as unknown as Result;
62
71
  },
63
- config,
72
+ swrConfig,
64
73
  );
65
74
  }
@@ -48,10 +48,7 @@ export const createClient = (config: Config = {}): Client => {
48
48
  };
49
49
 
50
50
  if (opts.security) {
51
- await setAuthParams({
52
- ...opts,
53
- security: opts.security,
54
- });
51
+ await setAuthParams(opts);
55
52
  }
56
53
 
57
54
  if (opts.requestValidator) {
@@ -151,12 +151,13 @@ type MethodFn = <
151
151
 
152
152
  type SseFn = <
153
153
  TData = unknown,
154
- TError = unknown,
154
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
155
+ _TError = unknown,
155
156
  ThrowOnError extends boolean = false,
156
157
  TResponseStyle extends ResponseStyle = 'fields',
157
158
  >(
158
159
  options: Omit<RequestOptions<never, TResponseStyle, ThrowOnError>, 'method'>,
159
- ) => Promise<ServerSentEventsResult<TData, TError>>;
160
+ ) => Promise<ServerSentEventsResult<TData>>;
160
161
 
161
162
  type RequestFn = <
162
163
  TData = unknown,
@@ -118,14 +118,12 @@ const checkForExistence = (
118
118
  return false;
119
119
  };
120
120
 
121
- export const setAuthParams = async ({
122
- security,
123
- ...options
124
- }: Pick<Required<RequestOptions>, 'security'> &
125
- Pick<RequestOptions, 'auth' | 'query'> & {
121
+ export async function setAuthParams(
122
+ options: Pick<RequestOptions, 'auth' | 'query' | 'security'> & {
126
123
  headers: Headers;
127
- }) => {
128
- for (const auth of security) {
124
+ },
125
+ ): Promise<void> {
126
+ for (const auth of options.security ?? []) {
129
127
  if (checkForExistence(options, auth.name)) {
130
128
  continue;
131
129
  }
@@ -154,7 +152,7 @@ export const setAuthParams = async ({
154
152
  break;
155
153
  }
156
154
  }
157
- };
155
+ }
158
156
 
159
157
  export const buildUrl: Client['buildUrl'] = (options) =>
160
158
  getUrl({
@@ -104,10 +104,10 @@ const stripEmptySlots = (params: Params) => {
104
104
 
105
105
  export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
106
106
  const params: Params = {
107
- body: {},
108
- headers: {},
109
- path: {},
110
- query: {},
107
+ body: Object.create(null),
108
+ headers: Object.create(null),
109
+ path: Object.create(null),
110
+ query: Object.create(null),
111
111
  };
112
112
 
113
113
  const map = buildKeyMap(fields);
@@ -79,12 +79,30 @@ function detectLocale(): string | null {
79
79
  return null;
80
80
  }
81
81
 
82
- /** Default baseUrl from `NEXT_PUBLIC_API_URL`.
82
+ /** Default baseUrl.
83
83
  *
84
- * Both browser and server use NEXT_PUBLIC_API_URL requests go
85
- * directly to Django without Next.js proxy.
84
+ * Browser: uses NEXT_PUBLIC_API_PROXY_URL if set (empty string = same-origin
85
+ * Next.js proxy), otherwise falls back to NEXT_PUBLIC_API_URL directly.
86
+ * Set NEXT_PUBLIC_API_PROXY_URL='' in apps that proxy /apix/* via Next.js
87
+ * Route Handler; leave it unset in apps that hit Django directly.
88
+ *
89
+ * Server (SSR / RSC / Edge): use NEXT_PUBLIC_API_URL so server-side fetch
90
+ * reaches Django directly (proxy is a same-origin HTTP round-trip on server).
86
91
  */
87
92
  function defaultBaseUrl(): string {
93
+ if (typeof window !== 'undefined') {
94
+ try {
95
+ if (typeof process !== 'undefined' && process.env) {
96
+ if (process.env.NEXT_PUBLIC_STATIC_BUILD === 'true') return '';
97
+ // NEXT_PUBLIC_API_PROXY_URL='' means same-origin proxy (e.g. /apix/* route handler).
98
+ // Next.js inlines defined vars as their value; undefined means the var was not set.
99
+ if (process.env.NEXT_PUBLIC_API_PROXY_URL !== undefined)
100
+ return process.env.NEXT_PUBLIC_API_PROXY_URL;
101
+ return process.env.NEXT_PUBLIC_API_URL || '';
102
+ }
103
+ } catch {}
104
+ return '';
105
+ }
88
106
  try {
89
107
  if (typeof process !== 'undefined' && process.env) {
90
108
  if (process.env.NEXT_PUBLIC_STATIC_BUILD === 'true') return '';
@@ -96,13 +114,11 @@ function defaultBaseUrl(): string {
96
114
 
97
115
  /** Default API key fallback from `NEXT_PUBLIC_API_KEY`.
98
116
  *
99
- * In the browser: returns null requests go through the Next.js rewrite
100
- * and the key is injected server-side (never exposed in the bundle).
101
- * On the server: reads NEXT_PUBLIC_API_KEY as a fallback for SSR calls
102
- * that bypass the rewrite (e.g. server components calling Django directly).
117
+ * Both browser and server: if NEXT_PUBLIC_API_KEY is set it is used as a
118
+ * global fallback (e.g. a public demo key). When not set, returns null so
119
+ * per-request keys passed via options.headers take effect unobstructed.
103
120
  */
104
121
  function defaultApiKey(): string | null {
105
- if (isBrowser) return null;
106
122
  try {
107
123
  if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_API_KEY) {
108
124
  return process.env.NEXT_PUBLIC_API_KEY;
@@ -286,6 +302,37 @@ async function tryRefresh(): Promise<string | null> {
286
302
  * non-browser environments, so headers populated by the interceptor
287
303
  * are simply absent server-side (which is the correct behaviour
288
304
  * unless the caller explicitly sets a server-side token).
305
+ *
306
+ * ── How API key auth works ────────────────────────────────────────────────
307
+ *
308
+ * There are three ways to supply X-API-Key, in priority order:
309
+ *
310
+ * 1. Per-request header (highest priority, overrides everything):
311
+ * SomeApi.someEndpoint({ headers: { 'X-API-Key': userKey } })
312
+ * Use this in playgrounds or any context where the key comes from
313
+ * user input rather than app config.
314
+ *
315
+ * 2. Global override via auth.setApiKey(key) — applies to every request
316
+ * from this client until cleared. Good for single-user sessions.
317
+ *
318
+ * 3. NEXT_PUBLIC_API_KEY env var — read by defaultApiKey() on every request.
319
+ * Use as a public fallback key (e.g. a shared demo key for unauthenticated
320
+ * visitors). When not set, returns null so per-request keys work freely.
321
+ *
322
+ * ── baseUrl gotcha with proxy packages ───────────────────────────────────
323
+ *
324
+ * If another package (e.g. @carapis/catalog-api/client) calls
325
+ * `auth.setBaseUrl('')` on import, ALL requests from this shared client
326
+ * will go to relative paths (Next.js proxy) instead of hitting Django
327
+ * directly. This is intentional for the public catalog — but breaks any
328
+ * feature that needs to reach Django with a dynamic API key from the
329
+ * browser (e.g. a playground).
330
+ *
331
+ * Fix: pass `baseUrl` explicitly per-call to override the global setting:
332
+ * SomeApi.someEndpoint({
333
+ * baseUrl: process.env.NEXT_PUBLIC_API_URL,
334
+ * headers: { 'X-API-Key': userKey },
335
+ * })
289
336
  */
290
337
  export function installAuthOnClient(client: HeyClient): void {
291
338
  if (_client) return; // idempotent
@@ -303,8 +350,10 @@ export function installAuthOnClient(client: HeyClient): void {
303
350
  const locale = auth.getLocale();
304
351
  if (locale) request.headers.set('Accept-Language', locale);
305
352
 
353
+ // Only set X-API-Key from global store if NOT already present on the
354
+ // request — per-request headers (option 1 above) take precedence.
306
355
  const apiKey = auth.getApiKey();
307
- if (apiKey) request.headers.set('X-API-Key', apiKey);
356
+ if (apiKey && !request.headers.has('X-API-Key')) request.headers.set('X-API-Key', apiKey);
308
357
 
309
358
  try {
310
359
  const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -37,5 +37,11 @@ export { CfgAccountsApiKey, CfgAccountsOauth, CfgAccounts, CfgAccountsProfile, C
37
37
  export { CfgCentrifugo } from './_cfg_centrifugo';
38
38
  export { CfgTotpBackupCodes, CfgTotp, CfgTotpSetup, CfgTotpVerify } from './_cfg_totp';
39
39
 
40
+ // Generated DTO / schema types (Hey API). The per-group barrels each
41
+ // re-export these too; surfacing them at the top level lets consumers
42
+ // `import { FleetSummary } from '<api>'` without reaching into a
43
+ // group subpath.
44
+ export type * from './types.gen';
45
+
40
46
  // Shared utilities (errors, storage adapters, logger).
41
47
  export * from './helpers';
@@ -20,6 +20,9 @@
20
20
  },
21
21
  {
22
22
  "cookieAuth": []
23
+ },
24
+ {
25
+ "apiKeyAuth": []
23
26
  }
24
27
  ],
25
28
  "responses": {
@@ -71,6 +74,9 @@
71
74
  },
72
75
  {
73
76
  "cookieAuth": []
77
+ },
78
+ {
79
+ "apiKeyAuth": []
74
80
  }
75
81
  ],
76
82
  "responses": {
@@ -131,6 +137,9 @@
131
137
  },
132
138
  {
133
139
  "cookieAuth": []
140
+ },
141
+ {
142
+ "apiKeyAuth": []
134
143
  }
135
144
  ],
136
145
  "responses": {
@@ -26,11 +26,15 @@ export class CfgAccountsApiKey {
26
26
  */
27
27
  public static cfgAccountsApiKeyRetrieve<ThrowOnError extends boolean = false>(options?: Options<CfgAccountsApiKeyRetrieveData, ThrowOnError>) {
28
28
  return (options?.client ?? client).get<CfgAccountsApiKeyRetrieveResponses, CfgAccountsApiKeyRetrieveErrors, ThrowOnError>({
29
- security: [{ scheme: 'bearer', type: 'http' }, {
29
+ security: [
30
+ { scheme: 'bearer', type: 'http' },
31
+ {
30
32
  in: 'cookie',
31
33
  name: 'sessionid',
32
34
  type: 'apiKey'
33
- }],
35
+ },
36
+ { name: 'X-API-Key', type: 'apiKey' }
37
+ ],
34
38
  url: '/cfg/accounts/api-key/',
35
39
  ...options
36
40
  });
@@ -43,11 +47,15 @@ export class CfgAccountsApiKey {
43
47
  */
44
48
  public static cfgAccountsApiKeyRegenerateCreate<ThrowOnError extends boolean = false>(options: Options<CfgAccountsApiKeyRegenerateCreateData, ThrowOnError>) {
45
49
  return (options.client ?? client).post<CfgAccountsApiKeyRegenerateCreateResponses, CfgAccountsApiKeyRegenerateCreateErrors, ThrowOnError>({
46
- security: [{ scheme: 'bearer', type: 'http' }, {
50
+ security: [
51
+ { scheme: 'bearer', type: 'http' },
52
+ {
47
53
  in: 'cookie',
48
54
  name: 'sessionid',
49
55
  type: 'apiKey'
50
- }],
56
+ },
57
+ { name: 'X-API-Key', type: 'apiKey' }
58
+ ],
51
59
  url: '/cfg/accounts/api-key/regenerate/',
52
60
  ...options,
53
61
  headers: {
@@ -64,11 +72,15 @@ export class CfgAccountsApiKey {
64
72
  */
65
73
  public static cfgAccountsApiKeyTestCreate<ThrowOnError extends boolean = false>(options: Options<CfgAccountsApiKeyTestCreateData, ThrowOnError>) {
66
74
  return (options.client ?? client).post<CfgAccountsApiKeyTestCreateResponses, unknown, ThrowOnError>({
67
- security: [{ scheme: 'bearer', type: 'http' }, {
75
+ security: [
76
+ { scheme: 'bearer', type: 'http' },
77
+ {
68
78
  in: 'cookie',
69
79
  name: 'sessionid',
70
80
  type: 'apiKey'
71
- }],
81
+ },
82
+ { name: 'X-API-Key', type: 'apiKey' }
83
+ ],
72
84
  url: '/cfg/accounts/api-key/test/',
73
85
  ...options,
74
86
  headers: {
@@ -311,17 +311,18 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormReturn => {
311
311
 
312
312
  useAutoAuth({
313
313
  allowedPaths: [authPath],
314
- onOTPDetected: (detectedOtp: string) => {
314
+ onOTPDetected: (detectedOtp: string, detectedEmail?: string) => {
315
315
  if (isAutoSubmitFromUrlRef.current || isLoading) return;
316
316
  isAutoSubmitFromUrlRef.current = true;
317
317
 
318
318
  authLogger.info('OTP detected from URL, auto-submitting');
319
319
 
320
- const savedEmail = getSavedEmail();
321
- const autoIdentifier = savedEmail || '';
320
+ // Prefer the email from the magic-link URL (works on a fresh browser/device),
321
+ // fall back to the one saved from a previous request on this browser.
322
+ const autoIdentifier = detectedEmail || getSavedEmail() || '';
322
323
 
323
324
  if (!autoIdentifier) {
324
- authLogger.warn('No saved identifier found for auto-submit');
325
+ authLogger.warn('No identifier found for auto-submit (no email in URL or storage)');
325
326
  isAutoSubmitFromUrlRef.current = false;
326
327
  return;
327
328
  }
@@ -9,12 +9,25 @@ import { useCfgRouter } from './useCfgRouter';
9
9
  import { useQueryParams } from './useQueryParams';
10
10
 
11
11
  export interface UseAutoAuthOptions {
12
- onOTPDetected?: (otp: string) => void;
12
+ /** Called with the OTP from the URL, plus the optional `email` query param if present. */
13
+ onOTPDetected?: (otp: string, email?: string) => void;
13
14
  cleanupUrl?: boolean;
14
15
  /** Paths where auto-auth should be active. Default: ['/auth'] */
15
16
  allowedPaths?: string[];
16
17
  }
17
18
 
19
+ /**
20
+ * Strip a leading 2-letter locale segment and any trailing slash so paths
21
+ * compare consistently regardless of the next-intl locale prefix.
22
+ * e.g. '/ru/auth/' -> '/auth', '/auth' -> '/auth', '/' -> '/'
23
+ */
24
+ const normalizePath = (path: string | null): string => {
25
+ if (!path) return '';
26
+ const withoutLocale = path.replace(/^\/[a-z]{2}(?=\/|$)/, '');
27
+ const trimmed = withoutLocale.replace(/\/+$/, '');
28
+ return trimmed || '/';
29
+ };
30
+
18
31
  /**
19
32
  * Hook for automatic authentication from URL query parameters
20
33
  * Detects OTP from URL and triggers callback
@@ -25,8 +38,12 @@ export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
25
38
  const pathname = usePathname();
26
39
  const router = useCfgRouter();
27
40
 
28
- // Check if current path is in allowed paths
29
- const isAllowedPath = allowedPaths.some(path => pathname === path || pathname?.startsWith(path + '/'));
41
+ // Check if current path is in allowed paths, ignoring the locale prefix
42
+ // (next-intl may keep it in the URL, e.g. /ru/auth) and trailing slashes.
43
+ const normalizedPath = normalizePath(pathname);
44
+ const isAllowedPath = allowedPaths.some(
45
+ path => normalizedPath === path || normalizedPath.startsWith(path + '/')
46
+ );
30
47
 
31
48
  const hasOTP = !!(queryParams.get('otp'));
32
49
  const isReady = !!pathname && hasOTP && isAllowedPath;
@@ -35,12 +52,13 @@ export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
35
52
  if (!isReady) return;
36
53
 
37
54
  const queryOtp = queryParams.get('otp') as string;
55
+ const queryEmail = queryParams.get('email') || undefined;
38
56
 
39
57
  // Handle OTP detection - only on allowed paths to avoid conflicts with other pages
40
58
  // (e.g., /dashboard/device uses ?otp= for device authorization, not user auth)
41
59
  if (queryOtp && typeof queryOtp === 'string' && queryOtp.length === AUTH_CONSTANTS.EMAIL_OTP_LENGTH) {
42
60
  authLogger.info('OTP detected in URL on auth page:', queryOtp);
43
- onOTPDetected?.(queryOtp);
61
+ onOTPDetected?.(queryOtp, queryEmail);
44
62
  }
45
63
 
46
64
  // Clean up URL to remove sensitive params for security
@@ -51,6 +69,7 @@ export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
51
69
  if (cleanupUrl && queryOtp) {
52
70
  const cleanQuery = Object.fromEntries(queryParams.entries());
53
71
  delete cleanQuery.otp;
72
+ delete cleanQuery.email;
54
73
  const queryString = new URLSearchParams(cleanQuery).toString();
55
74
  const realPathname = typeof window !== 'undefined' ? window.location.pathname : pathname;
56
75
  router.replace(queryString ? `${realPathname}?${queryString}` : realPathname);