@alepha/react 0.6.0 → 0.6.2

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.
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var core = require('@alepha/core');
4
- var useAuth = require('./useAuth-DOVx2kqa.cjs');
4
+ var useAuth = require('./useAuth-B9ypF48n.cjs');
5
5
  require('react/jsx-runtime');
6
6
  require('react');
7
7
  require('@alepha/server');
@@ -1,6 +1,6 @@
1
1
  import { autoInject, $inject, Alepha } from '@alepha/core';
2
- import { a as $page, P as PageDescriptorProvider, l as ReactBrowserProvider, A as Auth } from './useAuth-i7wbKVrt.js';
3
- export { $ as $auth, L as Link, N as NestedView, R as Router, 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';
2
+ import { a as $page, P as PageDescriptorProvider, l as ReactBrowserProvider, A as Auth } from './useAuth-Ps01oe8e.js';
3
+ export { $ as $auth, L as Link, N as NestedView, R as Router, 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-Ps01oe8e.js';
4
4
  import 'react/jsx-runtime';
5
5
  import 'react';
6
6
  import '@alepha/server';
package/dist/index.cjs CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  var core = require('@alepha/core');
4
4
  var server = require('@alepha/server');
5
- var useAuth = require('./useAuth-DOVx2kqa.cjs');
5
+ var useAuth = require('./useAuth-B9ypF48n.cjs');
6
6
  var openidClient = require('openid-client');
7
7
  var node_fs = require('node:fs');
8
8
  var promises = require('node:fs/promises');
9
9
  var node_path = require('node:path');
10
+ var cheerio = require('cheerio');
10
11
  var server$1 = require('react-dom/server');
11
12
  require('react/jsx-runtime');
12
13
  require('react');
@@ -16,7 +17,7 @@ require('path-to-regexp');
16
17
  class ReactAuthProvider {
17
18
  log = core.$logger();
18
19
  alepha = core.$inject(core.Alepha);
19
- fastifyCookieProvider = core.$inject(server.FastifyCookieProvider);
20
+ serverCookieProvider = core.$inject(server.ServerCookieProvider);
20
21
  authProviders = [];
21
22
  authorizationCode = server.$cookie({
22
23
  name: "authorizationCode",
@@ -54,23 +55,30 @@ class ReactAuthProvider {
54
55
  name: "configure",
55
56
  handler: async () => {
56
57
  const auths = this.alepha.getDescriptorValues(useAuth.$auth);
57
- for (const auth of auths) {
58
- const options = auth.value.options;
58
+ for (const { value, key, instance } of auths) {
59
+ const options = value.options;
59
60
  if (options.oidc) {
61
+ this.log.debug(
62
+ `Discover OIDC auth provider -> ${options.oidc.issuer}`
63
+ );
64
+ const client = await openidClient.discovery(
65
+ new URL(options.oidc.issuer),
66
+ options.oidc.clientId,
67
+ {
68
+ client_secret: options.oidc.clientSecret
69
+ },
70
+ void 0,
71
+ {
72
+ execute: [openidClient.allowInsecureRequests]
73
+ }
74
+ );
75
+ instance[key].jwks = () => {
76
+ return client.serverMetadata().jwks_uri;
77
+ };
60
78
  this.authProviders.push({
61
- name: options.name ?? auth.key,
79
+ name: options.name ?? key,
62
80
  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
- )
81
+ client
74
82
  });
75
83
  }
76
84
  }
@@ -79,21 +87,19 @@ class ReactAuthProvider {
79
87
  /**
80
88
  * Configure Fastify to forward Session Access Token to Header Authorization.
81
89
  */
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
- }
90
+ onRequest = core.$hook({
91
+ name: "server:onRequest",
92
+ after: this.serverCookieProvider,
93
+ handler: async ({ request }) => {
94
+ if (request.cookies && !this.isViteFile(request.url.pathname) && !!this.authProviders.length) {
95
+ const tokens = await this.refresh(request.cookies);
96
+ if (tokens) {
97
+ request.headers.rep("authorization", `Bearer ${tokens.access_token}`);
95
98
  }
96
- });
99
+ if (this.user.get(request.cookies) && !this.tokens.get(request.cookies)) {
100
+ this.user.del(request.cookies);
101
+ }
102
+ }
97
103
  }
98
104
  });
99
105
  /**
@@ -122,7 +128,9 @@ class ReactAuthProvider {
122
128
  });
123
129
  return newTokens;
124
130
  } catch (e) {
125
- this.log.warn(e, "Failed to refresh token -");
131
+ if (e instanceof Error) {
132
+ this.log.warn("Failed to refresh token", e.message);
133
+ }
126
134
  }
127
135
  }
128
136
  this.tokens.del(cookies);
@@ -142,7 +150,7 @@ class ReactAuthProvider {
142
150
  */
143
151
  login = server.$route({
144
152
  security: false,
145
- private: true,
153
+ internal: true,
146
154
  url: "/_oauth/login",
147
155
  group: "auth",
148
156
  method: "GET",
@@ -153,13 +161,13 @@ class ReactAuthProvider {
153
161
  })
154
162
  },
155
163
  handler: async ({ query, cookies, url }) => {
156
- const { client } = this.provider(query.provider);
164
+ const { client, redirectUri } = this.provider(query.provider);
157
165
  const codeVerifier = openidClient.randomPKCECodeVerifier();
158
166
  const codeChallenge = await openidClient.calculatePKCECodeChallenge(codeVerifier);
159
167
  const scope = "openid profile email";
160
- let redirect_uri = this.authProviders[0].redirectUri;
168
+ let redirect_uri = redirectUri;
161
169
  if (redirect_uri.startsWith("/")) {
162
- redirect_uri = `${url.protocol}://${url.host}${redirect_uri}`;
170
+ redirect_uri = `${url.protocol}//${url.host}${redirect_uri}`;
163
171
  }
164
172
  const parameters = {
165
173
  redirect_uri,
@@ -184,7 +192,7 @@ class ReactAuthProvider {
184
192
  */
185
193
  callback = server.$route({
186
194
  security: false,
187
- private: true,
195
+ internal: true,
188
196
  url: "/_oauth/callback",
189
197
  group: "auth",
190
198
  method: "GET",
@@ -246,7 +254,7 @@ class ReactAuthProvider {
246
254
  */
247
255
  logout = server.$route({
248
256
  security: false,
249
- private: true,
257
+ internal: true,
250
258
  url: "/_oauth/logout",
251
259
  group: "auth",
252
260
  method: "GET",
@@ -316,13 +324,13 @@ const envSchema$1 = core.t.object({
316
324
  REACT_SERVER_DIST: core.t.string({ default: "client" }),
317
325
  REACT_SERVER_PREFIX: core.t.string({ default: "" }),
318
326
  REACT_SSR_ENABLED: core.t.boolean({ default: false }),
319
- REACT_SSR_OUTLET: core.t.string({ default: "<!--ssr-outlet-->" })
327
+ REACT_ROOT_ID: core.t.string({ default: "root" })
320
328
  });
321
329
  class ReactServerProvider {
322
330
  log = core.$logger();
323
331
  alepha = core.$inject(core.Alepha);
324
332
  router = core.$inject(useAuth.Router);
325
- server = core.$inject(server.ServerProvider);
333
+ serverProvider = core.$inject(server.ServerProvider);
326
334
  env = core.$inject(envSchema$1);
327
335
  configure = core.$hook({
328
336
  name: "configure",
@@ -330,7 +338,9 @@ class ReactServerProvider {
330
338
  await this.configureRoutes();
331
339
  }
332
340
  });
341
+ id = Math.random().toString(36).substring(2, 7);
333
342
  async configureRoutes() {
343
+ this.alepha.state("ReactServerProvider.ssr", false);
334
344
  if (this.alepha.isTest()) {
335
345
  this.processDescriptors();
336
346
  }
@@ -338,14 +348,15 @@ class ReactServerProvider {
338
348
  return;
339
349
  }
340
350
  if (process.env.VITE_ALEPHA_DEV === "true") {
351
+ const url = `http://${process.env.SERVER_HOST}:${process.env.SERVER_PORT}`;
341
352
  this.log.info("SSR (vite) OK");
342
- const templateUrl = "http://127.0.0.1:5173/index.html";
343
- this.log.debug(`Fetch template from ${templateUrl}`);
353
+ this.alepha.state("ReactServerProvider.ssr", true);
354
+ const templateUrl = `${url}/index.html`;
344
355
  const route2 = this.createHandler(
345
- () => fetch(templateUrl).then((it) => it.text()).catch(() => void 0).then((it) => it ? this.checkTemplate(it) : void 0)
356
+ () => fetch(templateUrl).then((it) => it.text()).catch(() => void 0)
346
357
  );
347
- await this.server.route(route2);
348
- await this.server.route({
358
+ await this.serverProvider.route(route2);
359
+ await this.serverProvider.route({
349
360
  ...route2,
350
361
  url: "*"
351
362
  });
@@ -367,37 +378,16 @@ class ReactServerProvider {
367
378
  this.log.warn("Missing static files, SSR will be disabled");
368
379
  return;
369
380
  }
370
- await this.server.serve(this.createStaticHandler(root));
381
+ await this.serverProvider.serve(this.createStaticHandler(root));
371
382
  }
372
- const template = this.checkTemplate(
373
- this.alepha.state("ReactServerProvider.template") ?? await promises.readFile(node_path.join(root, "index.html"), "utf-8")
374
- );
383
+ const template = this.alepha.state("ReactServerProvider.template") ?? await promises.readFile(node_path.join(root, "index.html"), "utf-8");
375
384
  const route = this.createHandler(async () => template);
376
- await this.server.route(route);
377
- await this.server.route({
385
+ await this.serverProvider.route(route);
386
+ await this.serverProvider.route({
378
387
  ...route,
379
388
  url: "*"
380
389
  });
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;
390
+ this.alepha.state("ReactServerProvider.ssr", true);
401
391
  }
402
392
  /**
403
393
  *
@@ -434,13 +424,14 @@ class ReactServerProvider {
434
424
  if (response) {
435
425
  return response;
436
426
  }
437
- return await this.ssr(ctx.url, template, {
438
- user: ctx.user,
439
- cookies: ctx.cookies
440
- });
427
+ return await this.ssr(ctx.url, template, ctx);
441
428
  }
442
429
  };
443
430
  }
431
+ /**
432
+ *
433
+ * @protected
434
+ */
444
435
  processDescriptors() {
445
436
  const pages = this.alepha.getDescriptorValues(useAuth.$page);
446
437
  for (const { key, instance, value } of pages) {
@@ -479,13 +470,26 @@ class ReactServerProvider {
479
470
  *
480
471
  * @param url
481
472
  * @param template
482
- * @param page
473
+ * @param args
483
474
  */
484
- async ssr(url, template = this.env.REACT_SSR_OUTLET, page = {}) {
475
+ async ssr(url, template, args = {}) {
476
+ const hasAuth = this.alepha.has(ReactAuthProvider);
477
+ if (!args.user && args.cookies && hasAuth) {
478
+ const auth = this.alepha.get(ReactAuthProvider);
479
+ args.user = auth.user.get(args.cookies);
480
+ if (args.user) {
481
+ args.user.roles = [];
482
+ }
483
+ }
484
+ if (this.alepha.has(server.ServerLinksProvider) && hasAuth) {
485
+ const srv = this.alepha.get(server.ServerLinksProvider);
486
+ args.links = await srv.links();
487
+ this.alepha.als.set("links", args.links);
488
+ }
485
489
  const { element, layers, redirect, context } = await this.router.render(
486
490
  url.pathname + url.search,
487
491
  {
488
- args: page
492
+ args
489
493
  }
490
494
  );
491
495
  if (redirect) {
@@ -496,42 +500,67 @@ class ReactServerProvider {
496
500
  }
497
501
  });
498
502
  }
499
- const appHtml = server$1.renderToString(element);
503
+ const html = server$1.renderToString(element);
504
+ const $ = cheerio.load(template);
500
505
  const script = `<script>window.__ssr=${JSON.stringify({
506
+ links: args.links,
501
507
  layers: layers.map((it) => ({
502
508
  ...it,
509
+ error: it.error ? {
510
+ ...it.error,
511
+ name: it.error.name,
512
+ message: it.error.message,
513
+ stack: it.error.stack
514
+ // TODO: Hide stack in production ?
515
+ } : void 0,
503
516
  index: void 0,
504
517
  path: void 0,
505
518
  element: void 0
506
519
  }))
507
520
  })}<\/script>`;
508
- const index = template.indexOf("</body>");
509
- if (index !== -1) {
510
- template = template.slice(0, index) + script + template.slice(index);
521
+ const body = $("body");
522
+ const root = body.find(`#${this.env.REACT_ROOT_ID}`);
523
+ if (root.length) {
524
+ root.html(html);
525
+ } else {
526
+ body.prepend(`<div id="${this.env.REACT_ROOT_ID}">${html}</div>`);
511
527
  }
512
- if (context.helmet) {
513
- template = this.renderHelmetContext(template, context.helmet);
528
+ body.append(script);
529
+ if (context.head) {
530
+ this.renderHeadContext($, context.head);
514
531
  }
515
- template = template.replace(this.env.REACT_SSR_OUTLET, appHtml);
516
- return new Response(template, {
532
+ return new Response($.html(), {
517
533
  headers: { "Content-Type": "text/html" }
518
534
  });
519
535
  }
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
- );
536
+ renderHeadContext($, headContext) {
537
+ const head = $("head");
538
+ if (head) {
539
+ if (headContext.title) {
540
+ head.find("title").remove();
541
+ head.append(`<title>${headContext.title}</title>`);
542
+ }
543
+ if (headContext.meta) {
544
+ for (const it of headContext.meta) {
545
+ const meta = head.find(`meta[name="${it.name}"]`);
546
+ if (meta.length) {
547
+ meta.attr("content", it.content);
548
+ } else {
549
+ head.append(`<meta name="${it.name}" content="${it.content}" />`);
550
+ }
551
+ }
552
+ }
553
+ }
554
+ if (headContext.htmlAttributes) {
555
+ for (const [key, value] of Object.entries(headContext.htmlAttributes)) {
556
+ $("html").attr(key, value);
557
+ }
558
+ }
559
+ if (headContext.bodyAttributes) {
560
+ for (const [key, value] of Object.entries(headContext.bodyAttributes)) {
561
+ $("body").attr(key, value);
532
562
  }
533
563
  }
534
- return template;
535
564
  }
536
565
  }
537
566