@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.browser.cjs +23 -19
- package/dist/index.browser.js +5 -4
- package/dist/index.cjs +416 -339
- package/dist/index.d.ts +591 -420
- package/dist/index.js +395 -320
- package/dist/{useRouterState-BlKHWZwk.cjs → useAuth-DOVx2kqa.cjs} +243 -102
- package/dist/{useRouterState-CvFCmaq7.js → useAuth-i7wbKVrt.js} +214 -77
- package/package.json +20 -18
- package/src/components/NestedView.tsx +0 -36
- package/src/contexts/RouterContext.ts +0 -15
- package/src/contexts/RouterLayerContext.ts +0 -10
- package/src/descriptors/$page.ts +0 -90
- package/src/hooks/RouterHookApi.ts +0 -154
- package/src/hooks/useActive.ts +0 -57
- package/src/hooks/useClient.ts +0 -6
- package/src/hooks/useInject.ts +0 -12
- package/src/hooks/useQueryParams.ts +0 -59
- package/src/hooks/useRouter.ts +0 -28
- package/src/hooks/useRouterEvents.ts +0 -43
- package/src/hooks/useRouterState.ts +0 -23
- package/src/index.browser.ts +0 -19
- package/src/index.shared.ts +0 -17
- package/src/index.ts +0 -29
- package/src/providers/PageDescriptorProvider.ts +0 -52
- package/src/providers/ReactBrowserProvider.ts +0 -228
- package/src/providers/ReactServerProvider.ts +0 -244
- package/src/providers/ReactSessionProvider.ts +0 -363
- package/src/services/Router.ts +0 -742
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import { $cache } from "@alepha/cache";
|
|
3
|
-
import { $hook, $inject, $logger, type Static, t } from "@alepha/core";
|
|
4
|
-
import { SecurityProvider } from "@alepha/security";
|
|
5
|
-
import { $route, BadRequestError, ServerProvider } from "@alepha/server";
|
|
6
|
-
import {
|
|
7
|
-
type Configuration,
|
|
8
|
-
allowInsecureRequests,
|
|
9
|
-
authorizationCodeGrant,
|
|
10
|
-
buildAuthorizationUrl,
|
|
11
|
-
buildEndSessionUrl,
|
|
12
|
-
calculatePKCECodeChallenge,
|
|
13
|
-
discovery,
|
|
14
|
-
randomPKCECodeVerifier,
|
|
15
|
-
refreshTokenGrant,
|
|
16
|
-
} from "openid-client";
|
|
17
|
-
|
|
18
|
-
export const sessionUserSchema = t.object({
|
|
19
|
-
id: t.string(),
|
|
20
|
-
name: t.optional(t.string()),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
export const sessionSchema = t.object({
|
|
24
|
-
user: t.optional(sessionUserSchema),
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
export type Session = Static<typeof sessionSchema>;
|
|
28
|
-
|
|
29
|
-
const envSchema = t.object({
|
|
30
|
-
REACT_OIDC_ISSUER: t.optional(t.string()),
|
|
31
|
-
REACT_OIDC_CLIENT_ID: t.optional(t.string()),
|
|
32
|
-
REACT_OIDC_CLIENT_SECRET: t.optional(t.string()),
|
|
33
|
-
REACT_OIDC_REDIRECT_URI: t.optional(t.string()),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
declare module "fastify" {
|
|
37
|
-
interface FastifyRequest {
|
|
38
|
-
session?: ReactServerSession;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
declare module "@alepha/core" {
|
|
43
|
-
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export class ReactSessionProvider {
|
|
47
|
-
protected readonly SSID = "ssid";
|
|
48
|
-
protected readonly log = $logger();
|
|
49
|
-
protected readonly env = $inject(envSchema);
|
|
50
|
-
protected readonly serverProvider = $inject(ServerProvider);
|
|
51
|
-
protected readonly securityProvider = $inject(SecurityProvider);
|
|
52
|
-
protected readonly sessions = $cache<ReactServerSession>();
|
|
53
|
-
protected clients: Configuration[] = [];
|
|
54
|
-
|
|
55
|
-
public get redirectUri() {
|
|
56
|
-
return (
|
|
57
|
-
this.env.REACT_OIDC_REDIRECT_URI ??
|
|
58
|
-
`${this.serverProvider.hostname}/api/callback`
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
protected readonly configure = $hook({
|
|
63
|
-
name: "configure",
|
|
64
|
-
priority: 100,
|
|
65
|
-
handler: async () => {
|
|
66
|
-
const issuer = this.env.REACT_OIDC_ISSUER;
|
|
67
|
-
const clientId = this.env.REACT_OIDC_CLIENT_ID;
|
|
68
|
-
if (!issuer || !clientId) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const client = await discovery(
|
|
73
|
-
new URL(issuer),
|
|
74
|
-
clientId,
|
|
75
|
-
{
|
|
76
|
-
client_secret: this.env.REACT_OIDC_CLIENT_SECRET,
|
|
77
|
-
},
|
|
78
|
-
undefined,
|
|
79
|
-
{
|
|
80
|
-
execute: [allowInsecureRequests],
|
|
81
|
-
},
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
this.clients = [client];
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
*
|
|
90
|
-
* @param sessionId
|
|
91
|
-
* @param session
|
|
92
|
-
* @protected
|
|
93
|
-
*/
|
|
94
|
-
protected async setSession(sessionId: string, session: ReactServerSession) {
|
|
95
|
-
await this.sessions.set(sessionId, session, {
|
|
96
|
-
days: 1,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
*
|
|
102
|
-
* @param sessionId
|
|
103
|
-
* @protected
|
|
104
|
-
*/
|
|
105
|
-
protected async getSession(
|
|
106
|
-
sessionId: string,
|
|
107
|
-
): Promise<ReactServerSession | undefined> {
|
|
108
|
-
const session = await this.sessions.get(sessionId);
|
|
109
|
-
if (!session) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const now = Date.now();
|
|
114
|
-
|
|
115
|
-
if (session.expires_in && session.issued_at) {
|
|
116
|
-
const expiresAt = session.issued_at + (session.expires_in - 10) * 1000;
|
|
117
|
-
if (expiresAt < now) {
|
|
118
|
-
if (session.refresh_token) {
|
|
119
|
-
try {
|
|
120
|
-
const newTokens = await refreshTokenGrant(
|
|
121
|
-
this.clients[0],
|
|
122
|
-
session.refresh_token,
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
await this.setSession(sessionId, {
|
|
126
|
-
...newTokens,
|
|
127
|
-
issued_at: Date.now(),
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
return newTokens;
|
|
131
|
-
} catch (e) {
|
|
132
|
-
this.log.error(e, "Failed to refresh token");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
await this.sessions.invalidate(sessionId);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (!session.issued_at && session.access_token) {
|
|
141
|
-
await this.sessions.invalidate(sessionId);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return session;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
*
|
|
150
|
-
* @protected
|
|
151
|
-
*/
|
|
152
|
-
protected readonly beforeRequest = $hook({
|
|
153
|
-
name: "configure:fastify",
|
|
154
|
-
priority: 100,
|
|
155
|
-
handler: async (app) => {
|
|
156
|
-
app.decorateRequest("session");
|
|
157
|
-
app.addHook("onRequest", async (req) => {
|
|
158
|
-
const sessionId = (req as any).cookies[this.SSID];
|
|
159
|
-
if (sessionId && !isViteFile(req.url)) {
|
|
160
|
-
const session = await this.getSession(sessionId);
|
|
161
|
-
if (session) {
|
|
162
|
-
req.session = session;
|
|
163
|
-
if (session.access_token) {
|
|
164
|
-
req.headers.authorization = `Bearer ${session.access_token}`;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
*
|
|
174
|
-
*/
|
|
175
|
-
public readonly login = $route({
|
|
176
|
-
security: false,
|
|
177
|
-
url: "/login",
|
|
178
|
-
method: "GET",
|
|
179
|
-
schema: {
|
|
180
|
-
query: t.object({
|
|
181
|
-
redirect: t.optional(t.string()),
|
|
182
|
-
}),
|
|
183
|
-
},
|
|
184
|
-
handler: async ({ query }) => {
|
|
185
|
-
const client = this.clients[0];
|
|
186
|
-
|
|
187
|
-
const codeVerifier = randomPKCECodeVerifier();
|
|
188
|
-
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
|
189
|
-
const scope = "openid profile email";
|
|
190
|
-
|
|
191
|
-
const parameters: Record<string, string> = {
|
|
192
|
-
redirect_uri: this.redirectUri,
|
|
193
|
-
scope,
|
|
194
|
-
code_challenge: codeChallenge,
|
|
195
|
-
code_challenge_method: "S256",
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const sessionId = crypto.randomUUID();
|
|
199
|
-
await this.setSession(sessionId, {
|
|
200
|
-
authorizationCodeGrant: {
|
|
201
|
-
codeVerifier,
|
|
202
|
-
redirectUri: query.redirect ?? "/",
|
|
203
|
-
// TODO: add nonce, max_age, state
|
|
204
|
-
},
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
return new Response("", {
|
|
208
|
-
status: 302,
|
|
209
|
-
headers: {
|
|
210
|
-
"Set-Cookie": `${this.SSID}=${sessionId}; HttpOnly; Path=/; SameSite=Lax;`,
|
|
211
|
-
Location: buildAuthorizationUrl(client, parameters).toString(),
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
*
|
|
219
|
-
*/
|
|
220
|
-
public readonly callback = $route({
|
|
221
|
-
security: false,
|
|
222
|
-
url: "/callback",
|
|
223
|
-
method: "GET",
|
|
224
|
-
schema: {
|
|
225
|
-
headers: t.record(t.string(), t.string()),
|
|
226
|
-
cookies: t.object({
|
|
227
|
-
ssid: t.string(),
|
|
228
|
-
}),
|
|
229
|
-
},
|
|
230
|
-
handler: async ({ cookies, url }) => {
|
|
231
|
-
const sessionId = cookies.ssid;
|
|
232
|
-
const session = await this.getSession(sessionId);
|
|
233
|
-
if (!session) {
|
|
234
|
-
throw new BadRequestError("Missing session");
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (!session.authorizationCodeGrant) {
|
|
238
|
-
throw new BadRequestError("Invalid session - missing code verifier");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const [, search] = url.split("?");
|
|
242
|
-
const tokens = await authorizationCodeGrant(
|
|
243
|
-
this.clients[0],
|
|
244
|
-
new URL(`${this.redirectUri}?${search}`),
|
|
245
|
-
{
|
|
246
|
-
pkceCodeVerifier: session.authorizationCodeGrant.codeVerifier,
|
|
247
|
-
expectedNonce: session.authorizationCodeGrant.nonce,
|
|
248
|
-
expectedState: session.authorizationCodeGrant.state,
|
|
249
|
-
maxAge: session.authorizationCodeGrant.max_age,
|
|
250
|
-
},
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
await this.setSession(sessionId, {
|
|
254
|
-
...tokens,
|
|
255
|
-
issued_at: Date.now(),
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
return new Response("", {
|
|
259
|
-
status: 302,
|
|
260
|
-
headers: {
|
|
261
|
-
Location: session.authorizationCodeGrant.redirectUri ?? "/",
|
|
262
|
-
},
|
|
263
|
-
});
|
|
264
|
-
},
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
public readonly logout = $route({
|
|
268
|
-
security: false,
|
|
269
|
-
url: "/logout",
|
|
270
|
-
method: "GET",
|
|
271
|
-
schema: {
|
|
272
|
-
query: t.object({
|
|
273
|
-
redirect: t.optional(t.string()),
|
|
274
|
-
}),
|
|
275
|
-
cookies: t.object({
|
|
276
|
-
ssid: t.string(),
|
|
277
|
-
}),
|
|
278
|
-
},
|
|
279
|
-
handler: async ({ query, cookies }, { fastify }: any) => {
|
|
280
|
-
const session = fastify?.req.session;
|
|
281
|
-
|
|
282
|
-
await this.sessions.invalidate(cookies.ssid);
|
|
283
|
-
|
|
284
|
-
const redirect = query.redirect ?? "/";
|
|
285
|
-
|
|
286
|
-
const params = new URLSearchParams();
|
|
287
|
-
params.set("post_logout_redirect_uri", redirect);
|
|
288
|
-
if (session?.id_token) {
|
|
289
|
-
params.set("id_token_hint", session.id_token);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return new Response("", {
|
|
293
|
-
status: 302,
|
|
294
|
-
headers: {
|
|
295
|
-
"Set-Cookie": `${this.SSID}=; HttpOnly; Path=/; SameSite=Lax;`,
|
|
296
|
-
Location: buildEndSessionUrl(this.clients[0], params).toString(),
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
|
-
},
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
public readonly session = $route({
|
|
303
|
-
security: false,
|
|
304
|
-
url: "/_session",
|
|
305
|
-
method: "GET",
|
|
306
|
-
schema: {
|
|
307
|
-
headers: t.object({
|
|
308
|
-
authorization: t.string(),
|
|
309
|
-
}),
|
|
310
|
-
response: sessionSchema,
|
|
311
|
-
},
|
|
312
|
-
handler: async ({ headers }) => {
|
|
313
|
-
try {
|
|
314
|
-
return {
|
|
315
|
-
user: await this.securityProvider.createUserFromToken(
|
|
316
|
-
headers.authorization,
|
|
317
|
-
),
|
|
318
|
-
};
|
|
319
|
-
} catch (e) {
|
|
320
|
-
return {};
|
|
321
|
-
}
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export interface ReactServerSession {
|
|
327
|
-
access_token?: string;
|
|
328
|
-
expires_in?: number;
|
|
329
|
-
refresh_token?: string;
|
|
330
|
-
id_token?: string;
|
|
331
|
-
scope?: string;
|
|
332
|
-
issued_at?: number;
|
|
333
|
-
|
|
334
|
-
authorizationCodeGrant?: {
|
|
335
|
-
codeVerifier: string;
|
|
336
|
-
redirectUri: string;
|
|
337
|
-
nonce?: string;
|
|
338
|
-
max_age?: number;
|
|
339
|
-
state?: string;
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const isViteFile = (file: string) => {
|
|
344
|
-
const [pathname] = file.split("?");
|
|
345
|
-
|
|
346
|
-
// swagger
|
|
347
|
-
if (pathname.startsWith("/docs")) {
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// static assets
|
|
352
|
-
if (pathname.match(/\.\w{2,5}$/)) {
|
|
353
|
-
return true;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// vite internal files
|
|
357
|
-
if (pathname.startsWith("/@")) {
|
|
358
|
-
return true;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// our backend files
|
|
362
|
-
return false;
|
|
363
|
-
};
|