@tumbaland/frontend-core 1.13.0 → 1.14.1
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/authService.d.ts +4 -1
- package/dist/authService.js +107 -26
- package/package.json +1 -1
package/dist/authService.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface AuthServiceConfig {
|
|
|
3
3
|
/** Read fresh at call time (config may not be resolved yet at module load). */
|
|
4
4
|
getAuthServiceUrl: () => string;
|
|
5
5
|
getAuthFrontUrl: () => string;
|
|
6
|
+
/** The shell's URL. When the app runs inside it, the shell answers auth checks. */
|
|
7
|
+
getShellUrl?: () => string | undefined;
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
8
10
|
* Builds a per-front auth service instance. Each of the 7 frontends that
|
|
@@ -37,10 +39,11 @@ export type AuthService = ReturnType<typeof createAuthService>;
|
|
|
37
39
|
export interface AppAuthConfig {
|
|
38
40
|
AUTH_SERVICE_URL?: string;
|
|
39
41
|
AUTH_FRONT_URL?: string;
|
|
42
|
+
SHELL_FRONT_URL?: string;
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
42
45
|
* Convenience over `createAuthService` for the frontends, which all read the
|
|
43
|
-
* same
|
|
46
|
+
* same auth URLs (and the shell's) from their resolved config. Centralizes that key mapping
|
|
44
47
|
* so each app's `services/authService.ts` is just:
|
|
45
48
|
*
|
|
46
49
|
* export const authService = createAppAuthService(getGlobalConfig);
|
package/dist/authService.js
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
import { getCorrelationId, getSessionId, logger } from './monitoring';
|
|
2
2
|
import { clearSessionScopedStorage } from './sessionStorage';
|
|
3
3
|
const AUTH_CACHE_TTL = 5 * 60 * 1000;
|
|
4
|
+
/** How long an embedded app waits for the shell before asking auth-service itself. */
|
|
5
|
+
const SHELL_AUTH_TIMEOUT_MS = 3000;
|
|
6
|
+
/**
|
|
7
|
+
* The shell's origin, when this app runs inside it: framed, and loaded with the
|
|
8
|
+
* `embedded=1` flag the shell puts in the iframe src (see buildEmbeddedUrl in
|
|
9
|
+
* @tumbaland/components). A direct visit is never embedded.
|
|
10
|
+
*/
|
|
11
|
+
function embeddingShellOrigin(shellUrl) {
|
|
12
|
+
if (typeof window === 'undefined' || window.parent === window.self || !shellUrl)
|
|
13
|
+
return null;
|
|
14
|
+
if (new URLSearchParams(window.location.search).get('embedded') !== '1')
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
return new URL(shellUrl).origin;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Asks the shell for its auth state instead of calling /auth/verify.
|
|
25
|
+
*
|
|
26
|
+
* The shell has already checked the session when it framed this app, and
|
|
27
|
+
* every app calling verify on its own meant several parallel renewals on each
|
|
28
|
+
* page open. Resolves to null when the shell does not answer in time (an older
|
|
29
|
+
* shell, or a slow one), and the caller then asks auth-service itself.
|
|
30
|
+
*
|
|
31
|
+
* The answer only decides what the app shows. Every API call still carries the
|
|
32
|
+
* session cookie and is checked by the service it goes to.
|
|
33
|
+
*
|
|
34
|
+
* Contract lives in @tumbaland/components `routing/messages` (app:auth-request
|
|
35
|
+
* and shell:auth); a lower layer cannot import it, so the literals are repeated.
|
|
36
|
+
*/
|
|
37
|
+
function askShellForAuth(shellOrigin) {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
40
|
+
const finish = (result) => {
|
|
41
|
+
window.clearTimeout(timer);
|
|
42
|
+
window.removeEventListener('message', onMessage);
|
|
43
|
+
resolve(result);
|
|
44
|
+
};
|
|
45
|
+
const onMessage = (event) => {
|
|
46
|
+
if (event.origin !== shellOrigin || event.source !== window.parent)
|
|
47
|
+
return;
|
|
48
|
+
const data = event.data;
|
|
49
|
+
if (!data || data.source !== 'tumbaland-shell' || data.type !== 'shell:auth' || data.id !== id)
|
|
50
|
+
return;
|
|
51
|
+
finish({ authenticated: data.authenticated === true, user: data.user ?? null });
|
|
52
|
+
};
|
|
53
|
+
const timer = window.setTimeout(() => {
|
|
54
|
+
logger.warn('Shell did not answer the auth check; asking auth-service instead');
|
|
55
|
+
finish(null);
|
|
56
|
+
}, SHELL_AUTH_TIMEOUT_MS);
|
|
57
|
+
window.addEventListener('message', onMessage);
|
|
58
|
+
window.parent.postMessage({ source: 'tumbaland-app', type: 'app:auth-request', id }, shellOrigin);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
4
61
|
/**
|
|
5
62
|
* Tell the shell its session just ended.
|
|
6
63
|
*
|
|
@@ -52,10 +109,51 @@ function authHeaders() {
|
|
|
52
109
|
export function createAuthService(config) {
|
|
53
110
|
let authCache = null;
|
|
54
111
|
let authCacheTime = 0;
|
|
112
|
+
let pendingCheck = null;
|
|
113
|
+
// Bumped by clearAuthCache, so a check already in flight (say, across a
|
|
114
|
+
// logout) cannot put its stale answer back into the cache.
|
|
115
|
+
let cacheGeneration = 0;
|
|
116
|
+
/** Inside the shell, the shell's answer; otherwise (or if it does not answer) auth-service's. */
|
|
117
|
+
const resolveAuth = async () => {
|
|
118
|
+
const generation = cacheGeneration;
|
|
119
|
+
const remember = (result) => {
|
|
120
|
+
if (generation === cacheGeneration) {
|
|
121
|
+
authCache = result;
|
|
122
|
+
authCacheTime = Date.now();
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
};
|
|
126
|
+
const shellOrigin = embeddingShellOrigin(config.getShellUrl?.());
|
|
127
|
+
if (shellOrigin) {
|
|
128
|
+
const fromShell = await askShellForAuth(shellOrigin);
|
|
129
|
+
if (fromShell)
|
|
130
|
+
return remember(fromShell);
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetch(`${config.getAuthServiceUrl()}/auth/verify`, {
|
|
134
|
+
credentials: 'include',
|
|
135
|
+
headers: authHeaders()
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
139
|
+
}
|
|
140
|
+
const data = await response.json();
|
|
141
|
+
return remember({
|
|
142
|
+
authenticated: data.success && data.authenticated,
|
|
143
|
+
user: data.user || null
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
logger.error('Auth check failed', error);
|
|
148
|
+
return remember({ authenticated: false, user: null });
|
|
149
|
+
}
|
|
150
|
+
};
|
|
55
151
|
return {
|
|
56
152
|
clearAuthCache() {
|
|
57
153
|
authCache = null;
|
|
58
154
|
authCacheTime = 0;
|
|
155
|
+
pendingCheck = null;
|
|
156
|
+
cacheGeneration += 1;
|
|
59
157
|
},
|
|
60
158
|
/** Verifies the session against auth-service. Cached for 5 minutes. */
|
|
61
159
|
async checkAuth() {
|
|
@@ -63,30 +161,13 @@ export function createAuthService(config) {
|
|
|
63
161
|
if (authCache && now - authCacheTime < AUTH_CACHE_TTL) {
|
|
64
162
|
return authCache;
|
|
65
163
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
164
|
+
// Callers that ask at the same time share one check.
|
|
165
|
+
if (!pendingCheck) {
|
|
166
|
+
pendingCheck = resolveAuth().finally(() => {
|
|
167
|
+
pendingCheck = null;
|
|
70
168
|
});
|
|
71
|
-
if (!response.ok) {
|
|
72
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
73
|
-
}
|
|
74
|
-
const data = await response.json();
|
|
75
|
-
const result = {
|
|
76
|
-
authenticated: data.success && data.authenticated,
|
|
77
|
-
user: data.user || null
|
|
78
|
-
};
|
|
79
|
-
authCache = result;
|
|
80
|
-
authCacheTime = now;
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
logger.error('Auth check failed', error);
|
|
85
|
-
const result = { authenticated: false, user: null };
|
|
86
|
-
authCache = result;
|
|
87
|
-
authCacheTime = now;
|
|
88
|
-
return result;
|
|
89
169
|
}
|
|
170
|
+
return pendingCheck;
|
|
90
171
|
},
|
|
91
172
|
redirectToLogin(returnUrl) {
|
|
92
173
|
const currentUrl = returnUrl || window.location.href;
|
|
@@ -104,8 +185,7 @@ export function createAuthService(config) {
|
|
|
104
185
|
}
|
|
105
186
|
const data = await response.json();
|
|
106
187
|
if (data.success) {
|
|
107
|
-
|
|
108
|
-
authCacheTime = 0;
|
|
188
|
+
this.clearAuthCache();
|
|
109
189
|
clearSessionScopedStorage();
|
|
110
190
|
notifyShellOfLogout();
|
|
111
191
|
window.location.reload();
|
|
@@ -149,7 +229,7 @@ export function createAuthService(config) {
|
|
|
149
229
|
}
|
|
150
230
|
/**
|
|
151
231
|
* Convenience over `createAuthService` for the frontends, which all read the
|
|
152
|
-
* same
|
|
232
|
+
* same auth URLs (and the shell's) from their resolved config. Centralizes that key mapping
|
|
153
233
|
* so each app's `services/authService.ts` is just:
|
|
154
234
|
*
|
|
155
235
|
* export const authService = createAppAuthService(getGlobalConfig);
|
|
@@ -159,6 +239,7 @@ export function createAuthService(config) {
|
|
|
159
239
|
export function createAppAuthService(getGlobalConfig) {
|
|
160
240
|
return createAuthService({
|
|
161
241
|
getAuthServiceUrl: () => getGlobalConfig().AUTH_SERVICE_URL,
|
|
162
|
-
getAuthFrontUrl: () => getGlobalConfig().AUTH_FRONT_URL
|
|
242
|
+
getAuthFrontUrl: () => getGlobalConfig().AUTH_FRONT_URL,
|
|
243
|
+
getShellUrl: () => getGlobalConfig().SHELL_FRONT_URL
|
|
163
244
|
});
|
|
164
245
|
}
|