@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.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
+ 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(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://${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
- 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,23 @@ 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, ctx);
118
438
  }
119
439
  };
120
440
  }
121
441
  processDescriptors() {
122
- const pages = this.alepha.getDescriptorValues(useRouterState.$page);
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 user
479
+ * @param args
159
480
  */
160
- async ssr(url, template = this.env.REACT_SSR_OUTLET, user) {
161
- const { element, layers, redirect } = await this.router.render(url, {
162
- user
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
- 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];
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
- * @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;
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
- if (!session.issued_at && session.access_token) {
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(ReactServerProvider).with(ReactSessionProvider).with(useRouterState.PageDescriptorProvider);
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(useRouterState.$page, ReactModule);
549
+ core.autoInject(useAuth.$page, ReactModule);
550
+ core.autoInject(useAuth.$auth, ReactAuthProvider, useAuth.Auth);
476
551
 
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;
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;