@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 +7 -0
- package/brain/signatures.md +11 -0
- package/dist/index.cjs +126 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +124 -0
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +9 -0
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.js +9 -0
- package/dist/middlewares/index.js.map +1 -1
- package/package.json +1 -1
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` |
|
package/brain/signatures.md
CHANGED
|
@@ -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,
|