@alepha/react 0.6.0 → 0.6.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/index.browser.cjs +0 -0
- package/dist/index.browser.js +0 -0
- package/dist/index.cjs +11 -14
- package/dist/index.d.ts +2 -2
- package/dist/index.js +11 -14
- package/package.json +4 -4
- package/src/components/Link.tsx +0 -22
- package/src/components/NestedView.tsx +0 -36
- package/src/constants/SSID.ts +0 -1
- package/src/contexts/RouterContext.ts +0 -15
- package/src/contexts/RouterLayerContext.ts +0 -10
- package/src/descriptors/$auth.ts +0 -28
- package/src/descriptors/$page.ts +0 -144
- package/src/errors/RedirectionError.ts +0 -7
- package/src/hooks/RouterHookApi.ts +0 -154
- package/src/hooks/useActive.ts +0 -57
- package/src/hooks/useAuth.ts +0 -29
- package/src/hooks/useClient.ts +0 -6
- package/src/hooks/useInject.ts +0 -12
- package/src/hooks/useQueryParams.ts +0 -59
- package/src/hooks/useRouter.ts +0 -28
- package/src/hooks/useRouterEvents.ts +0 -43
- package/src/hooks/useRouterState.ts +0 -23
- package/src/index.browser.ts +0 -21
- package/src/index.shared.ts +0 -28
- package/src/index.ts +0 -46
- package/src/providers/PageDescriptorProvider.ts +0 -52
- package/src/providers/ReactAuthProvider.ts +0 -410
- package/src/providers/ReactBrowserProvider.ts +0 -250
- package/src/providers/ReactServerProvider.ts +0 -301
- package/src/services/Auth.ts +0 -45
- package/src/services/Router.ts +0 -855
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
import { $hook, $inject, $logger, Alepha, t } from "@alepha/core";
|
|
2
|
-
import {
|
|
3
|
-
$cookie,
|
|
4
|
-
$route,
|
|
5
|
-
BadRequestError,
|
|
6
|
-
type CookieManager,
|
|
7
|
-
FastifyCookieProvider,
|
|
8
|
-
} from "@alepha/server";
|
|
9
|
-
import {
|
|
10
|
-
type Configuration,
|
|
11
|
-
allowInsecureRequests,
|
|
12
|
-
authorizationCodeGrant,
|
|
13
|
-
buildAuthorizationUrl,
|
|
14
|
-
buildEndSessionUrl,
|
|
15
|
-
calculatePKCECodeChallenge,
|
|
16
|
-
discovery,
|
|
17
|
-
randomPKCECodeVerifier,
|
|
18
|
-
refreshTokenGrant,
|
|
19
|
-
} from "openid-client";
|
|
20
|
-
import { $auth } from "../descriptors/$auth";
|
|
21
|
-
import type { PreviousLayerData } from "../services/Router";
|
|
22
|
-
|
|
23
|
-
export class ReactAuthProvider {
|
|
24
|
-
protected readonly log = $logger();
|
|
25
|
-
protected readonly alepha = $inject(Alepha);
|
|
26
|
-
protected readonly fastifyCookieProvider = $inject(FastifyCookieProvider);
|
|
27
|
-
protected authProviders: AuthProvider[] = [];
|
|
28
|
-
|
|
29
|
-
protected readonly authorizationCode = $cookie({
|
|
30
|
-
name: "authorizationCode",
|
|
31
|
-
ttl: { minutes: 15 },
|
|
32
|
-
httpOnly: true,
|
|
33
|
-
schema: t.object({
|
|
34
|
-
codeVerifier: t.optional(t.string({ size: "long" })),
|
|
35
|
-
redirectUri: t.optional(t.string({ size: "long" })),
|
|
36
|
-
}),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
protected readonly tokens = $cookie({
|
|
40
|
-
name: "tokens",
|
|
41
|
-
ttl: { days: 1 },
|
|
42
|
-
httpOnly: true,
|
|
43
|
-
compress: true,
|
|
44
|
-
schema: t.object({
|
|
45
|
-
access_token: t.optional(t.string({ size: "rich" })),
|
|
46
|
-
expires_in: t.optional(t.number()),
|
|
47
|
-
refresh_token: t.optional(t.string({ size: "rich" })),
|
|
48
|
-
id_token: t.optional(t.string({ size: "rich" })),
|
|
49
|
-
scope: t.optional(t.string()),
|
|
50
|
-
issued_at: t.optional(t.number()),
|
|
51
|
-
}),
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
protected readonly user = $cookie({
|
|
55
|
-
name: "user",
|
|
56
|
-
ttl: { days: 1 },
|
|
57
|
-
schema: t.object({
|
|
58
|
-
id: t.string(),
|
|
59
|
-
name: t.optional(t.string()),
|
|
60
|
-
email: t.optional(t.string()),
|
|
61
|
-
}),
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
protected readonly configure = $hook({
|
|
65
|
-
name: "configure",
|
|
66
|
-
handler: async () => {
|
|
67
|
-
const auths = this.alepha.getDescriptorValues($auth);
|
|
68
|
-
for (const auth of auths) {
|
|
69
|
-
const options = auth.value.options;
|
|
70
|
-
if (options.oidc) {
|
|
71
|
-
this.authProviders.push({
|
|
72
|
-
name: options.name ?? auth.key,
|
|
73
|
-
redirectUri: options.oidc.redirectUri ?? "/api/_oauth/callback",
|
|
74
|
-
client: await discovery(
|
|
75
|
-
new URL(options.oidc.issuer),
|
|
76
|
-
options.oidc.clientId,
|
|
77
|
-
{
|
|
78
|
-
client_secret: options.oidc.clientSecret,
|
|
79
|
-
},
|
|
80
|
-
undefined,
|
|
81
|
-
{
|
|
82
|
-
execute: [allowInsecureRequests],
|
|
83
|
-
},
|
|
84
|
-
),
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Configure Fastify to forward Session Access Token to Header Authorization.
|
|
93
|
-
*/
|
|
94
|
-
protected readonly configureFastify = $hook({
|
|
95
|
-
name: "configure:fastify",
|
|
96
|
-
after: this.fastifyCookieProvider,
|
|
97
|
-
handler: async (app) => {
|
|
98
|
-
app.addHook("onRequest", async (req) => {
|
|
99
|
-
if (
|
|
100
|
-
req.cookies &&
|
|
101
|
-
!this.isViteFile(req.url) &&
|
|
102
|
-
!!this.authProviders.length
|
|
103
|
-
) {
|
|
104
|
-
const tokens = await this.refresh(req.cookies);
|
|
105
|
-
if (tokens) {
|
|
106
|
-
req.headers.authorization = `Bearer ${tokens.access_token}`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (this.user.get(req.cookies) && !this.tokens.get(req.cookies)) {
|
|
110
|
-
this.user.del(req.cookies);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
*
|
|
119
|
-
* @param cookies
|
|
120
|
-
* @protected
|
|
121
|
-
*/
|
|
122
|
-
protected async refresh(
|
|
123
|
-
cookies: CookieManager,
|
|
124
|
-
): Promise<SessionTokens | undefined> {
|
|
125
|
-
const now = Date.now();
|
|
126
|
-
const tokens = this.tokens.get(cookies);
|
|
127
|
-
if (!tokens) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (tokens.expires_in && tokens.issued_at) {
|
|
132
|
-
const expiresAt = tokens.issued_at + (tokens.expires_in - 10) * 1000;
|
|
133
|
-
if (expiresAt < now) {
|
|
134
|
-
// is expired
|
|
135
|
-
if (tokens.refresh_token) {
|
|
136
|
-
// but has refresh token
|
|
137
|
-
try {
|
|
138
|
-
const newTokens = await refreshTokenGrant(
|
|
139
|
-
this.authProviders[0].client,
|
|
140
|
-
tokens.refresh_token,
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
this.tokens.set(cookies, {
|
|
144
|
-
...newTokens,
|
|
145
|
-
issued_at: Date.now(),
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
return newTokens;
|
|
149
|
-
} catch (e) {
|
|
150
|
-
this.log.warn(e, "Failed to refresh token -");
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// session expired and no (valid) refresh token
|
|
155
|
-
this.tokens.del(cookies);
|
|
156
|
-
this.user.del(cookies);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (!tokens.issued_at && tokens.access_token) {
|
|
162
|
-
this.tokens.del(cookies);
|
|
163
|
-
this.user.del(cookies);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return tokens;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
*
|
|
172
|
-
*/
|
|
173
|
-
public readonly login = $route({
|
|
174
|
-
security: false,
|
|
175
|
-
private: true,
|
|
176
|
-
url: "/_oauth/login",
|
|
177
|
-
group: "auth",
|
|
178
|
-
method: "GET",
|
|
179
|
-
schema: {
|
|
180
|
-
query: t.object({
|
|
181
|
-
redirect: t.optional(t.string()),
|
|
182
|
-
provider: t.optional(t.string()),
|
|
183
|
-
}),
|
|
184
|
-
},
|
|
185
|
-
handler: async ({ query, cookies, url }) => {
|
|
186
|
-
const { client } = this.provider(query.provider);
|
|
187
|
-
|
|
188
|
-
const codeVerifier = randomPKCECodeVerifier();
|
|
189
|
-
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
|
190
|
-
const scope = "openid profile email";
|
|
191
|
-
|
|
192
|
-
let redirect_uri = this.authProviders[0].redirectUri;
|
|
193
|
-
if (redirect_uri.startsWith("/")) {
|
|
194
|
-
redirect_uri = `${url.protocol}://${url.host}${redirect_uri}`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const parameters: Record<string, string> = {
|
|
198
|
-
redirect_uri,
|
|
199
|
-
scope,
|
|
200
|
-
code_challenge: codeChallenge,
|
|
201
|
-
code_challenge_method: "S256",
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
this.authorizationCode.set(cookies, {
|
|
205
|
-
codeVerifier,
|
|
206
|
-
redirectUri: query.redirect ?? "/",
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
return new Response("", {
|
|
210
|
-
status: 302,
|
|
211
|
-
headers: {
|
|
212
|
-
Location: buildAuthorizationUrl(client, parameters).toString(),
|
|
213
|
-
},
|
|
214
|
-
});
|
|
215
|
-
},
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
*
|
|
220
|
-
*/
|
|
221
|
-
public readonly callback = $route({
|
|
222
|
-
security: false,
|
|
223
|
-
private: true,
|
|
224
|
-
url: "/_oauth/callback",
|
|
225
|
-
group: "auth",
|
|
226
|
-
method: "GET",
|
|
227
|
-
schema: {
|
|
228
|
-
query: t.object({
|
|
229
|
-
provider: t.optional(t.string()),
|
|
230
|
-
}),
|
|
231
|
-
},
|
|
232
|
-
handler: async ({ url, cookies, query }) => {
|
|
233
|
-
const { client } = this.provider(query.provider);
|
|
234
|
-
|
|
235
|
-
const authorizationCode = this.authorizationCode.get(cookies);
|
|
236
|
-
if (!authorizationCode) {
|
|
237
|
-
throw new BadRequestError("Missing code verifier");
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const tokens = await authorizationCodeGrant(client, url, {
|
|
241
|
-
pkceCodeVerifier: authorizationCode.codeVerifier,
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
this.authorizationCode.del(cookies);
|
|
245
|
-
|
|
246
|
-
this.tokens.set(cookies, {
|
|
247
|
-
...tokens,
|
|
248
|
-
issued_at: Date.now(),
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
const user = this.userFromAccessToken(tokens.access_token);
|
|
252
|
-
if (user) {
|
|
253
|
-
this.user.set(cookies, user);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return Response.redirect(authorizationCode.redirectUri ?? "/");
|
|
257
|
-
},
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
*
|
|
262
|
-
* @param accessToken
|
|
263
|
-
* @protected
|
|
264
|
-
*/
|
|
265
|
-
protected userFromAccessToken(accessToken: string) {
|
|
266
|
-
try {
|
|
267
|
-
const parts = accessToken.split(".");
|
|
268
|
-
if (parts.length !== 3) {
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const payload = parts[1];
|
|
273
|
-
const decoded = JSON.parse(atob(payload));
|
|
274
|
-
if (!decoded.sub) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
id: decoded.sub,
|
|
280
|
-
name: decoded.name,
|
|
281
|
-
email: decoded.email,
|
|
282
|
-
// organization
|
|
283
|
-
// ...
|
|
284
|
-
};
|
|
285
|
-
} catch (e) {
|
|
286
|
-
this.log.warn(e, "Failed to decode access token");
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
*
|
|
292
|
-
*/
|
|
293
|
-
public readonly logout = $route({
|
|
294
|
-
security: false,
|
|
295
|
-
private: true,
|
|
296
|
-
url: "/_oauth/logout",
|
|
297
|
-
group: "auth",
|
|
298
|
-
method: "GET",
|
|
299
|
-
schema: {
|
|
300
|
-
query: t.object({
|
|
301
|
-
redirect: t.optional(t.string()),
|
|
302
|
-
provider: t.optional(t.string()),
|
|
303
|
-
}),
|
|
304
|
-
},
|
|
305
|
-
handler: async ({ query, cookies }) => {
|
|
306
|
-
const { client } = this.provider(query.provider);
|
|
307
|
-
const tokens = this.tokens.get(cookies);
|
|
308
|
-
const idToken = tokens?.id_token;
|
|
309
|
-
|
|
310
|
-
const redirect = query.redirect ?? "/";
|
|
311
|
-
const params = new URLSearchParams();
|
|
312
|
-
|
|
313
|
-
params.set("post_logout_redirect_uri", redirect);
|
|
314
|
-
if (idToken) {
|
|
315
|
-
params.set("id_token_hint", idToken);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
this.tokens.del(cookies);
|
|
319
|
-
this.user.del(cookies);
|
|
320
|
-
|
|
321
|
-
return Response.redirect(buildEndSessionUrl(client, params).toString());
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
*
|
|
327
|
-
* @param name
|
|
328
|
-
* @protected
|
|
329
|
-
*/
|
|
330
|
-
protected provider(name?: string) {
|
|
331
|
-
if (!name) {
|
|
332
|
-
const client = this.authProviders[0];
|
|
333
|
-
if (!client) {
|
|
334
|
-
throw new BadRequestError("Client name is required");
|
|
335
|
-
}
|
|
336
|
-
return client;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const authProvider = this.authProviders.find(
|
|
340
|
-
(provider) => provider.name === name,
|
|
341
|
-
);
|
|
342
|
-
if (!authProvider) {
|
|
343
|
-
throw new BadRequestError(`Client ${name} not found`);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return authProvider;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
*
|
|
351
|
-
* @param file
|
|
352
|
-
* @protected
|
|
353
|
-
*/
|
|
354
|
-
protected isViteFile(file: string) {
|
|
355
|
-
const [pathname] = file.split("?");
|
|
356
|
-
|
|
357
|
-
// swagger
|
|
358
|
-
if (pathname.startsWith("/docs")) {
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// static assets
|
|
363
|
-
if (pathname.match(/\.\w{2,5}$/)) {
|
|
364
|
-
return true;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// vite internal files
|
|
368
|
-
if (pathname.startsWith("/@")) {
|
|
369
|
-
return true;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// our backend files
|
|
373
|
-
return false;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
export interface SessionTokens {
|
|
378
|
-
access_token?: string;
|
|
379
|
-
expires_in?: number;
|
|
380
|
-
refresh_token?: string;
|
|
381
|
-
id_token?: string;
|
|
382
|
-
scope?: string;
|
|
383
|
-
issued_at?: number;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export interface SessionAuthorizationCode {
|
|
387
|
-
codeVerifier?: string;
|
|
388
|
-
redirectUri?: string;
|
|
389
|
-
nonce?: string;
|
|
390
|
-
max_age?: number;
|
|
391
|
-
state?: string;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export interface AuthProvider {
|
|
395
|
-
name: string;
|
|
396
|
-
redirectUri: string;
|
|
397
|
-
client: Configuration;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
export interface ReactUser {
|
|
401
|
-
id: string;
|
|
402
|
-
name?: string;
|
|
403
|
-
email?: string;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
export interface ReactHydrationState {
|
|
407
|
-
user?: ReactUser;
|
|
408
|
-
auth?: "server" | "client";
|
|
409
|
-
layers?: PreviousLayerData[];
|
|
410
|
-
}
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
import { $hook, $inject, $logger } from "@alepha/core";
|
|
2
|
-
import type { UserAccountToken } from "@alepha/security";
|
|
3
|
-
import { HttpClient } from "@alepha/server";
|
|
4
|
-
import type { Root } from "react-dom/client";
|
|
5
|
-
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
6
|
-
import type {
|
|
7
|
-
PreviousLayerData,
|
|
8
|
-
RouterMatchOptions,
|
|
9
|
-
RouterState,
|
|
10
|
-
} from "../services/Router";
|
|
11
|
-
import { Router } from "../services/Router";
|
|
12
|
-
import type { RouterRenderHelmetContext } from "../services/Router";
|
|
13
|
-
import type { ReactHydrationState } from "./ReactAuthProvider";
|
|
14
|
-
|
|
15
|
-
export class ReactBrowserProvider {
|
|
16
|
-
protected readonly log = $logger();
|
|
17
|
-
protected readonly client = $inject(HttpClient);
|
|
18
|
-
protected readonly router = $inject(Router);
|
|
19
|
-
protected root!: Root;
|
|
20
|
-
|
|
21
|
-
public transitioning?: {
|
|
22
|
-
to: string;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
public state: RouterState = {
|
|
26
|
-
layers: [],
|
|
27
|
-
pathname: "",
|
|
28
|
-
search: "",
|
|
29
|
-
context: {},
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
*
|
|
34
|
-
*/
|
|
35
|
-
public get document() {
|
|
36
|
-
return window.document;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
*/
|
|
42
|
-
public get history() {
|
|
43
|
-
return window.history;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
*/
|
|
49
|
-
public get url(): string {
|
|
50
|
-
return window.location.pathname + window.location.search;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
*
|
|
55
|
-
* @param props
|
|
56
|
-
*/
|
|
57
|
-
public async invalidate(props?: Record<string, any>) {
|
|
58
|
-
const previous: PreviousLayerData[] = [];
|
|
59
|
-
|
|
60
|
-
if (props) {
|
|
61
|
-
const [key] = Object.keys(props);
|
|
62
|
-
const value = props[key];
|
|
63
|
-
|
|
64
|
-
for (const layer of this.state.layers) {
|
|
65
|
-
if (layer.props?.[key]) {
|
|
66
|
-
previous.push({
|
|
67
|
-
...layer,
|
|
68
|
-
props: {
|
|
69
|
-
...layer.props,
|
|
70
|
-
[key]: value,
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
previous.push(layer);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
await this.render({ previous });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
*
|
|
84
|
-
* @param url
|
|
85
|
-
* @param options
|
|
86
|
-
*/
|
|
87
|
-
public async go(url: string, options: RouterGoOptions = {}): Promise<void> {
|
|
88
|
-
const result = await this.render({
|
|
89
|
-
url,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
if (result.url !== url) {
|
|
93
|
-
this.history.replaceState({}, "", result.url);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (options.replace) {
|
|
98
|
-
this.history.replaceState({}, "", url);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
this.history.pushState({}, "", url);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
*
|
|
107
|
-
* @param options
|
|
108
|
-
* @protected
|
|
109
|
-
*/
|
|
110
|
-
protected async render(
|
|
111
|
-
options: {
|
|
112
|
-
url?: string;
|
|
113
|
-
previous?: PreviousLayerData[];
|
|
114
|
-
} = {},
|
|
115
|
-
): Promise<{ url: string }> {
|
|
116
|
-
const previous = options.previous ?? this.state.layers;
|
|
117
|
-
const url = options.url ?? this.url;
|
|
118
|
-
|
|
119
|
-
this.transitioning = { to: url };
|
|
120
|
-
|
|
121
|
-
const result = await this.router.render(url, {
|
|
122
|
-
previous,
|
|
123
|
-
state: this.state,
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
if (result.redirect) {
|
|
127
|
-
return await this.render({ url: result.redirect });
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
this.transitioning = undefined;
|
|
131
|
-
|
|
132
|
-
return { url };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
protected renderHelmetContext(ctx: RouterRenderHelmetContext) {
|
|
136
|
-
if (ctx.title) {
|
|
137
|
-
this.document.title = ctx.title;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Get embedded layers from the server.
|
|
143
|
-
*
|
|
144
|
-
* @protected
|
|
145
|
-
*/
|
|
146
|
-
protected getHydrationState(): ReactHydrationState | undefined {
|
|
147
|
-
try {
|
|
148
|
-
if ("__ssr" in window && typeof window.__ssr === "object") {
|
|
149
|
-
return window.__ssr as ReactHydrationState;
|
|
150
|
-
}
|
|
151
|
-
} catch (error) {
|
|
152
|
-
console.error(error);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
*
|
|
158
|
-
* @protected
|
|
159
|
-
*/
|
|
160
|
-
protected getRootElement() {
|
|
161
|
-
const root = this.document.getElementById("root");
|
|
162
|
-
if (root) {
|
|
163
|
-
return root;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const div = this.document.createElement("div");
|
|
167
|
-
div.id = "root";
|
|
168
|
-
|
|
169
|
-
this.document.body.appendChild(div);
|
|
170
|
-
|
|
171
|
-
return div;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
protected getUserFromCookies(): UserAccountToken | undefined {
|
|
175
|
-
const cookies = this.document.cookie.split("; ");
|
|
176
|
-
const userCookie = cookies.find((cookie) => cookie.startsWith("user="));
|
|
177
|
-
try {
|
|
178
|
-
if (userCookie) {
|
|
179
|
-
return JSON.parse(decodeURIComponent(userCookie.split("=")[1]));
|
|
180
|
-
}
|
|
181
|
-
} catch (error) {
|
|
182
|
-
this.log.warn(error, "Failed to parse user cookie");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return undefined;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
*
|
|
192
|
-
* @protected
|
|
193
|
-
*/
|
|
194
|
-
protected ready = $hook({
|
|
195
|
-
name: "ready",
|
|
196
|
-
handler: async () => {
|
|
197
|
-
const cache = this.getHydrationState();
|
|
198
|
-
const previous = cache?.layers ?? [];
|
|
199
|
-
|
|
200
|
-
await this.render({ previous });
|
|
201
|
-
|
|
202
|
-
const element = this.router.root(this.state, {
|
|
203
|
-
user: cache?.user ?? this.getUserFromCookies(),
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
if (previous.length > 0) {
|
|
207
|
-
this.root = hydrateRoot(this.getRootElement(), element);
|
|
208
|
-
this.log.info("Hydrated root element");
|
|
209
|
-
} else {
|
|
210
|
-
this.root = createRoot(this.getRootElement());
|
|
211
|
-
this.root.render(element);
|
|
212
|
-
this.log.info("Created root element");
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
window.addEventListener("popstate", () => {
|
|
216
|
-
this.render();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
this.router.on("end", ({ context }) => {
|
|
220
|
-
if (context.helmet) {
|
|
221
|
-
this.renderHelmetContext(context.helmet);
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
*
|
|
229
|
-
* @protected
|
|
230
|
-
*/
|
|
231
|
-
protected stop = $hook({
|
|
232
|
-
name: "stop",
|
|
233
|
-
handler: async () => {
|
|
234
|
-
if (this.root) {
|
|
235
|
-
this.root.unmount();
|
|
236
|
-
this.log.info("Unmounted root element");
|
|
237
|
-
}
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ---------------------------------------------------------------------------------------------------------------------
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
*
|
|
246
|
-
*/
|
|
247
|
-
export interface RouterGoOptions {
|
|
248
|
-
replace?: boolean;
|
|
249
|
-
match?: RouterMatchOptions;
|
|
250
|
-
}
|