@fonderie/auth 7.8.1 → 7.9.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/brain/outcomes.md CHANGED
@@ -136,7 +136,7 @@ Raw SQL ships in `node_modules/@fonderie/auth/dist/migrations/sql/` — read it
136
136
 
137
137
  | Method | Path | Middleware chain (auth / validation / handler) |
138
138
  |---|---|---|
139
- | GET | `/_admin/users` | `async (ctx) => { const email = new URL(ctx.request.url).searchParams.get('email')?.trim().toLowerCase(); if (!email) return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'email is required'); const user = await users.findByEmail(email); return user ? setApiResponse(HTTP.OK, 'USER', 'User', toAdminUserDTO(user)) : NOT_FOUND(); }` |
139
+ | GET | `/_admin/users` | `async (ctx) => { const params = new URL(ctx.request.url).searchParams; const email = params.get('email')?.trim().toLowerCase(); if (email) { const user = await users.findByEmail(email); return user ? setApiResponse(HTTP.OK, 'USER', 'User', toAdminUserDTO(user)) : NOT_FOUND(); } const rawLimit = Number(params.get('limit') ?? 50); const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50; const cursorParam = params.get('cursor'); const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null; if (cursorParam && !cursor) return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor'); const page = await users.list({ limit, ...(cursor ? { cursor } : {}) }); return setApiResponse(HTTP.OK, 'USERS', 'Users', toAdminUserPageDTO(page)); }` |
140
140
  | GET | `/_admin/users/:id` | `async (ctx) => { const user = await users.findById(idOf(ctx)); return user ? setApiResponse(HTTP.OK, 'USER', 'User', toAdminUserDTO(user)) : NOT_FOUND(); }` |
141
141
  | GET | `/_admin/users/:id/login-history` | `async (ctx) => { if (!(await users.findById(idOf(ctx)))) return NOT_FOUND(); const params = new URL(ctx.request.url).searchParams; const rawLimit = Number(params.get('limit') ?? 50); const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50; const cursorParam = params.get('cursor'); const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null; if (cursorParam && !cursor) return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor'); const page = await events.listByUser({ userId: idOf(ctx), limit, ...(cursor ? { cursor } : {}), }); return setApiResponse( HTTP.OK, 'LOGIN_HISTORY', 'Login history', toLoginHistoryPageDTO(page), ); }` |
142
142
  | DELETE | `/_admin/users/:id/sessions` | `async (ctx) => { if (!(await users.findById(idOf(ctx)))) return NOT_FOUND(); await sessions.deleteByUser(idOf(ctx)); return setApiResponse(HTTP.OK, 'SESSIONS_REVOKED', 'All sessions revoked'); }` |
@@ -227,4 +227,6 @@ interface IAdminUserDTO extends IUserDTO {
227
227
  function describeAuthAdminRoutes(store: IStoreAdapter): IAdminRoute[]
228
228
 
229
229
  function toAdminUserDTO(user: IUser): IAdminUserDTO
230
+
231
+ function toAdminUserPageDTO(page: IUserPage): IAdminUserPageDTO
230
232
  ```
package/dist/index.cjs CHANGED
@@ -83,6 +83,7 @@ __export(index_exports, {
83
83
  schemas: () => schemas_exports,
84
84
  startUserRetention: () => startUserRetention,
85
85
  toAdminUserDTO: () => toAdminUserDTO,
86
+ toAdminUserPageDTO: () => toAdminUserPageDTO,
86
87
  toUserDTO: () => toUserDTO,
87
88
  validate: () => import_middlewares.validate,
88
89
  validateAuthConfig: () => validateAuthConfig,
@@ -583,6 +584,29 @@ var UserModel = class {
583
584
  this.store = store;
584
585
  }
585
586
  store;
587
+ // One page of users, newest first. Keyset-paginated on (created_at, id) —
588
+ // the same cursor contract as login history and the audit log. The +1
589
+ // over-fetch that detects a next page lives here so no outer clamp can
590
+ // shave it off. Soft-deleted rows are excluded, as in every other finder.
591
+ async list(query = {}) {
592
+ const limit = Math.min(query.limit ?? 50, MAX_LIST_LIMIT);
593
+ const params = [];
594
+ const where = ["deleted_at IS NULL"];
595
+ if (query.cursor) {
596
+ params.push(query.cursor.createdAt, query.cursor.id);
597
+ where.push(`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`);
598
+ }
599
+ params.push(limit + 1);
600
+ const rows = await this.store.query(
601
+ `SELECT ${USER_COLUMNS}, created_at::text AS "createdAtRaw"
602
+ FROM fonderie_users
603
+ WHERE ${where.join(" AND ")}
604
+ ORDER BY created_at DESC, id DESC
605
+ LIMIT $${params.length}`,
606
+ params
607
+ );
608
+ return { users: rows.slice(0, limit), hasMore: rows.length > limit };
609
+ }
586
610
  async findById(id) {
587
611
  const [row] = await this.store.query(
588
612
  `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1 AND deleted_at IS NULL`,
@@ -855,6 +879,7 @@ var UserModel = class {
855
879
  return row ?? null;
856
880
  }
857
881
  };
882
+ var MAX_LIST_LIMIT = 200;
858
883
 
859
884
  // src/models/session.model.ts
860
885
  var SessionModel = class {
@@ -3243,6 +3268,11 @@ function toAdminUserDTO(user) {
3243
3268
  createdAt: user.createdAt.toISOString()
3244
3269
  };
3245
3270
  }
3271
+ function toAdminUserPageDTO(page) {
3272
+ const last = page.users[page.users.length - 1];
3273
+ const nextCursor = page.hasMore && last ? (0, import_core12.encodeKeysetCursor)(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;
3274
+ return { users: page.users.map(toAdminUserDTO), nextCursor };
3275
+ }
3246
3276
  var ADMIN_PREFIX = "/_admin";
3247
3277
  var idOf = (ctx) => ctx.meta.params?.["id"] ?? "";
3248
3278
  var NOT_FOUND = () => (0, import_core12.setApiResponse)(import_core12.HTTP.NOT_FOUND, "NOT_FOUND", "No such user");
@@ -3265,12 +3295,24 @@ function adminRouteTable(store) {
3265
3295
  [
3266
3296
  "GET",
3267
3297
  "/_admin/users",
3298
+ // With an email, the exact lookup support reaches for. Without one, a
3299
+ // page of users — an operator who has to know a name before they can
3300
+ // see anything cannot discover who signed up this morning.
3268
3301
  async (ctx) => {
3269
- const email2 = new URL(ctx.request.url).searchParams.get("email")?.trim().toLowerCase();
3270
- if (!email2)
3271
- return (0, import_core12.setApiResponse)(import_core12.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
3272
- const user = await users.findByEmail(email2);
3273
- return user ? (0, import_core12.setApiResponse)(import_core12.HTTP.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
3302
+ const params = new URL(ctx.request.url).searchParams;
3303
+ const email2 = params.get("email")?.trim().toLowerCase();
3304
+ if (email2) {
3305
+ const user = await users.findByEmail(email2);
3306
+ return user ? (0, import_core12.setApiResponse)(import_core12.HTTP.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
3307
+ }
3308
+ const rawLimit = Number(params.get("limit") ?? 50);
3309
+ const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
3310
+ const cursorParam = params.get("cursor");
3311
+ const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
3312
+ if (cursorParam && !cursor)
3313
+ return (0, import_core12.setApiResponse)(import_core12.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
3314
+ const page = await users.list({ limit, ...cursor ? { cursor } : {} });
3315
+ return (0, import_core12.setApiResponse)(import_core12.HTTP.OK, "USERS", "Users", toAdminUserPageDTO(page));
3274
3316
  }
3275
3317
  ],
3276
3318
  [
@@ -3634,6 +3676,7 @@ function startUserRetention(store, options) {
3634
3676
  schemas,
3635
3677
  startUserRetention,
3636
3678
  toAdminUserDTO,
3679
+ toAdminUserPageDTO,
3637
3680
  toUserDTO,
3638
3681
  validate,
3639
3682
  validateAuthConfig,