@alepha/react 0.5.1 → 0.6.0
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 +23 -19
- package/dist/index.browser.js +18 -0
- package/dist/index.cjs +419 -340
- package/dist/{index.d.cts → index.d.ts} +591 -420
- package/dist/index.js +554 -0
- package/dist/{useRouterState-BlKHWZwk.cjs → useAuth-DOVx2kqa.cjs} +243 -102
- package/dist/{useRouterState-CvFCmaq7.mjs → useAuth-i7wbKVrt.js} +214 -77
- package/package.json +21 -23
- package/src/components/Link.tsx +22 -0
- package/src/components/NestedView.tsx +2 -2
- package/src/constants/SSID.ts +1 -0
- package/src/contexts/RouterContext.ts +2 -2
- package/src/descriptors/$auth.ts +28 -0
- package/src/descriptors/$page.ts +57 -3
- package/src/errors/RedirectionError.ts +7 -0
- package/src/hooks/useAuth.ts +29 -0
- package/src/hooks/useInject.ts +3 -3
- package/src/index.browser.ts +3 -1
- package/src/index.shared.ts +14 -3
- package/src/index.ts +23 -6
- package/src/providers/ReactAuthProvider.ts +410 -0
- package/src/providers/ReactBrowserProvider.ts +41 -19
- package/src/providers/ReactServerProvider.ts +106 -49
- package/src/services/Auth.ts +45 -0
- package/src/services/Router.ts +154 -41
- package/dist/index.browser.mjs +0 -17
- package/dist/index.d.mts +0 -872
- package/dist/index.mjs +0 -477
- package/src/providers/ReactSessionProvider.ts +0 -363
package/dist/index.js
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { $logger, $inject, Alepha, t, $hook, autoInject } from '@alepha/core';
|
|
2
|
+
import { FastifyCookieProvider, $cookie, $route, BadRequestError, ServerProvider, ServerModule, ServerLinksProvider } from '@alepha/server';
|
|
3
|
+
import { $ as $auth, R as Router, a as $page, A as Auth, P as PageDescriptorProvider } from './useAuth-i7wbKVrt.js';
|
|
4
|
+
export { L as Link, N as NestedView, l as ReactBrowserProvider, m as RedirectionError, b as RouterContext, d as RouterHookApi, c as RouterLayerContext, p as pageDescriptorKey, j as useActive, k as useAuth, e as useClient, u as useInject, f as useQueryParams, g as useRouter, h as useRouterEvents, i as useRouterState } from './useAuth-i7wbKVrt.js';
|
|
5
|
+
import { discovery, allowInsecureRequests, refreshTokenGrant, randomPKCECodeVerifier, calculatePKCECodeChallenge, buildAuthorizationUrl, authorizationCodeGrant, buildEndSessionUrl } from 'openid-client';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { renderToString } from 'react-dom/server';
|
|
10
|
+
import 'react/jsx-runtime';
|
|
11
|
+
import 'react';
|
|
12
|
+
import 'react-dom/client';
|
|
13
|
+
import 'path-to-regexp';
|
|
14
|
+
|
|
15
|
+
class ReactAuthProvider {
|
|
16
|
+
log = $logger();
|
|
17
|
+
alepha = $inject(Alepha);
|
|
18
|
+
fastifyCookieProvider = $inject(FastifyCookieProvider);
|
|
19
|
+
authProviders = [];
|
|
20
|
+
authorizationCode = $cookie({
|
|
21
|
+
name: "authorizationCode",
|
|
22
|
+
ttl: { minutes: 15 },
|
|
23
|
+
httpOnly: true,
|
|
24
|
+
schema: t.object({
|
|
25
|
+
codeVerifier: t.optional(t.string({ size: "long" })),
|
|
26
|
+
redirectUri: t.optional(t.string({ size: "long" }))
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
tokens = $cookie({
|
|
30
|
+
name: "tokens",
|
|
31
|
+
ttl: { days: 1 },
|
|
32
|
+
httpOnly: true,
|
|
33
|
+
compress: true,
|
|
34
|
+
schema: t.object({
|
|
35
|
+
access_token: t.optional(t.string({ size: "rich" })),
|
|
36
|
+
expires_in: t.optional(t.number()),
|
|
37
|
+
refresh_token: t.optional(t.string({ size: "rich" })),
|
|
38
|
+
id_token: t.optional(t.string({ size: "rich" })),
|
|
39
|
+
scope: t.optional(t.string()),
|
|
40
|
+
issued_at: t.optional(t.number())
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
user = $cookie({
|
|
44
|
+
name: "user",
|
|
45
|
+
ttl: { days: 1 },
|
|
46
|
+
schema: t.object({
|
|
47
|
+
id: t.string(),
|
|
48
|
+
name: t.optional(t.string()),
|
|
49
|
+
email: t.optional(t.string())
|
|
50
|
+
})
|
|
51
|
+
});
|
|
52
|
+
configure = $hook({
|
|
53
|
+
name: "configure",
|
|
54
|
+
handler: async () => {
|
|
55
|
+
const auths = this.alepha.getDescriptorValues($auth);
|
|
56
|
+
for (const auth of auths) {
|
|
57
|
+
const options = auth.value.options;
|
|
58
|
+
if (options.oidc) {
|
|
59
|
+
this.authProviders.push({
|
|
60
|
+
name: options.name ?? auth.key,
|
|
61
|
+
redirectUri: options.oidc.redirectUri ?? "/api/_oauth/callback",
|
|
62
|
+
client: await discovery(
|
|
63
|
+
new URL(options.oidc.issuer),
|
|
64
|
+
options.oidc.clientId,
|
|
65
|
+
{
|
|
66
|
+
client_secret: options.oidc.clientSecret
|
|
67
|
+
},
|
|
68
|
+
void 0,
|
|
69
|
+
{
|
|
70
|
+
execute: [allowInsecureRequests]
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* Configure Fastify to forward Session Access Token to Header Authorization.
|
|
80
|
+
*/
|
|
81
|
+
configureFastify = $hook({
|
|
82
|
+
name: "configure:fastify",
|
|
83
|
+
after: this.fastifyCookieProvider,
|
|
84
|
+
handler: async (app) => {
|
|
85
|
+
app.addHook("onRequest", async (req) => {
|
|
86
|
+
if (req.cookies && !this.isViteFile(req.url) && !!this.authProviders.length) {
|
|
87
|
+
const tokens = await this.refresh(req.cookies);
|
|
88
|
+
if (tokens) {
|
|
89
|
+
req.headers.authorization = `Bearer ${tokens.access_token}`;
|
|
90
|
+
}
|
|
91
|
+
if (this.user.get(req.cookies) && !this.tokens.get(req.cookies)) {
|
|
92
|
+
this.user.del(req.cookies);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
/**
|
|
99
|
+
*
|
|
100
|
+
* @param cookies
|
|
101
|
+
* @protected
|
|
102
|
+
*/
|
|
103
|
+
async refresh(cookies) {
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
const tokens = this.tokens.get(cookies);
|
|
106
|
+
if (!tokens) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (tokens.expires_in && tokens.issued_at) {
|
|
110
|
+
const expiresAt = tokens.issued_at + (tokens.expires_in - 10) * 1e3;
|
|
111
|
+
if (expiresAt < now) {
|
|
112
|
+
if (tokens.refresh_token) {
|
|
113
|
+
try {
|
|
114
|
+
const newTokens = await refreshTokenGrant(
|
|
115
|
+
this.authProviders[0].client,
|
|
116
|
+
tokens.refresh_token
|
|
117
|
+
);
|
|
118
|
+
this.tokens.set(cookies, {
|
|
119
|
+
...newTokens,
|
|
120
|
+
issued_at: Date.now()
|
|
121
|
+
});
|
|
122
|
+
return newTokens;
|
|
123
|
+
} catch (e) {
|
|
124
|
+
this.log.warn(e, "Failed to refresh token -");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
this.tokens.del(cookies);
|
|
128
|
+
this.user.del(cookies);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!tokens.issued_at && tokens.access_token) {
|
|
133
|
+
this.tokens.del(cookies);
|
|
134
|
+
this.user.del(cookies);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
return tokens;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
*
|
|
141
|
+
*/
|
|
142
|
+
login = $route({
|
|
143
|
+
security: false,
|
|
144
|
+
private: true,
|
|
145
|
+
url: "/_oauth/login",
|
|
146
|
+
group: "auth",
|
|
147
|
+
method: "GET",
|
|
148
|
+
schema: {
|
|
149
|
+
query: t.object({
|
|
150
|
+
redirect: t.optional(t.string()),
|
|
151
|
+
provider: t.optional(t.string())
|
|
152
|
+
})
|
|
153
|
+
},
|
|
154
|
+
handler: async ({ query, cookies, url }) => {
|
|
155
|
+
const { client } = this.provider(query.provider);
|
|
156
|
+
const codeVerifier = randomPKCECodeVerifier();
|
|
157
|
+
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
|
158
|
+
const scope = "openid profile email";
|
|
159
|
+
let redirect_uri = this.authProviders[0].redirectUri;
|
|
160
|
+
if (redirect_uri.startsWith("/")) {
|
|
161
|
+
redirect_uri = `${url.protocol}://${url.host}${redirect_uri}`;
|
|
162
|
+
}
|
|
163
|
+
const parameters = {
|
|
164
|
+
redirect_uri,
|
|
165
|
+
scope,
|
|
166
|
+
code_challenge: codeChallenge,
|
|
167
|
+
code_challenge_method: "S256"
|
|
168
|
+
};
|
|
169
|
+
this.authorizationCode.set(cookies, {
|
|
170
|
+
codeVerifier,
|
|
171
|
+
redirectUri: query.redirect ?? "/"
|
|
172
|
+
});
|
|
173
|
+
return new Response("", {
|
|
174
|
+
status: 302,
|
|
175
|
+
headers: {
|
|
176
|
+
Location: buildAuthorizationUrl(client, parameters).toString()
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
/**
|
|
182
|
+
*
|
|
183
|
+
*/
|
|
184
|
+
callback = $route({
|
|
185
|
+
security: false,
|
|
186
|
+
private: true,
|
|
187
|
+
url: "/_oauth/callback",
|
|
188
|
+
group: "auth",
|
|
189
|
+
method: "GET",
|
|
190
|
+
schema: {
|
|
191
|
+
query: t.object({
|
|
192
|
+
provider: t.optional(t.string())
|
|
193
|
+
})
|
|
194
|
+
},
|
|
195
|
+
handler: async ({ url, cookies, query }) => {
|
|
196
|
+
const { client } = this.provider(query.provider);
|
|
197
|
+
const authorizationCode = this.authorizationCode.get(cookies);
|
|
198
|
+
if (!authorizationCode) {
|
|
199
|
+
throw new BadRequestError("Missing code verifier");
|
|
200
|
+
}
|
|
201
|
+
const tokens = await authorizationCodeGrant(client, url, {
|
|
202
|
+
pkceCodeVerifier: authorizationCode.codeVerifier
|
|
203
|
+
});
|
|
204
|
+
this.authorizationCode.del(cookies);
|
|
205
|
+
this.tokens.set(cookies, {
|
|
206
|
+
...tokens,
|
|
207
|
+
issued_at: Date.now()
|
|
208
|
+
});
|
|
209
|
+
const user = this.userFromAccessToken(tokens.access_token);
|
|
210
|
+
if (user) {
|
|
211
|
+
this.user.set(cookies, user);
|
|
212
|
+
}
|
|
213
|
+
return Response.redirect(authorizationCode.redirectUri ?? "/");
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
/**
|
|
217
|
+
*
|
|
218
|
+
* @param accessToken
|
|
219
|
+
* @protected
|
|
220
|
+
*/
|
|
221
|
+
userFromAccessToken(accessToken) {
|
|
222
|
+
try {
|
|
223
|
+
const parts = accessToken.split(".");
|
|
224
|
+
if (parts.length !== 3) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const payload = parts[1];
|
|
228
|
+
const decoded = JSON.parse(atob(payload));
|
|
229
|
+
if (!decoded.sub) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
id: decoded.sub,
|
|
234
|
+
name: decoded.name,
|
|
235
|
+
email: decoded.email
|
|
236
|
+
// organization
|
|
237
|
+
// ...
|
|
238
|
+
};
|
|
239
|
+
} catch (e) {
|
|
240
|
+
this.log.warn(e, "Failed to decode access token");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
*
|
|
245
|
+
*/
|
|
246
|
+
logout = $route({
|
|
247
|
+
security: false,
|
|
248
|
+
private: true,
|
|
249
|
+
url: "/_oauth/logout",
|
|
250
|
+
group: "auth",
|
|
251
|
+
method: "GET",
|
|
252
|
+
schema: {
|
|
253
|
+
query: t.object({
|
|
254
|
+
redirect: t.optional(t.string()),
|
|
255
|
+
provider: t.optional(t.string())
|
|
256
|
+
})
|
|
257
|
+
},
|
|
258
|
+
handler: async ({ query, cookies }) => {
|
|
259
|
+
const { client } = this.provider(query.provider);
|
|
260
|
+
const tokens = this.tokens.get(cookies);
|
|
261
|
+
const idToken = tokens?.id_token;
|
|
262
|
+
const redirect = query.redirect ?? "/";
|
|
263
|
+
const params = new URLSearchParams();
|
|
264
|
+
params.set("post_logout_redirect_uri", redirect);
|
|
265
|
+
if (idToken) {
|
|
266
|
+
params.set("id_token_hint", idToken);
|
|
267
|
+
}
|
|
268
|
+
this.tokens.del(cookies);
|
|
269
|
+
this.user.del(cookies);
|
|
270
|
+
return Response.redirect(buildEndSessionUrl(client, params).toString());
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
/**
|
|
274
|
+
*
|
|
275
|
+
* @param name
|
|
276
|
+
* @protected
|
|
277
|
+
*/
|
|
278
|
+
provider(name) {
|
|
279
|
+
if (!name) {
|
|
280
|
+
const client = this.authProviders[0];
|
|
281
|
+
if (!client) {
|
|
282
|
+
throw new BadRequestError("Client name is required");
|
|
283
|
+
}
|
|
284
|
+
return client;
|
|
285
|
+
}
|
|
286
|
+
const authProvider = this.authProviders.find(
|
|
287
|
+
(provider) => provider.name === name
|
|
288
|
+
);
|
|
289
|
+
if (!authProvider) {
|
|
290
|
+
throw new BadRequestError(`Client ${name} not found`);
|
|
291
|
+
}
|
|
292
|
+
return authProvider;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
*
|
|
296
|
+
* @param file
|
|
297
|
+
* @protected
|
|
298
|
+
*/
|
|
299
|
+
isViteFile(file) {
|
|
300
|
+
const [pathname] = file.split("?");
|
|
301
|
+
if (pathname.startsWith("/docs")) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
if (pathname.match(/\.\w{2,5}$/)) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
if (pathname.startsWith("/@")) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const envSchema$1 = t.object({
|
|
315
|
+
REACT_SERVER_DIST: t.string({ default: "client" }),
|
|
316
|
+
REACT_SERVER_PREFIX: t.string({ default: "" }),
|
|
317
|
+
REACT_SSR_ENABLED: t.boolean({ default: false }),
|
|
318
|
+
REACT_SSR_OUTLET: t.string({ default: "<!--ssr-outlet-->" })
|
|
319
|
+
});
|
|
320
|
+
class ReactServerProvider {
|
|
321
|
+
log = $logger();
|
|
322
|
+
alepha = $inject(Alepha);
|
|
323
|
+
router = $inject(Router);
|
|
324
|
+
server = $inject(ServerProvider);
|
|
325
|
+
env = $inject(envSchema$1);
|
|
326
|
+
configure = $hook({
|
|
327
|
+
name: "configure",
|
|
328
|
+
handler: async () => {
|
|
329
|
+
await this.configureRoutes();
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
async configureRoutes() {
|
|
333
|
+
if (this.alepha.isTest()) {
|
|
334
|
+
this.processDescriptors();
|
|
335
|
+
}
|
|
336
|
+
if (this.router.empty()) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (process.env.VITE_ALEPHA_DEV === "true") {
|
|
340
|
+
this.log.info("SSR (vite) OK");
|
|
341
|
+
const templateUrl = "http://127.0.0.1:5173/index.html";
|
|
342
|
+
this.log.debug(`Fetch template from ${templateUrl}`);
|
|
343
|
+
const route2 = this.createHandler(
|
|
344
|
+
() => fetch(templateUrl).then((it) => it.text()).catch(() => void 0).then((it) => it ? this.checkTemplate(it) : void 0)
|
|
345
|
+
);
|
|
346
|
+
await this.server.route(route2);
|
|
347
|
+
await this.server.route({
|
|
348
|
+
...route2,
|
|
349
|
+
url: "*"
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
let root = "";
|
|
354
|
+
if (!this.alepha.isServerless()) {
|
|
355
|
+
const maybe = [
|
|
356
|
+
join(process.cwd(), this.env.REACT_SERVER_DIST),
|
|
357
|
+
join(process.cwd(), "..", this.env.REACT_SERVER_DIST)
|
|
358
|
+
];
|
|
359
|
+
for (const it of maybe) {
|
|
360
|
+
if (existsSync(it)) {
|
|
361
|
+
root = it;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (!root) {
|
|
366
|
+
this.log.warn("Missing static files, SSR will be disabled");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
await this.server.serve(this.createStaticHandler(root));
|
|
370
|
+
}
|
|
371
|
+
const template = this.checkTemplate(
|
|
372
|
+
this.alepha.state("ReactServerProvider.template") ?? await readFile(join(root, "index.html"), "utf-8")
|
|
373
|
+
);
|
|
374
|
+
const route = this.createHandler(async () => template);
|
|
375
|
+
await this.server.route(route);
|
|
376
|
+
await this.server.route({
|
|
377
|
+
...route,
|
|
378
|
+
url: "*"
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Check if the template contains the outlet.
|
|
383
|
+
*
|
|
384
|
+
* @param template
|
|
385
|
+
* @protected
|
|
386
|
+
*/
|
|
387
|
+
checkTemplate(template) {
|
|
388
|
+
if (!template.includes(this.env.REACT_SSR_OUTLET)) {
|
|
389
|
+
if (!template.includes('<div id="root"></div>')) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
`Missing React SSR outlet in index.html, please add ${this.env.REACT_SSR_OUTLET} to the index.html file`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return template.replace(
|
|
395
|
+
`<div id="root"></div>`,
|
|
396
|
+
`<div id="root">${this.env.REACT_SSR_OUTLET}</div>`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
return template;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
*
|
|
403
|
+
* @param root
|
|
404
|
+
* @protected
|
|
405
|
+
*/
|
|
406
|
+
createStaticHandler(root) {
|
|
407
|
+
return {
|
|
408
|
+
root,
|
|
409
|
+
prefix: this.env.REACT_SERVER_PREFIX,
|
|
410
|
+
logLevel: "warn",
|
|
411
|
+
cacheControl: true,
|
|
412
|
+
immutable: true,
|
|
413
|
+
preCompressed: true,
|
|
414
|
+
maxAge: "30d",
|
|
415
|
+
index: false
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
*
|
|
420
|
+
* @param templateLoader
|
|
421
|
+
* @protected
|
|
422
|
+
*/
|
|
423
|
+
createHandler(templateLoader) {
|
|
424
|
+
return {
|
|
425
|
+
method: "GET",
|
|
426
|
+
url: "/",
|
|
427
|
+
handler: async (ctx) => {
|
|
428
|
+
const template = await templateLoader();
|
|
429
|
+
if (!template) {
|
|
430
|
+
return new Response("Not found", { status: 404 });
|
|
431
|
+
}
|
|
432
|
+
const response = this.notFoundHandler(ctx.url);
|
|
433
|
+
if (response) {
|
|
434
|
+
return response;
|
|
435
|
+
}
|
|
436
|
+
return await this.ssr(ctx.url, template, {
|
|
437
|
+
user: ctx.user,
|
|
438
|
+
cookies: ctx.cookies
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
processDescriptors() {
|
|
444
|
+
const pages = this.alepha.getDescriptorValues($page);
|
|
445
|
+
for (const { key, instance, value } of pages) {
|
|
446
|
+
instance[key].render = async (options = {}) => {
|
|
447
|
+
const name = value.options.name ?? key;
|
|
448
|
+
const page = this.router.page(name);
|
|
449
|
+
const layers = await this.router.createLayers(
|
|
450
|
+
"",
|
|
451
|
+
page,
|
|
452
|
+
options.params ?? {},
|
|
453
|
+
options.query ?? {},
|
|
454
|
+
[]
|
|
455
|
+
);
|
|
456
|
+
return renderToString(
|
|
457
|
+
this.router.root({
|
|
458
|
+
layers,
|
|
459
|
+
pathname: "",
|
|
460
|
+
search: "",
|
|
461
|
+
context: {}
|
|
462
|
+
})
|
|
463
|
+
);
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
*
|
|
469
|
+
* @param url
|
|
470
|
+
* @protected
|
|
471
|
+
*/
|
|
472
|
+
notFoundHandler(url) {
|
|
473
|
+
if (url.pathname.match(/\.\w+$/)) {
|
|
474
|
+
return new Response("Not found", { status: 404 });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
*
|
|
479
|
+
* @param url
|
|
480
|
+
* @param template
|
|
481
|
+
* @param page
|
|
482
|
+
*/
|
|
483
|
+
async ssr(url, template = this.env.REACT_SSR_OUTLET, page = {}) {
|
|
484
|
+
const { element, layers, redirect, context } = await this.router.render(
|
|
485
|
+
url.pathname + url.search,
|
|
486
|
+
{
|
|
487
|
+
args: page
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
if (redirect) {
|
|
491
|
+
return new Response("", {
|
|
492
|
+
status: 302,
|
|
493
|
+
headers: {
|
|
494
|
+
Location: redirect
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
const appHtml = renderToString(element);
|
|
499
|
+
const script = `<script>window.__ssr=${JSON.stringify({
|
|
500
|
+
layers: layers.map((it) => ({
|
|
501
|
+
...it,
|
|
502
|
+
index: void 0,
|
|
503
|
+
path: void 0,
|
|
504
|
+
element: void 0
|
|
505
|
+
}))
|
|
506
|
+
})}<\/script>`;
|
|
507
|
+
const index = template.indexOf("</body>");
|
|
508
|
+
if (index !== -1) {
|
|
509
|
+
template = template.slice(0, index) + script + template.slice(index);
|
|
510
|
+
}
|
|
511
|
+
if (context.helmet) {
|
|
512
|
+
template = this.renderHelmetContext(template, context.helmet);
|
|
513
|
+
}
|
|
514
|
+
template = template.replace(this.env.REACT_SSR_OUTLET, appHtml);
|
|
515
|
+
return new Response(template, {
|
|
516
|
+
headers: { "Content-Type": "text/html" }
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
renderHelmetContext(template, helmetContext) {
|
|
520
|
+
if (helmetContext.title) {
|
|
521
|
+
if (template.includes("<title>")) {
|
|
522
|
+
template = template.replace(
|
|
523
|
+
/<title>.*<\/title>/,
|
|
524
|
+
`<title>${helmetContext.title}</title>`
|
|
525
|
+
);
|
|
526
|
+
} else {
|
|
527
|
+
template = template.replace(
|
|
528
|
+
"</head>",
|
|
529
|
+
`<title>${helmetContext.title}</title></head>`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return template;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const envSchema = t.object({
|
|
538
|
+
REACT_AUTH_ENABLED: t.boolean({ default: false })
|
|
539
|
+
});
|
|
540
|
+
class ReactModule {
|
|
541
|
+
env = $inject(envSchema);
|
|
542
|
+
alepha = $inject(Alepha);
|
|
543
|
+
constructor() {
|
|
544
|
+
this.alepha.with(ServerModule).with(ServerLinksProvider).with(PageDescriptorProvider).with(ReactServerProvider);
|
|
545
|
+
if (this.env.REACT_AUTH_ENABLED) {
|
|
546
|
+
this.alepha.with(ReactAuthProvider);
|
|
547
|
+
this.alepha.with(Auth);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
autoInject($page, ReactModule);
|
|
552
|
+
autoInject($auth, ReactAuthProvider, Auth);
|
|
553
|
+
|
|
554
|
+
export { $auth, $page, Auth, PageDescriptorProvider, ReactAuthProvider, ReactModule, ReactServerProvider, Router, envSchema$1 as envSchema };
|