@fonderie/auth 7.7.4 → 7.8.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/brain/outcomes.md CHANGED
@@ -136,6 +136,13 @@ 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(); }` |
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
+ | 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
+ | 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'); }` |
143
+ | GET | `/_admin/users/:id/sessions` | `async (ctx) => { if (!(await users.findById(idOf(ctx)))) return NOT_FOUND(); const rows = await sessions.listLiveByUser(idOf(ctx)); return setApiResponse( HTTP.OK, 'SESSIONS', 'Live sessions', rows.map((r) => toSessionDTO(r, null)), ); }` |
144
+ | POST | `/_admin/users/:id/suspend` | `setSuspended(true)` |
145
+ | POST | `/_admin/users/:id/unsuspend` | `setSuspended(false)` |
139
146
  | GET | `/auth/apple` | `oauth.appleInit` |
140
147
  | POST | `/auth/apple/callback` | `ipLimit('login') → oauth.appleCallback` |
141
148
  | POST | `/auth/apple/native` | `ipLimit('login') → validate(appleNativeSchema) → oauth.appleNative` |
@@ -50,6 +50,7 @@ interface IMfaChallenge {
50
50
 
51
51
  new AuthModule(store: IStoreAdapter, config: IAuthConfig, bus?: EventBus | undefined): AuthModule
52
52
  .name: "@fonderie/auth"
53
+ .describeAdmin(): IAdminDescription
53
54
  .checkReadiness(): IReadinessProblem[]
54
55
  .install(app: IFonderieApp): void
55
56
 
@@ -216,4 +217,14 @@ interface IAuthRateLimitConfig {
216
217
  }
217
218
 
218
219
  type AuthLimitedRoute = 'login' | 'register' | 'forgot' | 'reset' | 'verify' | 'mfaVerify';
220
+
221
+ interface IAdminUserDTO extends IUserDTO {
222
+ suspended: boolean;
223
+ deletedAt: string | null;
224
+ createdAt: string;
225
+ }
226
+
227
+ function describeAuthAdminRoutes(store: IStoreAdapter): IAdminRoute[]
228
+
229
+ function toAdminUserDTO(user: IUser): IAdminUserDTO
219
230
  ```
package/dist/index.cjs CHANGED
@@ -74,6 +74,7 @@ __export(index_exports, {
74
74
  MESSAGE_KEYS: () => MESSAGE_KEYS,
75
75
  buildAuthAccountLimiter: () => buildAuthAccountLimiter,
76
76
  buildAuthIpLimiter: () => buildAuthIpLimiter,
77
+ describeAuthAdminRoutes: () => describeAuthAdminRoutes,
77
78
  importUser: () => importUser,
78
79
  normalizeEmail: () => normalizeEmail,
79
80
  normalizeEmailSafe: () => normalizeEmailSafe,
@@ -81,6 +82,7 @@ __export(index_exports, {
81
82
  requireAuth: () => import_middlewares3.requireAuth,
82
83
  schemas: () => schemas_exports,
83
84
  startUserRetention: () => startUserRetention,
85
+ toAdminUserDTO: () => toAdminUserDTO,
84
86
  toUserDTO: () => toUserDTO,
85
87
  validate: () => import_middlewares.validate,
86
88
  validateAuthConfig: () => validateAuthConfig,
@@ -686,6 +688,15 @@ var UserModel = class {
686
688
  [id]
687
689
  );
688
690
  }
691
+ // The operator's lock: login, refresh and the session middleware all refuse a
692
+ // suspended user. Returns false when there is no such user.
693
+ async setSuspended(id, suspended) {
694
+ const rows = await this.store.query(
695
+ `UPDATE fonderie_users SET suspended = $2, updated_at = now() WHERE id = $1 AND deleted_at IS NULL RETURNING id`,
696
+ [id, suspended]
697
+ );
698
+ return rows.length > 0;
699
+ }
689
700
  async softDelete(id) {
690
701
  await this.store.query(
691
702
  `UPDATE fonderie_users SET deleted_at = now(), updated_at = now() WHERE id = $1`,
@@ -3222,6 +3233,115 @@ function extractToken(request) {
3222
3233
  return match?.[1] ?? null;
3223
3234
  }
3224
3235
 
3236
+ // src/admin.ts
3237
+ var import_core12 = require("@fonderie/core");
3238
+ function toAdminUserDTO(user) {
3239
+ return {
3240
+ ...toUserDTO(user),
3241
+ suspended: Boolean(user.suspended),
3242
+ deletedAt: user.deletedAt ? user.deletedAt.toISOString() : null,
3243
+ createdAt: user.createdAt.toISOString()
3244
+ };
3245
+ }
3246
+ var ADMIN_PREFIX = "/_admin";
3247
+ var idOf = (ctx) => ctx.meta.params?.["id"] ?? "";
3248
+ var NOT_FOUND = () => (0, import_core12.setApiResponse)(import_core12.HTTP.NOT_FOUND, "NOT_FOUND", "No such user");
3249
+ function adminRouteTable(store) {
3250
+ const users = new UserModel(store);
3251
+ const sessions = new SessionModel(store);
3252
+ const events = new LoginEventModel(store);
3253
+ const setSuspended = (suspended) => async (ctx) => {
3254
+ const ok = await users.setSuspended(idOf(ctx), suspended);
3255
+ if (!ok) return NOT_FOUND();
3256
+ const user = await users.findById(idOf(ctx));
3257
+ return user ? (0, import_core12.setApiResponse)(
3258
+ import_core12.HTTP.OK,
3259
+ suspended ? "USER_SUSPENDED" : "USER_UNSUSPENDED",
3260
+ suspended ? "User suspended" : "User unsuspended",
3261
+ toAdminUserDTO(user)
3262
+ ) : NOT_FOUND();
3263
+ };
3264
+ return [
3265
+ [
3266
+ "GET",
3267
+ "/_admin/users",
3268
+ 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();
3274
+ }
3275
+ ],
3276
+ [
3277
+ "GET",
3278
+ "/_admin/users/:id",
3279
+ async (ctx) => {
3280
+ const user = await users.findById(idOf(ctx));
3281
+ return user ? (0, import_core12.setApiResponse)(import_core12.HTTP.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
3282
+ }
3283
+ ],
3284
+ [
3285
+ "GET",
3286
+ "/_admin/users/:id/sessions",
3287
+ async (ctx) => {
3288
+ if (!await users.findById(idOf(ctx))) return NOT_FOUND();
3289
+ const rows = await sessions.listLiveByUser(idOf(ctx));
3290
+ return (0, import_core12.setApiResponse)(
3291
+ import_core12.HTTP.OK,
3292
+ "SESSIONS",
3293
+ "Live sessions",
3294
+ rows.map((r) => toSessionDTO(r, null))
3295
+ );
3296
+ }
3297
+ ],
3298
+ // Signs the user out everywhere. The one write support reaches for first.
3299
+ [
3300
+ "DELETE",
3301
+ "/_admin/users/:id/sessions",
3302
+ async (ctx) => {
3303
+ if (!await users.findById(idOf(ctx))) return NOT_FOUND();
3304
+ await sessions.deleteByUser(idOf(ctx));
3305
+ return (0, import_core12.setApiResponse)(import_core12.HTTP.OK, "SESSIONS_REVOKED", "All sessions revoked");
3306
+ }
3307
+ ],
3308
+ [
3309
+ "GET",
3310
+ "/_admin/users/:id/login-history",
3311
+ async (ctx) => {
3312
+ if (!await users.findById(idOf(ctx))) return NOT_FOUND();
3313
+ const params = new URL(ctx.request.url).searchParams;
3314
+ const rawLimit = Number(params.get("limit") ?? 50);
3315
+ const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
3316
+ const cursorParam = params.get("cursor");
3317
+ const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
3318
+ if (cursorParam && !cursor)
3319
+ return (0, import_core12.setApiResponse)(import_core12.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
3320
+ const page = await events.listByUser({
3321
+ userId: idOf(ctx),
3322
+ limit,
3323
+ ...cursor ? { cursor } : {}
3324
+ });
3325
+ return (0, import_core12.setApiResponse)(
3326
+ import_core12.HTTP.OK,
3327
+ "LOGIN_HISTORY",
3328
+ "Login history",
3329
+ toLoginHistoryPageDTO(page)
3330
+ );
3331
+ }
3332
+ ],
3333
+ ["POST", "/_admin/users/:id/suspend", setSuspended(true)],
3334
+ ["POST", "/_admin/users/:id/unsuspend", setSuspended(false)]
3335
+ ];
3336
+ }
3337
+ function describeAuthAdminRoutes(store) {
3338
+ return adminRouteTable(store).map(([method, path, h]) => ({
3339
+ method,
3340
+ path: path.slice(ADMIN_PREFIX.length),
3341
+ handlers: [h]
3342
+ }));
3343
+ }
3344
+
3225
3345
  // src/module.ts
3226
3346
  var AuthModule = class {
3227
3347
  constructor(store, config, bus) {
@@ -3234,6 +3354,10 @@ var AuthModule = class {
3234
3354
  config;
3235
3355
  bus;
3236
3356
  name = "@fonderie/auth";
3357
+ // Users, sessions, login history, suspend — only through @fonderie/admin.
3358
+ describeAdmin() {
3359
+ return { routes: describeAuthAdminRoutes(this.store) };
3360
+ }
3237
3361
  // Report config problems for app.checkProductionReadiness() (data, not throw).
3238
3362
  checkReadiness() {
3239
3363
  return collectAuthConfigProblems(this.config);
@@ -3501,6 +3625,7 @@ function startUserRetention(store, options) {
3501
3625
  MESSAGE_KEYS,
3502
3626
  buildAuthAccountLimiter,
3503
3627
  buildAuthIpLimiter,
3628
+ describeAuthAdminRoutes,
3504
3629
  importUser,
3505
3630
  normalizeEmail,
3506
3631
  normalizeEmailSafe,
@@ -3508,6 +3633,7 @@ function startUserRetention(store, options) {
3508
3633
  requireAuth,
3509
3634
  schemas,
3510
3635
  startUserRetention,
3636
+ toAdminUserDTO,
3511
3637
  toUserDTO,
3512
3638
  validate,
3513
3639
  validateAuthConfig,