@alepha/react 0.5.2 → 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.cjs CHANGED
@@ -2,19 +2,316 @@
2
2
 
3
3
  var core = require('@alepha/core');
4
4
  var server = require('@alepha/server');
5
- var useRouterState = require('./useRouterState-BlKHWZwk.cjs');
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
- var crypto = require('node:crypto');
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
+ private: 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 } = 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 = this.authProviders[0].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
+ private: 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
+ private: 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(useRouterState.Router);
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 starting in development mode");
45
- const templateUrl = `${this.server.hostname}/index.html`;
341
+ this.log.info("SSR (vite) OK");
342
+ const templateUrl = "http://127.0.0.1:5173/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
- url: "*",
53
- handler: route2.handler
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
- for (const it of maybe) {
64
- if (node_fs.existsSync(it)) {
65
- root = it;
66
- break;
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
- if (!root) {
70
- this.log.warn("Missing static files, SSR will be disabled");
71
- return;
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
- url: "/*",
79
- // alias for "not found handler"
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,26 @@ class ReactServerProvider {
104
423
  */
105
424
  createHandler(templateLoader) {
106
425
  return {
426
+ method: "GET",
107
427
  url: "/",
108
- handler: async ({ url, user }) => {
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, user);
437
+ return await this.ssr(ctx.url, template, {
438
+ user: ctx.user,
439
+ cookies: ctx.cookies
440
+ });
118
441
  }
119
442
  };
120
443
  }
121
444
  processDescriptors() {
122
- const pages = this.alepha.getDescriptorValues(useRouterState.$page);
445
+ const pages = this.alepha.getDescriptorValues(useAuth.$page);
123
446
  for (const { key, instance, value } of pages) {
124
447
  instance[key].render = async (options = {}) => {
125
448
  const name = value.options.name ?? key;
@@ -135,7 +458,8 @@ class ReactServerProvider {
135
458
  this.router.root({
136
459
  layers,
137
460
  pathname: "",
138
- search: ""
461
+ search: "",
462
+ context: {}
139
463
  })
140
464
  );
141
465
  };
@@ -147,7 +471,7 @@ class ReactServerProvider {
147
471
  * @protected
148
472
  */
149
473
  notFoundHandler(url) {
150
- if (url.match(/\.\w+$/)) {
474
+ if (url.pathname.match(/\.\w+$/)) {
151
475
  return new Response("Not found", { status: 404 });
152
476
  }
153
477
  }
@@ -155,12 +479,15 @@ class ReactServerProvider {
155
479
  *
156
480
  * @param url
157
481
  * @param template
158
- * @param user
482
+ * @param page
159
483
  */
160
- async ssr(url, template = this.env.REACT_SSR_OUTLET, user) {
161
- const { element, layers, redirect } = await this.router.render(url, {
162
- user
163
- });
484
+ async ssr(url, template = this.env.REACT_SSR_OUTLET, page = {}) {
485
+ const { element, layers, redirect, context } = await this.router.render(
486
+ url.pathname + url.search,
487
+ {
488
+ args: page
489
+ }
490
+ );
164
491
  if (redirect) {
165
492
  return new Response("", {
166
493
  status: 302,
@@ -176,324 +503,77 @@ class ReactServerProvider {
176
503
  index: void 0,
177
504
  path: void 0,
178
505
  element: void 0
179
- })),
180
- session: {
181
- user: user ? {
182
- id: user.id,
183
- name: user.name
184
- } : void 0
185
- }
506
+ }))
186
507
  })}<\/script>`;
187
508
  const index = template.indexOf("</body>");
188
509
  if (index !== -1) {
189
510
  template = template.slice(0, index) + script + template.slice(index);
190
511
  }
191
- return new Response(template.replace(this.env.REACT_SSR_OUTLET, appHtml), {
192
- headers: { "Content-Type": "text/html" }
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];
512
+ if (context.helmet) {
513
+ template = this.renderHelmetContext(template, context.helmet);
242
514
  }
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
515
+ template = template.replace(this.env.REACT_SSR_OUTLET, appHtml);
516
+ return new Response(template, {
517
+ headers: { "Content-Type": "text/html" }
253
518
  });
254
519
  }
255
- /**
256
- *
257
- * @param sessionId
258
- * @protected
259
- */
260
- async getSession(sessionId) {
261
- const session = await this.sessions.get(sessionId);
262
- if (!session) {
263
- return;
264
- }
265
- const now = Date.now();
266
- if (session.expires_in && session.issued_at) {
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;
520
+ renderHelmetContext(template, helmetContext) {
521
+ if (helmetContext.title) {
522
+ if (template.includes("<title>")) {
523
+ template = template.replace(
524
+ /<title>.*<\/title>/,
525
+ `<title>${helmetContext.title}</title>`
526
+ );
527
+ } else {
528
+ template = template.replace(
529
+ "</head>",
530
+ `<title>${helmetContext.title}</title></head>`
531
+ );
286
532
  }
287
533
  }
288
- if (!session.issued_at && session.access_token) {
289
- await this.sessions.invalidate(sessionId);
290
- return;
291
- }
292
- return session;
534
+ return template;
293
535
  }
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
536
  }
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
537
 
538
+ const envSchema = core.t.object({
539
+ REACT_AUTH_ENABLED: core.t.boolean({ default: false })
540
+ });
469
541
  class ReactModule {
542
+ env = core.$inject(envSchema);
470
543
  alepha = core.$inject(core.Alepha);
471
544
  constructor() {
472
- this.alepha.with(server.ServerModule).with(server.ServerLinksProvider).with(ReactServerProvider).with(ReactSessionProvider).with(useRouterState.PageDescriptorProvider);
545
+ this.alepha.with(server.ServerModule).with(server.ServerLinksProvider).with(useAuth.PageDescriptorProvider).with(ReactServerProvider);
546
+ if (this.env.REACT_AUTH_ENABLED) {
547
+ this.alepha.with(ReactAuthProvider);
548
+ this.alepha.with(useAuth.Auth);
549
+ }
473
550
  }
474
551
  }
475
- core.autoInject(useRouterState.$page, ReactModule);
552
+ core.autoInject(useAuth.$page, ReactModule);
553
+ core.autoInject(useAuth.$auth, ReactAuthProvider, useAuth.Auth);
476
554
 
477
- exports.$page = useRouterState.$page;
478
- exports.NestedView = useRouterState.NestedView;
479
- exports.PageDescriptorProvider = useRouterState.PageDescriptorProvider;
480
- exports.ReactBrowserProvider = useRouterState.ReactBrowserProvider;
481
- exports.RedirectException = useRouterState.RedirectException;
482
- exports.Router = useRouterState.Router;
483
- exports.RouterContext = useRouterState.RouterContext;
484
- exports.RouterHookApi = useRouterState.RouterHookApi;
485
- exports.RouterLayerContext = useRouterState.RouterLayerContext;
486
- exports.pageDescriptorKey = useRouterState.pageDescriptorKey;
487
- exports.useActive = useRouterState.useActive;
488
- exports.useClient = useRouterState.useClient;
489
- exports.useInject = useRouterState.useInject;
490
- exports.useQueryParams = useRouterState.useQueryParams;
491
- exports.useRouter = useRouterState.useRouter;
492
- exports.useRouterEvents = useRouterState.useRouterEvents;
493
- exports.useRouterState = useRouterState.useRouterState;
555
+ exports.$auth = useAuth.$auth;
556
+ exports.$page = useAuth.$page;
557
+ exports.Auth = useAuth.Auth;
558
+ exports.Link = useAuth.Link;
559
+ exports.NestedView = useAuth.NestedView;
560
+ exports.PageDescriptorProvider = useAuth.PageDescriptorProvider;
561
+ exports.ReactBrowserProvider = useAuth.ReactBrowserProvider;
562
+ exports.RedirectionError = useAuth.RedirectionError;
563
+ exports.Router = useAuth.Router;
564
+ exports.RouterContext = useAuth.RouterContext;
565
+ exports.RouterHookApi = useAuth.RouterHookApi;
566
+ exports.RouterLayerContext = useAuth.RouterLayerContext;
567
+ exports.pageDescriptorKey = useAuth.pageDescriptorKey;
568
+ exports.useActive = useAuth.useActive;
569
+ exports.useAuth = useAuth.useAuth;
570
+ exports.useClient = useAuth.useClient;
571
+ exports.useInject = useAuth.useInject;
572
+ exports.useQueryParams = useAuth.useQueryParams;
573
+ exports.useRouter = useAuth.useRouter;
574
+ exports.useRouterEvents = useAuth.useRouterEvents;
575
+ exports.useRouterState = useAuth.useRouterState;
576
+ exports.ReactAuthProvider = ReactAuthProvider;
494
577
  exports.ReactModule = ReactModule;
495
578
  exports.ReactServerProvider = ReactServerProvider;
496
- exports.ReactSessionProvider = ReactSessionProvider;
497
579
  exports.envSchema = envSchema$1;
498
- exports.sessionSchema = sessionSchema;
499
- exports.sessionUserSchema = sessionUserSchema;