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