@micha.bigler/ui-core-micha 2.2.12 → 2.2.14

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.
@@ -9,25 +9,52 @@ let redirectingToLogin = false;
9
9
  function isBrowser() {
10
10
  return typeof window !== "undefined";
11
11
  }
12
- // WICHTIG: Liste aller Routen, die OHNE Login funktionieren müssen.
13
- // Beginnt der Pfad mit einem dieser Strings, wird kein Auto-Redirect ausgelöst.
14
- const PUBLIC_PATHS = [
12
+ // Routen, die OHNE Login funktionieren müssen. Library-eigene Pfade sind eingefroren
13
+ // und können nicht entfernt werden sonst würde z. B. `removePublicPath("/login")`
14
+ // auf der Login-Page selbst einen Redirect-Loop auslösen.
15
+ const BUILTIN_PUBLIC_PATHS = Object.freeze([
15
16
  "/login",
16
17
  "/signup",
17
- "/reset-request-password", // Request Page
18
- "/invite", // Invite Link (/invite/:uid/:token)
19
- "/reset", // Reset Link (/reset/:uid/:token)
20
- "/welcome" // Optional: Falls Welcome auch öffentlich ist
21
- ];
18
+ "/reset-request-password",
19
+ "/invite", // /invite/:uid/:token
20
+ "/reset", // /reset/:uid/:token
21
+ "/welcome",
22
+ ]);
23
+ const CONSUMER_PUBLIC_PATHS = new Set();
24
+ /**
25
+ * Register an additional public path so a 401 on that route does not auto-redirect
26
+ * to `/login`. Typical use: a public landing on `/` in an otherwise authenticated app.
27
+ *
28
+ * MUST be called before the AuthProvider mounts (i.e. before `ReactDOM.render`).
29
+ * Calling it later won't help the bootstrap probe which fires on AuthProvider mount.
30
+ */
31
+ export function addPublicPath(path) {
32
+ if (typeof path === "string" && path) {
33
+ CONSUMER_PUBLIC_PATHS.add(path);
34
+ }
35
+ }
36
+ /** Remove a consumer-added public path. Library-internal paths are protected. */
37
+ export function removePublicPath(path) {
38
+ CONSUMER_PUBLIC_PATHS.delete(path);
39
+ }
22
40
  function isPublicSitePath(pathname) {
23
41
  return pathname === "/sites" || pathname.startsWith("/sites/");
24
42
  }
43
+ // Match rule: an entry of exactly "/" requires strict equality (avoids matching
44
+ // every path with startsWith). Other entries keep the looser prefix match so
45
+ // dynamic routes like /invite/:uid/:token still work.
46
+ function matchesPublicPath(pathname, entry) {
47
+ if (entry === "/")
48
+ return pathname === "/";
49
+ return pathname.startsWith(entry);
50
+ }
25
51
  function redirectToLoginOnce() {
26
52
  if (!isBrowser())
27
53
  return;
28
54
  const currentPath = window.location.pathname;
29
- // 1. Check: Sind wir auf einer öffentlichen Seite?
30
- const isPublicPage = isPublicSitePath(currentPath) || PUBLIC_PATHS.some(path => currentPath.startsWith(path));
55
+ const isPublicPage = isPublicSitePath(currentPath) ||
56
+ BUILTIN_PUBLIC_PATHS.some((path) => matchesPublicPath(currentPath, path)) ||
57
+ Array.from(CONSUMER_PUBLIC_PATHS).some((path) => matchesPublicPath(currentPath, path));
31
58
  // Wenn ja: NICHT weiterleiten. Der 401 Fehler wird an die Komponente durchgereicht.
32
59
  if (isPublicPage)
33
60
  return;
@@ -83,13 +110,18 @@ function extractAuthSignal(data) {
83
110
  return { code: null, i18nKey: null };
84
111
  }
85
112
  apiClient.interceptors.response.use((response) => response, (error) => {
86
- var _a, _b, _c, _d;
113
+ var _a, _b, _c, _d, _e;
87
114
  const status = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : null;
88
115
  const data = (_d = (_c = error === null || error === void 0 ? void 0 : error.response) === null || _c === void 0 ? void 0 : _c.data) !== null && _d !== void 0 ? _d : {};
89
116
  const { code, i18nKey } = extractAuthSignal(data);
90
117
  const isAuthStatus = status === 401 || status === 403;
91
118
  const isNotAuthenticated = code === "not_authenticated" || i18nKey === "auth.not_authenticated";
92
- if (isAuthStatus && isNotAuthenticated) {
119
+ // Per-request opt-out: bootstrap probes (e.g. fetchCurrentUser on app start)
120
+ // expect to handle 401 silently and must not trigger a redirect-on-mount.
121
+ // Carried as an axios config property, so it never travels to the backend
122
+ // (would otherwise trigger a CORS preflight on cross-origin requests).
123
+ const skipRedirect = ((_e = error === null || error === void 0 ? void 0 : error.config) === null || _e === void 0 ? void 0 : _e.skipAuthRedirect) === true;
124
+ if (isAuthStatus && isNotAuthenticated && !skipRedirect) {
93
125
  redirectToLoginOnce();
94
126
  }
95
127
  return Promise.reject(error);
@@ -12,7 +12,12 @@ function getCsrfToken() {
12
12
  // Session & User Core
13
13
  // -----------------------------
14
14
  export async function fetchCurrentUser() {
15
- const res = await apiClient.get(`${USERS_BASE}/current/`);
15
+ // Bootstrap-Probe: 401 darf nicht in einen Login-Redirect umschlagen,
16
+ // damit Public-Landings auf "/" sichtbar bleiben. `skipAuthRedirect` ist eine
17
+ // client-seitige axios-Config-Property und wird nicht ans Backend gesendet.
18
+ const res = await apiClient.get(`${USERS_BASE}/current/`, {
19
+ skipAuthRedirect: true,
20
+ });
16
21
  return res.data;
17
22
  }
18
23
  export async function fetchAuthMethods() {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // index.js (Entry Point deiner Library)
2
2
  // --- 1. Auth Context (Essentiell für den Wrapper) ---
3
3
  export { AuthContext, AuthProvider } from './auth/AuthContext';
4
- export { default as apiClient, ensureCsrfToken } from "./auth/apiClient";
4
+ export { default as apiClient, ensureCsrfToken, addPublicPath, removePublicPath, } from "./auth/apiClient";
5
5
  // --- 2. API & Services (Neue Struktur) ---
6
6
  // Statt dem 'authApi'-Objekt exportieren wir die Funktionen direkt.
7
7
  // Konsumenten können dann machen: import { loginWithPassword } from 'django-core-micha';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.2.12",
3
+ "version": "2.2.14",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "repository": {
@@ -13,29 +13,60 @@ function isBrowser() {
13
13
  return typeof window !== "undefined";
14
14
  }
15
15
 
16
- // WICHTIG: Liste aller Routen, die OHNE Login funktionieren müssen.
17
- // Beginnt der Pfad mit einem dieser Strings, wird kein Auto-Redirect ausgelöst.
18
- const PUBLIC_PATHS = [
16
+ // Routen, die OHNE Login funktionieren müssen. Library-eigene Pfade sind eingefroren
17
+ // und können nicht entfernt werden sonst würde z. B. `removePublicPath("/login")`
18
+ // auf der Login-Page selbst einen Redirect-Loop auslösen.
19
+ const BUILTIN_PUBLIC_PATHS = Object.freeze([
19
20
  "/login",
20
21
  "/signup",
21
- "/reset-request-password", // Request Page
22
- "/invite", // Invite Link (/invite/:uid/:token)
23
- "/reset", // Reset Link (/reset/:uid/:token)
24
- "/welcome" // Optional: Falls Welcome auch öffentlich ist
25
- ];
22
+ "/reset-request-password",
23
+ "/invite", // /invite/:uid/:token
24
+ "/reset", // /reset/:uid/:token
25
+ "/welcome",
26
+ ]);
27
+
28
+ const CONSUMER_PUBLIC_PATHS = new Set();
29
+
30
+ /**
31
+ * Register an additional public path so a 401 on that route does not auto-redirect
32
+ * to `/login`. Typical use: a public landing on `/` in an otherwise authenticated app.
33
+ *
34
+ * MUST be called before the AuthProvider mounts (i.e. before `ReactDOM.render`).
35
+ * Calling it later won't help the bootstrap probe which fires on AuthProvider mount.
36
+ */
37
+ export function addPublicPath(path) {
38
+ if (typeof path === "string" && path) {
39
+ CONSUMER_PUBLIC_PATHS.add(path);
40
+ }
41
+ }
42
+
43
+ /** Remove a consumer-added public path. Library-internal paths are protected. */
44
+ export function removePublicPath(path) {
45
+ CONSUMER_PUBLIC_PATHS.delete(path);
46
+ }
26
47
 
27
48
  function isPublicSitePath(pathname) {
28
49
  return pathname === "/sites" || pathname.startsWith("/sites/");
29
50
  }
30
51
 
52
+ // Match rule: an entry of exactly "/" requires strict equality (avoids matching
53
+ // every path with startsWith). Other entries keep the looser prefix match so
54
+ // dynamic routes like /invite/:uid/:token still work.
55
+ function matchesPublicPath(pathname, entry) {
56
+ if (entry === "/") return pathname === "/";
57
+ return pathname.startsWith(entry);
58
+ }
59
+
31
60
  function redirectToLoginOnce() {
32
61
  if (!isBrowser()) return;
33
62
 
34
63
  const currentPath = window.location.pathname;
35
64
 
36
- // 1. Check: Sind wir auf einer öffentlichen Seite?
37
- const isPublicPage = isPublicSitePath(currentPath) || PUBLIC_PATHS.some(path => currentPath.startsWith(path));
38
-
65
+ const isPublicPage =
66
+ isPublicSitePath(currentPath) ||
67
+ BUILTIN_PUBLIC_PATHS.some((path) => matchesPublicPath(currentPath, path)) ||
68
+ Array.from(CONSUMER_PUBLIC_PATHS).some((path) => matchesPublicPath(currentPath, path));
69
+
39
70
  // Wenn ja: NICHT weiterleiten. Der 401 Fehler wird an die Komponente durchgereicht.
40
71
  if (isPublicPage) return;
41
72
 
@@ -108,7 +139,13 @@ apiClient.interceptors.response.use(
108
139
  const isNotAuthenticated =
109
140
  code === "not_authenticated" || i18nKey === "auth.not_authenticated";
110
141
 
111
- if (isAuthStatus && isNotAuthenticated) {
142
+ // Per-request opt-out: bootstrap probes (e.g. fetchCurrentUser on app start)
143
+ // expect to handle 401 silently and must not trigger a redirect-on-mount.
144
+ // Carried as an axios config property, so it never travels to the backend
145
+ // (would otherwise trigger a CORS preflight on cross-origin requests).
146
+ const skipRedirect = error?.config?.skipAuthRedirect === true;
147
+
148
+ if (isAuthStatus && isNotAuthenticated && !skipRedirect) {
112
149
  redirectToLoginOnce();
113
150
  }
114
151
  return Promise.reject(error);
@@ -14,7 +14,12 @@ function getCsrfToken() {
14
14
  // -----------------------------
15
15
 
16
16
  export async function fetchCurrentUser() {
17
- const res = await apiClient.get(`${USERS_BASE}/current/`);
17
+ // Bootstrap-Probe: 401 darf nicht in einen Login-Redirect umschlagen,
18
+ // damit Public-Landings auf "/" sichtbar bleiben. `skipAuthRedirect` ist eine
19
+ // client-seitige axios-Config-Property und wird nicht ans Backend gesendet.
20
+ const res = await apiClient.get(`${USERS_BASE}/current/`, {
21
+ skipAuthRedirect: true,
22
+ });
18
23
  return res.data;
19
24
  }
20
25
 
package/src/index.js CHANGED
@@ -3,7 +3,12 @@
3
3
  // --- 1. Auth Context (Essentiell für den Wrapper) ---
4
4
  export { AuthContext, AuthProvider } from './auth/AuthContext';
5
5
 
6
- export { default as apiClient, ensureCsrfToken } from "./auth/apiClient";
6
+ export {
7
+ default as apiClient,
8
+ ensureCsrfToken,
9
+ addPublicPath,
10
+ removePublicPath,
11
+ } from "./auth/apiClient";
7
12
 
8
13
  // --- 2. API & Services (Neue Struktur) ---
9
14
  // Statt dem 'authApi'-Objekt exportieren wir die Funktionen direkt.