@alepha/react 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,46 +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
- // alias for "not found handler"
54
- handler: route2.handler
349
+ ...route2,
350
+ url: "*"
55
351
  });
56
352
  return;
57
353
  }
58
- const maybe = [
59
- node_path.join(process.cwd(), this.env.REACT_SERVER_DIST),
60
- node_path.join(process.cwd(), "..", this.env.REACT_SERVER_DIST),
61
- node_path.join(process.cwd(), "dist", this.env.REACT_SERVER_DIST)
62
- ];
63
354
  let root = "";
64
- for (const it of maybe) {
65
- if (node_fs.existsSync(it)) {
66
- root = it;
67
- 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
+ }
68
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));
69
371
  }
70
- if (!root) {
71
- this.log.warn("Missing static files, SSR will be disabled");
72
- return;
73
- }
74
- await this.server.serve(this.createStaticHandler(root));
75
- 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
+ );
76
375
  const route = this.createHandler(async () => template);
77
376
  await this.server.route(route);
78
377
  await this.server.route({
79
- url: "/*",
80
- // alias for "not found handler"
81
- handler: route.handler
378
+ ...route,
379
+ url: "*"
82
380
  });
83
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
+ }
84
402
  /**
85
403
  *
86
404
  * @param root
@@ -105,22 +423,26 @@ class ReactServerProvider {
105
423
  */
106
424
  createHandler(templateLoader) {
107
425
  return {
426
+ method: "GET",
108
427
  url: "/",
109
- handler: async ({ url, user }) => {
428
+ handler: async (ctx) => {
110
429
  const template = await templateLoader();
111
430
  if (!template) {
112
431
  return new Response("Not found", { status: 404 });
113
432
  }
114
- const response = this.notFoundHandler(url);
433
+ const response = this.notFoundHandler(ctx.url);
115
434
  if (response) {
116
435
  return response;
117
436
  }
118
- return await this.ssr(url, template, user);
437
+ return await this.ssr(ctx.url, template, {
438
+ user: ctx.user,
439
+ cookies: ctx.cookies
440
+ });
119
441
  }
120
442
  };
121
443
  }
122
444
  processDescriptors() {
123
- const pages = this.alepha.getDescriptorValues(useRouterState.$page);
445
+ const pages = this.alepha.getDescriptorValues(useAuth.$page);
124
446
  for (const { key, instance, value } of pages) {
125
447
  instance[key].render = async (options = {}) => {
126
448
  const name = value.options.name ?? key;
@@ -136,7 +458,8 @@ class ReactServerProvider {
136
458
  this.router.root({
137
459
  layers,
138
460
  pathname: "",
139
- search: ""
461
+ search: "",
462
+ context: {}
140
463
  })
141
464
  );
142
465
  };
@@ -148,7 +471,7 @@ class ReactServerProvider {
148
471
  * @protected
149
472
  */
150
473
  notFoundHandler(url) {
151
- if (url.match(/\.\w+$/)) {
474
+ if (url.pathname.match(/\.\w+$/)) {
152
475
  return new Response("Not found", { status: 404 });
153
476
  }
154
477
  }
@@ -156,12 +479,15 @@ class ReactServerProvider {
156
479
  *
157
480
  * @param url
158
481
  * @param template
159
- * @param user
482
+ * @param page
160
483
  */
161
- async ssr(url, template = this.env.REACT_SSR_OUTLET, user) {
162
- const { element, layers, redirect } = await this.router.render(url, {
163
- user
164
- });
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
+ );
165
491
  if (redirect) {
166
492
  return new Response("", {
167
493
  status: 302,
@@ -177,324 +503,77 @@ class ReactServerProvider {
177
503
  index: void 0,
178
504
  path: void 0,
179
505
  element: void 0
180
- })),
181
- session: {
182
- user: user ? {
183
- id: user.id,
184
- name: user.name
185
- } : void 0
186
- }
506
+ }))
187
507
  })}<\/script>`;
188
508
  const index = template.indexOf("</body>");
189
509
  if (index !== -1) {
190
510
  template = template.slice(0, index) + script + template.slice(index);
191
511
  }
192
- return new Response(template.replace(this.env.REACT_SSR_OUTLET, appHtml), {
193
- headers: { "Content-Type": "text/html" }
194
- });
195
- }
196
- }
197
-
198
- const sessionUserSchema = core.t.object({
199
- id: core.t.string(),
200
- name: core.t.optional(core.t.string())
201
- });
202
- const sessionSchema = core.t.object({
203
- user: core.t.optional(sessionUserSchema)
204
- });
205
- const envSchema = core.t.object({
206
- REACT_OIDC_ISSUER: core.t.optional(core.t.string()),
207
- REACT_OIDC_CLIENT_ID: core.t.optional(core.t.string()),
208
- REACT_OIDC_CLIENT_SECRET: core.t.optional(core.t.string()),
209
- REACT_OIDC_REDIRECT_URI: core.t.optional(core.t.string())
210
- });
211
- class ReactSessionProvider {
212
- SSID = "ssid";
213
- log = core.$logger();
214
- env = core.$inject(envSchema);
215
- serverProvider = core.$inject(server.ServerProvider);
216
- securityProvider = core.$inject(security.SecurityProvider);
217
- sessions = cache.$cache();
218
- clients = [];
219
- get redirectUri() {
220
- return this.env.REACT_OIDC_REDIRECT_URI ?? `${this.serverProvider.hostname}/api/callback`;
221
- }
222
- configure = core.$hook({
223
- name: "configure",
224
- priority: 100,
225
- handler: async () => {
226
- const issuer = this.env.REACT_OIDC_ISSUER;
227
- const clientId = this.env.REACT_OIDC_CLIENT_ID;
228
- if (!issuer || !clientId) {
229
- return;
230
- }
231
- const client = await openidClient.discovery(
232
- new URL(issuer),
233
- clientId,
234
- {
235
- client_secret: this.env.REACT_OIDC_CLIENT_SECRET
236
- },
237
- void 0,
238
- {
239
- execute: [openidClient.allowInsecureRequests]
240
- }
241
- );
242
- this.clients = [client];
512
+ if (context.helmet) {
513
+ template = this.renderHelmetContext(template, context.helmet);
243
514
  }
244
- });
245
- /**
246
- *
247
- * @param sessionId
248
- * @param session
249
- * @protected
250
- */
251
- async setSession(sessionId, session) {
252
- await this.sessions.set(sessionId, session, {
253
- days: 1
515
+ template = template.replace(this.env.REACT_SSR_OUTLET, appHtml);
516
+ return new Response(template, {
517
+ headers: { "Content-Type": "text/html" }
254
518
  });
255
519
  }
256
- /**
257
- *
258
- * @param sessionId
259
- * @protected
260
- */
261
- async getSession(sessionId) {
262
- const session = await this.sessions.get(sessionId);
263
- if (!session) {
264
- return;
265
- }
266
- const now = Date.now();
267
- if (session.expires_in && session.issued_at) {
268
- const expiresAt = session.issued_at + (session.expires_in - 10) * 1e3;
269
- if (expiresAt < now) {
270
- if (session.refresh_token) {
271
- try {
272
- const newTokens = await openidClient.refreshTokenGrant(
273
- this.clients[0],
274
- session.refresh_token
275
- );
276
- await this.setSession(sessionId, {
277
- ...newTokens,
278
- issued_at: Date.now()
279
- });
280
- return newTokens;
281
- } catch (e) {
282
- this.log.error(e, "Failed to refresh token");
283
- }
284
- }
285
- await this.sessions.invalidate(sessionId);
286
- 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
+ );
287
532
  }
288
533
  }
289
- if (!session.issued_at && session.access_token) {
290
- await this.sessions.invalidate(sessionId);
291
- return;
292
- }
293
- return session;
534
+ return template;
294
535
  }
295
- /**
296
- *
297
- * @protected
298
- */
299
- beforeRequest = core.$hook({
300
- name: "configure:fastify",
301
- priority: 100,
302
- handler: async (app) => {
303
- app.decorateRequest("session");
304
- app.addHook("onRequest", async (req) => {
305
- const sessionId = req.cookies[this.SSID];
306
- if (sessionId && !isViteFile(req.url)) {
307
- const session = await this.getSession(sessionId);
308
- if (session) {
309
- req.session = session;
310
- if (session.access_token) {
311
- req.headers.authorization = `Bearer ${session.access_token}`;
312
- }
313
- }
314
- }
315
- });
316
- }
317
- });
318
- /**
319
- *
320
- */
321
- login = server.$route({
322
- security: false,
323
- url: "/login",
324
- method: "GET",
325
- schema: {
326
- query: core.t.object({
327
- redirect: core.t.optional(core.t.string())
328
- })
329
- },
330
- handler: async ({ query }) => {
331
- const client = this.clients[0];
332
- const codeVerifier = openidClient.randomPKCECodeVerifier();
333
- const codeChallenge = await openidClient.calculatePKCECodeChallenge(codeVerifier);
334
- const scope = "openid profile email";
335
- const parameters = {
336
- redirect_uri: this.redirectUri,
337
- scope,
338
- code_challenge: codeChallenge,
339
- code_challenge_method: "S256"
340
- };
341
- const sessionId = crypto.randomUUID();
342
- await this.setSession(sessionId, {
343
- authorizationCodeGrant: {
344
- codeVerifier,
345
- redirectUri: query.redirect ?? "/"
346
- // TODO: add nonce, max_age, state
347
- }
348
- });
349
- return new Response("", {
350
- status: 302,
351
- headers: {
352
- "Set-Cookie": `${this.SSID}=${sessionId}; HttpOnly; Path=/; SameSite=Lax;`,
353
- Location: openidClient.buildAuthorizationUrl(client, parameters).toString()
354
- }
355
- });
356
- }
357
- });
358
- /**
359
- *
360
- */
361
- callback = server.$route({
362
- security: false,
363
- url: "/callback",
364
- method: "GET",
365
- schema: {
366
- headers: core.t.record(core.t.string(), core.t.string()),
367
- cookies: core.t.object({
368
- ssid: core.t.string()
369
- })
370
- },
371
- handler: async ({ cookies, url }) => {
372
- const sessionId = cookies.ssid;
373
- const session = await this.getSession(sessionId);
374
- if (!session) {
375
- throw new server.BadRequestError("Missing session");
376
- }
377
- if (!session.authorizationCodeGrant) {
378
- throw new server.BadRequestError("Invalid session - missing code verifier");
379
- }
380
- const [, search] = url.split("?");
381
- const tokens = await openidClient.authorizationCodeGrant(
382
- this.clients[0],
383
- new URL(`${this.redirectUri}?${search}`),
384
- {
385
- pkceCodeVerifier: session.authorizationCodeGrant.codeVerifier,
386
- expectedNonce: session.authorizationCodeGrant.nonce,
387
- expectedState: session.authorizationCodeGrant.state,
388
- maxAge: session.authorizationCodeGrant.max_age
389
- }
390
- );
391
- await this.setSession(sessionId, {
392
- ...tokens,
393
- issued_at: Date.now()
394
- });
395
- return new Response("", {
396
- status: 302,
397
- headers: {
398
- Location: session.authorizationCodeGrant.redirectUri ?? "/"
399
- }
400
- });
401
- }
402
- });
403
- logout = server.$route({
404
- security: false,
405
- url: "/logout",
406
- method: "GET",
407
- schema: {
408
- query: core.t.object({
409
- redirect: core.t.optional(core.t.string())
410
- }),
411
- cookies: core.t.object({
412
- ssid: core.t.string()
413
- })
414
- },
415
- handler: async ({ query, cookies }, { fastify }) => {
416
- const session = fastify?.req.session;
417
- await this.sessions.invalidate(cookies.ssid);
418
- const redirect = query.redirect ?? "/";
419
- const params = new URLSearchParams();
420
- params.set("post_logout_redirect_uri", redirect);
421
- if (session?.id_token) {
422
- params.set("id_token_hint", session.id_token);
423
- }
424
- return new Response("", {
425
- status: 302,
426
- headers: {
427
- "Set-Cookie": `${this.SSID}=; HttpOnly; Path=/; SameSite=Lax;`,
428
- Location: openidClient.buildEndSessionUrl(this.clients[0], params).toString()
429
- }
430
- });
431
- }
432
- });
433
- session = server.$route({
434
- security: false,
435
- url: "/_session",
436
- method: "GET",
437
- schema: {
438
- headers: core.t.object({
439
- authorization: core.t.string()
440
- }),
441
- response: sessionSchema
442
- },
443
- handler: async ({ headers }) => {
444
- try {
445
- return {
446
- user: await this.securityProvider.createUserFromToken(
447
- headers.authorization
448
- )
449
- };
450
- } catch (e) {
451
- return {};
452
- }
453
- }
454
- });
455
536
  }
456
- const isViteFile = (file) => {
457
- const [pathname] = file.split("?");
458
- if (pathname.startsWith("/docs")) {
459
- return false;
460
- }
461
- if (pathname.match(/\.\w{2,5}$/)) {
462
- return true;
463
- }
464
- if (pathname.startsWith("/@")) {
465
- return true;
466
- }
467
- return false;
468
- };
469
537
 
538
+ const envSchema = core.t.object({
539
+ REACT_AUTH_ENABLED: core.t.boolean({ default: false })
540
+ });
470
541
  class ReactModule {
542
+ env = core.$inject(envSchema);
471
543
  alepha = core.$inject(core.Alepha);
472
544
  constructor() {
473
- 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
+ }
474
550
  }
475
551
  }
476
- core.autoInject(useRouterState.$page, ReactModule);
552
+ core.autoInject(useAuth.$page, ReactModule);
553
+ core.autoInject(useAuth.$auth, ReactAuthProvider, useAuth.Auth);
477
554
 
478
- exports.$page = useRouterState.$page;
479
- exports.NestedView = useRouterState.NestedView;
480
- exports.PageDescriptorProvider = useRouterState.PageDescriptorProvider;
481
- exports.ReactBrowserProvider = useRouterState.ReactBrowserProvider;
482
- exports.RedirectException = useRouterState.RedirectException;
483
- exports.Router = useRouterState.Router;
484
- exports.RouterContext = useRouterState.RouterContext;
485
- exports.RouterHookApi = useRouterState.RouterHookApi;
486
- exports.RouterLayerContext = useRouterState.RouterLayerContext;
487
- exports.pageDescriptorKey = useRouterState.pageDescriptorKey;
488
- exports.useActive = useRouterState.useActive;
489
- exports.useClient = useRouterState.useClient;
490
- exports.useInject = useRouterState.useInject;
491
- exports.useQueryParams = useRouterState.useQueryParams;
492
- exports.useRouter = useRouterState.useRouter;
493
- exports.useRouterEvents = useRouterState.useRouterEvents;
494
- 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;
495
577
  exports.ReactModule = ReactModule;
496
578
  exports.ReactServerProvider = ReactServerProvider;
497
- exports.ReactSessionProvider = ReactSessionProvider;
498
579
  exports.envSchema = envSchema$1;
499
- exports.sessionSchema = sessionSchema;
500
- exports.sessionUserSchema = sessionUserSchema;