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