@fragno-dev/auth 0.0.14

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.
@@ -0,0 +1,677 @@
1
+ import { defineFragment, defineRoute, defineRoutes, instantiate } from "@fragno-dev/core";
2
+ import { createClientBuilder } from "@fragno-dev/core/client";
3
+ import { withDatabase } from "@fragno-dev/db";
4
+ import { column, idColumn, referenceColumn, schema } from "@fragno-dev/db/schema";
5
+ import { z } from "zod";
6
+ import { decodeCursor } from "@fragno-dev/db/cursor";
7
+
8
+ //#region src/schema.ts
9
+ const authSchema = schema("auth", (s) => {
10
+ return s.addTable("user", (t) => {
11
+ return t.addColumn("id", idColumn()).addColumn("email", column("string")).addColumn("passwordHash", column("string")).addColumn("role", column("string").defaultTo("user")).addColumn("createdAt", column("timestamp").defaultTo((b) => b.now())).createIndex("idx_user_email", ["email"]).createIndex("idx_user_id", ["id"], { unique: true });
12
+ }).addTable("session", (t) => {
13
+ return t.addColumn("id", idColumn()).addColumn("userId", referenceColumn()).addColumn("expiresAt", column("timestamp")).addColumn("createdAt", column("timestamp").defaultTo((b) => b.now())).createIndex("idx_session_user", ["userId"]);
14
+ }).addReference("sessionOwner", {
15
+ from: {
16
+ table: "session",
17
+ column: "userId"
18
+ },
19
+ to: {
20
+ table: "user",
21
+ column: "id"
22
+ },
23
+ type: "one"
24
+ }).alterTable("user", (t) => {
25
+ return t.createIndex("idx_user_createdAt", ["createdAt"]);
26
+ });
27
+ });
28
+
29
+ //#endregion
30
+ //#region src/user/password.ts
31
+ async function hashPassword(password) {
32
+ const encoder = new TextEncoder();
33
+ const salt = crypto.getRandomValues(new Uint8Array(16));
34
+ const iterations = 1e5;
35
+ const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, ["deriveBits"]);
36
+ const hashBuffer = await crypto.subtle.deriveBits({
37
+ name: "PBKDF2",
38
+ salt,
39
+ iterations,
40
+ hash: "SHA-256"
41
+ }, keyMaterial, 256);
42
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
43
+ const saltArray = Array.from(salt);
44
+ return `${saltArray.map((b) => b.toString(16).padStart(2, "0")).join("")}:${iterations}:${hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")}`;
45
+ }
46
+ async function verifyPassword(password, storedHash) {
47
+ const parts = storedHash.split(":");
48
+ if (parts.length !== 3) return false;
49
+ const [saltHex, iterationsStr, hashHex] = parts;
50
+ const iterations = Number.parseInt(iterationsStr, 10);
51
+ const isHex = (value) => /^[0-9a-f]+$/i.test(value) && value.length % 2 === 0;
52
+ if (!saltHex || !hashHex || !isHex(saltHex) || !isHex(hashHex)) return false;
53
+ if (!Number.isFinite(iterations) || iterations <= 0) return false;
54
+ const saltPairs = saltHex.match(/.{1,2}/g);
55
+ const hashPairs = hashHex.match(/.{1,2}/g);
56
+ if (!saltPairs || !hashPairs) return false;
57
+ const salt = new Uint8Array(saltPairs.map((byte) => Number.parseInt(byte, 16)));
58
+ const storedHashBytes = new Uint8Array(hashPairs.map((byte) => Number.parseInt(byte, 16)));
59
+ const encoder = new TextEncoder();
60
+ const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, ["deriveBits"]);
61
+ const hashBuffer = await crypto.subtle.deriveBits({
62
+ name: "PBKDF2",
63
+ salt,
64
+ iterations,
65
+ hash: "SHA-256"
66
+ }, keyMaterial, 256);
67
+ const hashArray = new Uint8Array(hashBuffer);
68
+ if (hashArray.length !== storedHashBytes.length) return false;
69
+ let isEqual = true;
70
+ for (let i = 0; i < hashArray.length; i++) if (hashArray[i] !== storedHashBytes[i]) isEqual = false;
71
+ return isEqual;
72
+ }
73
+
74
+ //#endregion
75
+ //#region src/utils/cookie.ts
76
+ /**
77
+ * Cookie utilities for session management
78
+ */
79
+ const COOKIE_NAME = "sessionid";
80
+ const MAX_AGE = 2592e3;
81
+ /**
82
+ * Parse cookies from a Cookie header string
83
+ */
84
+ function parseCookies(cookieHeader) {
85
+ if (!cookieHeader) return {};
86
+ const cookies = {};
87
+ const pairs = cookieHeader.split(";");
88
+ for (const pair of pairs) {
89
+ const [key, ...valueParts] = pair.split("=");
90
+ const trimmedKey = key?.trim();
91
+ const value = valueParts.join("=").trim();
92
+ if (trimmedKey) cookies[trimmedKey] = decodeURIComponent(value);
93
+ }
94
+ return cookies;
95
+ }
96
+ /**
97
+ * Build a Set-Cookie header string with security attributes
98
+ */
99
+ function buildSetCookieHeader(value, options = {}) {
100
+ const { httpOnly = true, secure = true, sameSite = "Strict", maxAge = MAX_AGE, path = "/" } = options;
101
+ const effectiveSecure = sameSite === "None" ? true : secure;
102
+ const parts = [
103
+ `${COOKIE_NAME}=${encodeURIComponent(value)}`,
104
+ `Max-Age=${maxAge}`,
105
+ `Path=${path}`
106
+ ];
107
+ if (httpOnly) parts.push("HttpOnly");
108
+ if (effectiveSecure) parts.push("Secure");
109
+ if (sameSite) parts.push(`SameSite=${sameSite}`);
110
+ return parts.join("; ");
111
+ }
112
+ /**
113
+ * Build a Set-Cookie header to clear the session cookie
114
+ */
115
+ function buildClearCookieHeader(options = {}) {
116
+ return buildSetCookieHeader("", {
117
+ ...options,
118
+ maxAge: 0
119
+ });
120
+ }
121
+ /**
122
+ * Extract session ID from headers, checking cookies first, then query/body
123
+ */
124
+ function extractSessionId(headers, queryParam, bodySessionId) {
125
+ const cookieHeader = headers.get("Cookie");
126
+ const cookies = parseCookies(cookieHeader);
127
+ const sessionIdFromCookie = cookies[COOKIE_NAME];
128
+ if (sessionIdFromCookie) return sessionIdFromCookie;
129
+ if (queryParam) return queryParam;
130
+ if (bodySessionId) return bodySessionId;
131
+ return null;
132
+ }
133
+
134
+ //#endregion
135
+ //#region src/user/user-actions.ts
136
+ function createUserServices() {
137
+ return {
138
+ createUser: function(email, passwordHash, role = "user") {
139
+ return this.serviceTx(authSchema).mutate(({ uow }) => {
140
+ const id = uow.create("user", {
141
+ email,
142
+ passwordHash,
143
+ role
144
+ });
145
+ return {
146
+ id: id.valueOf(),
147
+ email,
148
+ role
149
+ };
150
+ }).build();
151
+ },
152
+ getUserByEmail: function(email) {
153
+ return this.serviceTx(authSchema).retrieve((uow) => uow.findFirst("user", (b) => b.whereIndex("idx_user_email", (eb) => eb("email", "=", email)))).transformRetrieve(([user]) => user ? {
154
+ id: user.id.valueOf(),
155
+ email: user.email,
156
+ passwordHash: user.passwordHash,
157
+ role: user.role
158
+ } : null).build();
159
+ },
160
+ updateUserRole: function(userId, role) {
161
+ return this.serviceTx(authSchema).mutate(({ uow }) => {
162
+ uow.update("user", userId, (b) => b.set({ role }));
163
+ return { success: true };
164
+ }).build();
165
+ },
166
+ updateUserPassword: function(userId, passwordHash) {
167
+ return this.serviceTx(authSchema).mutate(({ uow }) => {
168
+ uow.update("user", userId, (b) => b.set({ passwordHash }));
169
+ return { success: true };
170
+ }).build();
171
+ },
172
+ signUpWithSession: function(email, passwordHash) {
173
+ const expiresAt = new Date();
174
+ expiresAt.setDate(expiresAt.getDate() + 30);
175
+ return this.serviceTx(authSchema).retrieve((uow) => uow.findFirst("user", (b) => b.whereIndex("idx_user_email", (eb) => eb("email", "=", email)))).mutate(({ uow, retrieveResult: [existingUser] }) => {
176
+ if (existingUser) return {
177
+ ok: false,
178
+ code: "email_already_exists"
179
+ };
180
+ const userId = uow.create("user", {
181
+ email,
182
+ passwordHash,
183
+ role: "user"
184
+ });
185
+ const sessionId = uow.create("session", {
186
+ userId,
187
+ expiresAt
188
+ });
189
+ return {
190
+ ok: true,
191
+ sessionId: sessionId.valueOf(),
192
+ userId: userId.valueOf(),
193
+ email,
194
+ role: "user"
195
+ };
196
+ }).build();
197
+ },
198
+ updateUserRoleWithSession: function(sessionId, userId, role) {
199
+ const now = new Date();
200
+ return this.serviceTx(authSchema).retrieve((uow) => uow.findFirst("session", (b) => b.whereIndex("primary", (eb) => eb("id", "=", sessionId)).join((j) => j.sessionOwner((b$1) => b$1.select([
201
+ "id",
202
+ "email",
203
+ "role"
204
+ ]))))).mutate(({ uow, retrieveResult: [session] }) => {
205
+ if (!session || !session.sessionOwner) return {
206
+ ok: false,
207
+ code: "session_invalid"
208
+ };
209
+ if (session.expiresAt < now) {
210
+ uow.delete("session", session.id, (b) => b.check());
211
+ return {
212
+ ok: false,
213
+ code: "session_invalid"
214
+ };
215
+ }
216
+ if (session.sessionOwner.role !== "admin") return {
217
+ ok: false,
218
+ code: "permission_denied"
219
+ };
220
+ uow.update("user", userId, (b) => b.set({ role }));
221
+ return { ok: true };
222
+ }).build();
223
+ },
224
+ changePasswordWithSession: function(sessionId, passwordHash) {
225
+ const now = new Date();
226
+ return this.serviceTx(authSchema).retrieve((uow) => uow.findFirst("session", (b) => b.whereIndex("primary", (eb) => eb("id", "=", sessionId)).join((j) => j.sessionOwner((b$1) => b$1.select([
227
+ "id",
228
+ "email",
229
+ "role"
230
+ ]))))).mutate(({ uow, retrieveResult: [session] }) => {
231
+ if (!session || !session.sessionOwner) return {
232
+ ok: false,
233
+ code: "session_invalid"
234
+ };
235
+ if (session.expiresAt < now) {
236
+ uow.delete("session", session.id, (b) => b.check());
237
+ return {
238
+ ok: false,
239
+ code: "session_invalid"
240
+ };
241
+ }
242
+ uow.update("user", session.sessionOwner.id, (b) => b.set({ passwordHash }).check());
243
+ return { ok: true };
244
+ }).build();
245
+ }
246
+ };
247
+ }
248
+ const userActionsRoutesFactory = defineRoutes().create(({ services, config }) => {
249
+ return [
250
+ defineRoute({
251
+ method: "PATCH",
252
+ path: "/users/:userId/role",
253
+ inputSchema: z.object({ role: z.enum(["user", "admin"]) }),
254
+ outputSchema: z.object({ success: z.boolean() }),
255
+ errorCodes: [
256
+ "invalid_input",
257
+ "session_invalid",
258
+ "permission_denied"
259
+ ],
260
+ handler: async function({ input, pathParams, headers, query }, { json, error }) {
261
+ const { role } = await input.valid();
262
+ const { userId } = pathParams;
263
+ const sessionId = extractSessionId(headers, query.get("sessionId"));
264
+ if (!sessionId) return error({
265
+ message: "Session ID required",
266
+ code: "session_invalid"
267
+ }, 400);
268
+ const [result] = await this.handlerTx().withServiceCalls(() => [services.updateUserRoleWithSession(sessionId, userId, role)]).execute();
269
+ if (!result.ok) {
270
+ if (result.code === "permission_denied") return error({
271
+ message: "Unauthorized",
272
+ code: "permission_denied"
273
+ }, 401);
274
+ return error({
275
+ message: "Invalid session",
276
+ code: "session_invalid"
277
+ }, 401);
278
+ }
279
+ return json({ success: true });
280
+ }
281
+ }),
282
+ defineRoute({
283
+ method: "POST",
284
+ path: "/sign-up",
285
+ inputSchema: z.object({
286
+ email: z.email(),
287
+ password: z.string().min(8).max(100)
288
+ }),
289
+ outputSchema: z.object({
290
+ sessionId: z.string(),
291
+ userId: z.string(),
292
+ email: z.string(),
293
+ role: z.enum(["user", "admin"])
294
+ }),
295
+ errorCodes: ["email_already_exists", "invalid_input"],
296
+ handler: async function({ input }, { json, error }) {
297
+ const { email, password } = await input.valid();
298
+ const passwordHash = await hashPassword(password);
299
+ const [result] = await this.handlerTx().withServiceCalls(() => [services.signUpWithSession(email, passwordHash)]).execute();
300
+ if (!result.ok) return error({
301
+ message: "Email already exists",
302
+ code: "email_already_exists"
303
+ }, 400);
304
+ const setCookieHeader = buildSetCookieHeader(result.sessionId, config.cookieOptions);
305
+ return json({
306
+ sessionId: result.sessionId,
307
+ userId: result.userId,
308
+ email: result.email,
309
+ role: result.role
310
+ }, { headers: { "Set-Cookie": setCookieHeader } });
311
+ }
312
+ }),
313
+ defineRoute({
314
+ method: "POST",
315
+ path: "/sign-in",
316
+ inputSchema: z.object({
317
+ email: z.email(),
318
+ password: z.string().min(8).max(100)
319
+ }),
320
+ outputSchema: z.object({
321
+ sessionId: z.string(),
322
+ userId: z.string(),
323
+ email: z.string(),
324
+ role: z.enum(["user", "admin"])
325
+ }),
326
+ errorCodes: ["invalid_credentials"],
327
+ handler: async function({ input }, { json, error }) {
328
+ const { email, password } = await input.valid();
329
+ const [user] = await this.handlerTx().withServiceCalls(() => [services.getUserByEmail(email)]).execute();
330
+ if (!user) return error({
331
+ message: "Invalid credentials",
332
+ code: "invalid_credentials"
333
+ }, 401);
334
+ const isValid = await verifyPassword(password, user.passwordHash);
335
+ if (!isValid) return error({
336
+ message: "Invalid credentials",
337
+ code: "invalid_credentials"
338
+ }, 401);
339
+ const [session] = await this.handlerTx().withServiceCalls(() => [services.createSession(user.id)]).execute();
340
+ const setCookieHeader = buildSetCookieHeader(session.id, config.cookieOptions);
341
+ return json({
342
+ sessionId: session.id,
343
+ userId: user.id,
344
+ email: user.email,
345
+ role: user.role
346
+ }, { headers: { "Set-Cookie": setCookieHeader } });
347
+ }
348
+ }),
349
+ defineRoute({
350
+ method: "POST",
351
+ path: "/change-password",
352
+ inputSchema: z.object({ newPassword: z.string().min(8).max(100) }),
353
+ outputSchema: z.object({ success: z.boolean() }),
354
+ errorCodes: ["session_invalid"],
355
+ handler: async function({ input, headers, query }, { json, error }) {
356
+ const { newPassword } = await input.valid();
357
+ const sessionId = extractSessionId(headers, query.get("sessionId"));
358
+ if (!sessionId) return error({
359
+ message: "Session ID required",
360
+ code: "session_invalid"
361
+ }, 400);
362
+ const passwordHash = await hashPassword(newPassword);
363
+ const [result] = await this.handlerTx().withServiceCalls(() => [services.changePasswordWithSession(sessionId, passwordHash)]).execute();
364
+ if (!result.ok) return error({
365
+ message: "Invalid session",
366
+ code: "session_invalid"
367
+ }, 401);
368
+ return json({ success: true });
369
+ }
370
+ })
371
+ ];
372
+ });
373
+
374
+ //#endregion
375
+ //#region src/session/session.ts
376
+ function createSessionServices(cookieOptions) {
377
+ const services = {
378
+ buildSessionCookie: function(sessionId) {
379
+ return buildSetCookieHeader(sessionId, cookieOptions);
380
+ },
381
+ createSession: function(userId) {
382
+ const expiresAt = new Date();
383
+ expiresAt.setDate(expiresAt.getDate() + 30);
384
+ return this.serviceTx(authSchema).mutate(({ uow }) => {
385
+ const id = uow.create("session", {
386
+ userId,
387
+ expiresAt
388
+ });
389
+ return {
390
+ id: id.valueOf(),
391
+ userId,
392
+ expiresAt
393
+ };
394
+ }).build();
395
+ },
396
+ validateSession: function(sessionId) {
397
+ const now = new Date();
398
+ return this.serviceTx(authSchema).retrieve((uow) => uow.findFirst("session", (b) => b.whereIndex("primary", (eb) => eb("id", "=", sessionId)).join((j) => j.sessionOwner((b$1) => b$1.select([
399
+ "id",
400
+ "email",
401
+ "role"
402
+ ]))))).mutate(({ uow, retrieveResult: [session] }) => {
403
+ if (!session) return null;
404
+ if (session.expiresAt < now) {
405
+ uow.delete("session", session.id, (b) => b.check());
406
+ return null;
407
+ }
408
+ if (!session.sessionOwner) return null;
409
+ return {
410
+ id: session.id.valueOf(),
411
+ userId: session.userId,
412
+ user: {
413
+ id: session.sessionOwner.id.valueOf(),
414
+ email: session.sessionOwner.email,
415
+ role: session.sessionOwner.role
416
+ }
417
+ };
418
+ }).build();
419
+ },
420
+ invalidateSession: function(sessionId) {
421
+ return this.serviceTx(authSchema).retrieve((uow) => uow.findFirst("session", (b) => b.whereIndex("primary", (eb) => eb("id", "=", sessionId)))).mutate(({ uow, retrieveResult: [session] }) => {
422
+ if (!session) return false;
423
+ uow.delete("session", session.id, (b) => b.check());
424
+ return true;
425
+ }).build();
426
+ },
427
+ getSession: function(headers) {
428
+ const sessionId = extractSessionId(headers);
429
+ const now = new Date();
430
+ return this.serviceTx(authSchema).retrieve((uow) => uow.findFirst("session", (b) => b.whereIndex("primary", (eb) => eb("id", "=", sessionId ?? "")).join((j) => j.sessionOwner((b$1) => b$1.select([
431
+ "id",
432
+ "email",
433
+ "role"
434
+ ]))))).mutate(({ uow, retrieveResult: [session] }) => {
435
+ if (!session || !sessionId) return void 0;
436
+ if (session.expiresAt < now) {
437
+ uow.delete("session", session.id, (b) => b.check());
438
+ return void 0;
439
+ }
440
+ if (!session.sessionOwner) return void 0;
441
+ return {
442
+ userId: session.sessionOwner.id.valueOf(),
443
+ email: session.sessionOwner.email
444
+ };
445
+ }).build();
446
+ }
447
+ };
448
+ return services;
449
+ }
450
+ const sessionRoutesFactory = defineRoutes().create(({ services, config }) => {
451
+ return [defineRoute({
452
+ method: "POST",
453
+ path: "/sign-out",
454
+ inputSchema: z.object({ sessionId: z.string().optional() }).optional(),
455
+ outputSchema: z.object({ success: z.boolean() }),
456
+ errorCodes: ["session_not_found"],
457
+ handler: async function({ input, headers }, { json, error }) {
458
+ const body = await input.valid();
459
+ const sessionId = extractSessionId(headers, null, body?.sessionId);
460
+ if (!sessionId) return error({
461
+ message: "Session ID required",
462
+ code: "session_not_found"
463
+ }, 400);
464
+ const [success] = await this.handlerTx().withServiceCalls(() => [services.invalidateSession(sessionId)]).execute();
465
+ const clearCookieHeader = buildClearCookieHeader(config.cookieOptions ?? {});
466
+ if (!success) return json({ success: false }, { headers: { "Set-Cookie": clearCookieHeader } });
467
+ return json({ success: true }, { headers: { "Set-Cookie": clearCookieHeader } });
468
+ }
469
+ }), defineRoute({
470
+ method: "GET",
471
+ path: "/me",
472
+ queryParameters: ["sessionId"],
473
+ outputSchema: z.object({
474
+ userId: z.string(),
475
+ email: z.string(),
476
+ role: z.enum(["user", "admin"])
477
+ }),
478
+ errorCodes: ["session_invalid"],
479
+ handler: async function({ query, headers }, { json, error }) {
480
+ const sessionId = extractSessionId(headers, query.get("sessionId"));
481
+ if (!sessionId) return error({
482
+ message: "Session ID required",
483
+ code: "session_invalid"
484
+ }, 400);
485
+ const [session] = await this.handlerTx().withServiceCalls(() => [services.validateSession(sessionId)]).execute();
486
+ if (!session) return error({
487
+ message: "Invalid session",
488
+ code: "session_invalid"
489
+ }, 401);
490
+ return json({
491
+ userId: session.user.id,
492
+ email: session.user.email,
493
+ role: session.user.role
494
+ });
495
+ }
496
+ })];
497
+ });
498
+
499
+ //#endregion
500
+ //#region src/user/user-overview.ts
501
+ function createUserOverviewServices() {
502
+ const mapUser = (user) => ({
503
+ id: String(user.id),
504
+ email: user.email,
505
+ role: user.role,
506
+ createdAt: user.createdAt
507
+ });
508
+ return { getUsersWithCursor: function(params) {
509
+ const { search, sortBy, sortOrder, pageSize, cursor } = params;
510
+ const effectiveSortBy = search ? "email" : sortBy;
511
+ const indexName = effectiveSortBy === "email" ? "idx_user_email" : "idx_user_createdAt";
512
+ const effectiveSortOrder = cursor ? cursor.orderDirection : sortOrder;
513
+ const effectivePageSize = cursor ? cursor.pageSize : pageSize;
514
+ return this.serviceTx(authSchema).retrieve((uow) => uow.findWithCursor("user", (b) => {
515
+ if (search) {
516
+ const query$1 = b.whereIndex("idx_user_email", (eb) => eb("email", "contains", search)).orderByIndex("idx_user_email", effectiveSortOrder).pageSize(effectivePageSize);
517
+ return cursor ? query$1.after(cursor) : query$1;
518
+ }
519
+ const query = b.whereIndex(indexName).orderByIndex(indexName, effectiveSortOrder).pageSize(effectivePageSize);
520
+ return cursor ? query.after(cursor) : query;
521
+ })).transformRetrieve(([result]) => ({
522
+ users: result.items.map(mapUser),
523
+ cursor: result.cursor,
524
+ hasNextPage: result.hasNextPage
525
+ })).build();
526
+ } };
527
+ }
528
+ const sortBySchema = z.enum(["email", "createdAt"]);
529
+ const sortOrderSchema = z.enum(["asc", "desc"]);
530
+ const userOverviewRoutesFactory = defineRoutes().create(({ services }) => {
531
+ const querySchema = z.object({
532
+ search: z.string().optional(),
533
+ sortBy: sortBySchema.default("createdAt"),
534
+ sortOrder: sortOrderSchema.default("desc"),
535
+ pageSize: z.coerce.number().int().min(1).max(100).default(20)
536
+ });
537
+ const parseCursor = (cursorParam) => {
538
+ if (!cursorParam) return void 0;
539
+ try {
540
+ return decodeCursor(cursorParam);
541
+ } catch {
542
+ return void 0;
543
+ }
544
+ };
545
+ return [defineRoute({
546
+ method: "GET",
547
+ path: "/users",
548
+ queryParameters: [
549
+ "search",
550
+ "sortBy",
551
+ "sortOrder",
552
+ "pageSize",
553
+ "cursor"
554
+ ],
555
+ outputSchema: z.object({
556
+ users: z.array(z.object({
557
+ id: z.string(),
558
+ email: z.string(),
559
+ role: z.enum(["user", "admin"]),
560
+ createdAt: z.string()
561
+ })),
562
+ cursor: z.string().optional(),
563
+ hasNextPage: z.boolean(),
564
+ sortBy: sortBySchema
565
+ }),
566
+ errorCodes: ["invalid_input"],
567
+ handler: async function({ query }, { json, error }) {
568
+ const parsed = querySchema.safeParse(Object.fromEntries(query.entries()));
569
+ if (!parsed.success) return error({
570
+ message: "Invalid query parameters",
571
+ code: "invalid_input"
572
+ }, 400);
573
+ const rawSearch = parsed.data.search?.trim();
574
+ const search = rawSearch ? rawSearch : void 0;
575
+ const sortBy = search ? "email" : parsed.data.sortBy;
576
+ const params = {
577
+ search,
578
+ sortBy,
579
+ sortOrder: parsed.data.sortOrder,
580
+ pageSize: parsed.data.pageSize
581
+ };
582
+ const cursor = parseCursor(query.get("cursor"));
583
+ const [result] = await this.handlerTx().withServiceCalls(() => [services.getUsersWithCursor({
584
+ ...params,
585
+ cursor
586
+ })]).execute();
587
+ return json({
588
+ users: result.users.map((user) => ({
589
+ id: user.id,
590
+ email: user.email,
591
+ role: user.role,
592
+ createdAt: user.createdAt.toISOString()
593
+ })),
594
+ cursor: result.cursor?.encode(),
595
+ hasNextPage: result.hasNextPage,
596
+ sortBy: params.sortBy
597
+ });
598
+ }
599
+ })];
600
+ });
601
+
602
+ //#endregion
603
+ //#region src/index.ts
604
+ const authFragmentDefinition = defineFragment("auth").extend(withDatabase(authSchema)).providesBaseService(({ defineService, config }) => {
605
+ return defineService({
606
+ ...createUserServices(),
607
+ ...createSessionServices(config.cookieOptions),
608
+ ...createUserOverviewServices()
609
+ });
610
+ }).build();
611
+ function createAuthFragment(config = {}, fragnoConfig) {
612
+ const options = {
613
+ ...fragnoConfig,
614
+ databaseNamespace: fragnoConfig.databaseNamespace !== void 0 ? fragnoConfig.databaseNamespace : "simple-auth-db"
615
+ };
616
+ return instantiate(authFragmentDefinition).withConfig(config).withOptions(options).withRoutes([
617
+ userActionsRoutesFactory,
618
+ sessionRoutesFactory,
619
+ userOverviewRoutesFactory
620
+ ]).build();
621
+ }
622
+ function createAuthFragmentClients(fragnoConfig) {
623
+ const config = { ...fragnoConfig };
624
+ const b = createClientBuilder(authFragmentDefinition, config, [
625
+ userActionsRoutesFactory,
626
+ sessionRoutesFactory,
627
+ userOverviewRoutesFactory
628
+ ], {
629
+ type: "options",
630
+ options: { credentials: "include" }
631
+ });
632
+ const useMe = b.createHook("/me");
633
+ const useSignUp = b.createMutator("POST", "/sign-up");
634
+ const useSignIn = b.createMutator("POST", "/sign-in");
635
+ const useSignOut = b.createMutator("POST", "/sign-out", (invalidate) => {
636
+ invalidate("GET", "/me", {});
637
+ invalidate("GET", "/users", {});
638
+ });
639
+ const useUsers = b.createHook("/users");
640
+ const useUpdateUserRole = b.createMutator("PATCH", "/users/:userId/role", (invalidate) => {
641
+ invalidate("GET", "/users", {});
642
+ invalidate("GET", "/me", {});
643
+ });
644
+ const useChangePassword = b.createMutator("POST", "/change-password");
645
+ return {
646
+ useSignUp,
647
+ useSignIn,
648
+ useSignOut,
649
+ useMe,
650
+ useUsers,
651
+ useUpdateUserRole,
652
+ useChangePassword,
653
+ signIn: { email: async ({ email, password, rememberMe: _rememberMe }) => {
654
+ return useSignIn.mutateQuery({ body: {
655
+ email,
656
+ password
657
+ } });
658
+ } },
659
+ signUp: { email: async ({ email, password }) => {
660
+ return useSignUp.mutateQuery({ body: {
661
+ email,
662
+ password
663
+ } });
664
+ } },
665
+ signOut: (params) => {
666
+ return useSignOut.mutateQuery({ body: params?.sessionId ? { sessionId: params.sessionId } : {} });
667
+ },
668
+ me: async (params) => {
669
+ if (params?.sessionId) return useMe.query({ query: { sessionId: params.sessionId } });
670
+ return useMe.query();
671
+ }
672
+ };
673
+ }
674
+
675
+ //#endregion
676
+ export { authFragmentDefinition, createAuthFragment, createAuthFragmentClients };
677
+ //# sourceMappingURL=index.js.map