najm-auth 1.1.36 → 1.1.39
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/README.md +17 -12
- package/dist/client/edge.d.ts +47 -0
- package/dist/client/edge.js +88 -0
- package/dist/client/server/index.d.ts +3 -46
- package/dist/index.d.ts +84 -38
- package/dist/index.js +551 -489
- package/package.json +15 -13
package/dist/index.js
CHANGED
|
@@ -228,6 +228,229 @@ var authSchema3 = {
|
|
|
228
228
|
rolePermissions: rolePermissionsTable3
|
|
229
229
|
};
|
|
230
230
|
|
|
231
|
+
// src/auth/index.ts
|
|
232
|
+
var auth_exports = {};
|
|
233
|
+
__export(auth_exports, {
|
|
234
|
+
AUTH_MODULE: () => AUTH_MODULE,
|
|
235
|
+
AuthController: () => AuthController,
|
|
236
|
+
AuthGuard: () => AuthGuard,
|
|
237
|
+
AuthResolver: () => AuthResolver,
|
|
238
|
+
AuthService: () => AuthService,
|
|
239
|
+
CookieManager: () => CookieManager,
|
|
240
|
+
EncryptionService: () => EncryptionService,
|
|
241
|
+
isAuth: () => isAuth,
|
|
242
|
+
runAsUser: () => runAsUser
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// src/auth/EncryptionService.ts
|
|
246
|
+
import { Inject, Injectable } from "najm-core";
|
|
247
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
248
|
+
|
|
249
|
+
// src/auth/password.ts
|
|
250
|
+
import bcrypt from "bcryptjs";
|
|
251
|
+
var getBunPassword = /* @__PURE__ */ __name(() => globalThis.Bun?.password, "getBunPassword");
|
|
252
|
+
async function hashPassword(password, rounds) {
|
|
253
|
+
if (!Number.isInteger(rounds) || rounds < 4 || rounds > 31) {
|
|
254
|
+
throw new Error("bcrypt rounds must be an integer between 4 and 31");
|
|
255
|
+
}
|
|
256
|
+
const bunPassword = getBunPassword();
|
|
257
|
+
if (bunPassword) {
|
|
258
|
+
return bunPassword.hash(password, { algorithm: "bcrypt", cost: rounds });
|
|
259
|
+
}
|
|
260
|
+
return bcrypt.hash(password, rounds);
|
|
261
|
+
}
|
|
262
|
+
__name(hashPassword, "hashPassword");
|
|
263
|
+
async function verifyPassword(password, hash) {
|
|
264
|
+
const bunPassword = getBunPassword();
|
|
265
|
+
if (bunPassword) {
|
|
266
|
+
return bunPassword.verify(password, hash);
|
|
267
|
+
}
|
|
268
|
+
return bcrypt.compare(password, hash);
|
|
269
|
+
}
|
|
270
|
+
__name(verifyPassword, "verifyPassword");
|
|
271
|
+
|
|
272
|
+
// src/auth/EncryptionService.ts
|
|
273
|
+
var __decorate = function(decorators, target, key, desc) {
|
|
274
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
275
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
276
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
277
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
278
|
+
};
|
|
279
|
+
var __metadata = function(k, v) {
|
|
280
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
281
|
+
};
|
|
282
|
+
var __param = function(paramIndex, decorator) {
|
|
283
|
+
return function(target, key) {
|
|
284
|
+
decorator(target, key, paramIndex);
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
var REQUIRED_KEY_BYTES = 32;
|
|
288
|
+
function decodeKey(raw) {
|
|
289
|
+
if (/^[0-9a-fA-F]{64}$/.test(raw)) {
|
|
290
|
+
return Buffer.from(raw, "hex");
|
|
291
|
+
}
|
|
292
|
+
if (/^[A-Za-z0-9+/]+=*$/.test(raw) || /^[A-Za-z0-9_-]+=*$/.test(raw)) {
|
|
293
|
+
const decoded = Buffer.from(raw, raw.includes("-") || raw.includes("_") ? "base64url" : "base64");
|
|
294
|
+
if (decoded.length === REQUIRED_KEY_BYTES)
|
|
295
|
+
return decoded;
|
|
296
|
+
}
|
|
297
|
+
if (Buffer.byteLength(raw, "utf8") === REQUIRED_KEY_BYTES) {
|
|
298
|
+
return Buffer.from(raw, "utf8");
|
|
299
|
+
}
|
|
300
|
+
throw new Error(`Encryption key must decode to ${REQUIRED_KEY_BYTES} bytes. Provide hex (64 chars) or base64 (32 bytes decoded).`);
|
|
301
|
+
}
|
|
302
|
+
__name(decodeKey, "decodeKey");
|
|
303
|
+
var EncryptionService = class EncryptionService2 {
|
|
304
|
+
static {
|
|
305
|
+
__name(this, "EncryptionService");
|
|
306
|
+
}
|
|
307
|
+
config;
|
|
308
|
+
encryptionKey;
|
|
309
|
+
constructor(encryptionKey) {
|
|
310
|
+
const raw = encryptionKey ?? process.env.NAJM_ENCRYPTION_KEY ?? "";
|
|
311
|
+
if (!raw) {
|
|
312
|
+
throw new Error("Encryption key missing. Set NAJM_ENCRYPTION_KEY (base64 or hex, 32 bytes) or pass auth({ encryptionKey })");
|
|
313
|
+
}
|
|
314
|
+
this.encryptionKey = decodeKey(raw);
|
|
315
|
+
}
|
|
316
|
+
async hashPassword(password) {
|
|
317
|
+
return hashPassword(password, this.config?.bcryptRounds ?? 10);
|
|
318
|
+
}
|
|
319
|
+
async comparePassword(password, hashedPassword) {
|
|
320
|
+
return verifyPassword(password, hashedPassword);
|
|
321
|
+
}
|
|
322
|
+
encrypt(plaintext) {
|
|
323
|
+
const iv = randomBytes(12);
|
|
324
|
+
const cipher = createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
325
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
326
|
+
const tag = cipher.getAuthTag();
|
|
327
|
+
return Buffer.concat([iv, tag, encrypted]).toString("base64url");
|
|
328
|
+
}
|
|
329
|
+
decrypt(ciphertext) {
|
|
330
|
+
const raw = Buffer.from(ciphertext, "base64url");
|
|
331
|
+
const iv = raw.subarray(0, 12);
|
|
332
|
+
const tag = raw.subarray(12, 28);
|
|
333
|
+
const encrypted = raw.subarray(28);
|
|
334
|
+
const decipher = createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
335
|
+
decipher.setAuthTag(tag);
|
|
336
|
+
return decipher.update(encrypted) + decipher.final("utf8");
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
__decorate([
|
|
340
|
+
Inject(AUTH_CONFIG),
|
|
341
|
+
__metadata("design:type", Object)
|
|
342
|
+
], EncryptionService.prototype, "config", void 0);
|
|
343
|
+
EncryptionService = __decorate([
|
|
344
|
+
Injectable(),
|
|
345
|
+
__param(0, Inject(AUTH_ENCRYPTION_KEY)),
|
|
346
|
+
__metadata("design:paramtypes", [String])
|
|
347
|
+
], EncryptionService);
|
|
348
|
+
|
|
349
|
+
// src/auth/CookieManager.ts
|
|
350
|
+
import { Service, Inject as Inject2 } from "najm-core";
|
|
351
|
+
import { CookieService } from "najm-cookies";
|
|
352
|
+
import timestring from "timestring";
|
|
353
|
+
var __decorate2 = function(decorators, target, key, desc) {
|
|
354
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
355
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
356
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
357
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
358
|
+
};
|
|
359
|
+
var __metadata2 = function(k, v) {
|
|
360
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
361
|
+
};
|
|
362
|
+
var _a;
|
|
363
|
+
var CookieManager = class CookieManager2 {
|
|
364
|
+
static {
|
|
365
|
+
__name(this, "CookieManager");
|
|
366
|
+
}
|
|
367
|
+
config;
|
|
368
|
+
cookieService;
|
|
369
|
+
get cookieName() {
|
|
370
|
+
return this.config.refreshCookieName || "refreshToken";
|
|
371
|
+
}
|
|
372
|
+
get sessionCookieName() {
|
|
373
|
+
return this.config.session.name;
|
|
374
|
+
}
|
|
375
|
+
get sessionMaxAge() {
|
|
376
|
+
return this.config.session.maxAge;
|
|
377
|
+
}
|
|
378
|
+
get sessionSecret() {
|
|
379
|
+
return this.config.session.secret || this.config.jwt.accessSecret;
|
|
380
|
+
}
|
|
381
|
+
// =========================================================================
|
|
382
|
+
// Refresh Token Cookie
|
|
383
|
+
// =========================================================================
|
|
384
|
+
setRefreshToken(refreshToken) {
|
|
385
|
+
const maxAge = timestring(this.config.jwt.refreshExpiresIn, "s");
|
|
386
|
+
this.cookieService.set(this.cookieName, refreshToken, { maxAge });
|
|
387
|
+
}
|
|
388
|
+
clearRefreshToken() {
|
|
389
|
+
this.cookieService.delete(this.cookieName);
|
|
390
|
+
}
|
|
391
|
+
getRefreshToken() {
|
|
392
|
+
return this.cookieService.get(this.cookieName);
|
|
393
|
+
}
|
|
394
|
+
hasRefreshToken() {
|
|
395
|
+
return this.cookieService.has(this.cookieName);
|
|
396
|
+
}
|
|
397
|
+
getCookieName() {
|
|
398
|
+
return this.cookieName;
|
|
399
|
+
}
|
|
400
|
+
// =========================================================================
|
|
401
|
+
// Session Cookie Cache (HMAC-signed, short-lived)
|
|
402
|
+
// =========================================================================
|
|
403
|
+
/**
|
|
404
|
+
* Write a signed session cookie containing user data, roles, and permissions.
|
|
405
|
+
* The cookie is HMAC-signed with the access secret so it is tamper-proof
|
|
406
|
+
* but readable without a database query. Short TTL (5 min) ensures freshness.
|
|
407
|
+
*/
|
|
408
|
+
setSessionCookie(data) {
|
|
409
|
+
const payload = { ...data, iat: Date.now() };
|
|
410
|
+
this.cookieService.setSigned(this.sessionCookieName, JSON.stringify(payload), this.sessionSecret, {
|
|
411
|
+
maxAge: this.sessionMaxAge,
|
|
412
|
+
httpOnly: true,
|
|
413
|
+
sameSite: "Lax",
|
|
414
|
+
path: "/"
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Read and verify the session cookie. Returns null if missing, expired,
|
|
419
|
+
* or signature verification fails.
|
|
420
|
+
*/
|
|
421
|
+
getSessionCookie() {
|
|
422
|
+
const raw = this.cookieService.getSigned(this.sessionCookieName, this.sessionSecret);
|
|
423
|
+
if (!raw)
|
|
424
|
+
return null;
|
|
425
|
+
try {
|
|
426
|
+
const data = JSON.parse(raw);
|
|
427
|
+
const age = Date.now() - data.iat;
|
|
428
|
+
if (age > this.sessionMaxAge * 1e3)
|
|
429
|
+
return null;
|
|
430
|
+
return data;
|
|
431
|
+
} catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Clear the session cookie (on logout, password change, etc.)
|
|
437
|
+
*/
|
|
438
|
+
clearSessionCookie() {
|
|
439
|
+
this.cookieService.delete(this.sessionCookieName);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
__decorate2([
|
|
443
|
+
Inject2(AUTH_CONFIG),
|
|
444
|
+
__metadata2("design:type", Object)
|
|
445
|
+
], CookieManager.prototype, "config", void 0);
|
|
446
|
+
__decorate2([
|
|
447
|
+
Inject2(CookieService),
|
|
448
|
+
__metadata2("design:type", typeof (_a = typeof CookieService !== "undefined" && CookieService) === "function" ? _a : Object)
|
|
449
|
+
], CookieManager.prototype, "cookieService", void 0);
|
|
450
|
+
CookieManager = __decorate2([
|
|
451
|
+
Service()
|
|
452
|
+
], CookieManager);
|
|
453
|
+
|
|
231
454
|
// src/auth/AuthController.ts
|
|
232
455
|
import { Controller } from "najm-core";
|
|
233
456
|
import { Get, Post, ResMsg } from "najm-core";
|
|
@@ -236,17 +459,18 @@ import { Body, User as User2, Headers } from "najm-core";
|
|
|
236
459
|
// src/auth/AuthService.ts
|
|
237
460
|
import { Injectable as Injectable7, Inject as Inject8 } from "najm-core";
|
|
238
461
|
import { Err as Err6, Log } from "najm-core";
|
|
239
|
-
import { I18n as
|
|
462
|
+
import { I18n as I18n5, I18nService as I18nService2 } from "najm-i18n";
|
|
240
463
|
import { EmailService, passwordResetTemplate } from "najm-email";
|
|
241
464
|
|
|
242
465
|
// src/users/UserService.ts
|
|
243
|
-
import { Injectable as Injectable5, Inject as
|
|
466
|
+
import { Injectable as Injectable5, Inject as Inject5 } from "najm-core";
|
|
244
467
|
import { Transaction } from "najm-database";
|
|
245
468
|
import { I18nService } from "najm-i18n";
|
|
469
|
+
import { I18n as I18n3 } from "najm-i18n";
|
|
246
470
|
|
|
247
471
|
// src/users/UserRepository.ts
|
|
248
|
-
import { eq as eq2, ne } from "drizzle-orm";
|
|
249
|
-
import { Repository, Inject } from "najm-core";
|
|
472
|
+
import { eq as eq2, ne, sql as sql4 } from "drizzle-orm";
|
|
473
|
+
import { Repository, Inject as Inject3 } from "najm-core";
|
|
250
474
|
import { DB } from "najm-database";
|
|
251
475
|
|
|
252
476
|
// src/shared/queries.ts
|
|
@@ -279,14 +503,22 @@ var AuthQueries = class {
|
|
|
279
503
|
updatedAt: this.schema.users.updatedAt
|
|
280
504
|
};
|
|
281
505
|
}
|
|
506
|
+
async getUserWithPermissions(where2) {
|
|
507
|
+
const rows = await this.db.select({
|
|
508
|
+
...this.userSelection(),
|
|
509
|
+
permissionName: this.schema.permissions.name
|
|
510
|
+
}).from(this.schema.users).leftJoin(this.schema.roles, eq(this.schema.users.roleId, this.schema.roles.id)).leftJoin(this.schema.rolePermissions, eq(this.schema.users.roleId, this.schema.rolePermissions.roleId)).leftJoin(this.schema.permissions, eq(this.schema.rolePermissions.permissionId, this.schema.permissions.id)).where(where2);
|
|
511
|
+
if (rows.length === 0)
|
|
512
|
+
return void 0;
|
|
513
|
+
const { permissionName: _permissionName, ...user } = rows[0];
|
|
514
|
+
const permissions = [...new Set(rows.map((row) => row.permissionName).filter(Boolean))];
|
|
515
|
+
return { ...user, permissions };
|
|
516
|
+
}
|
|
282
517
|
/**
|
|
283
518
|
* Get permissions for a user by their userId
|
|
284
519
|
*/
|
|
285
520
|
async getUserPermissions(userId) {
|
|
286
|
-
const
|
|
287
|
-
if (!user?.roleId)
|
|
288
|
-
return [];
|
|
289
|
-
const perms = await this.db.select({ name: this.schema.permissions.name }).from(this.schema.rolePermissions).leftJoin(this.schema.permissions, eq(this.schema.rolePermissions.permissionId, this.schema.permissions.id)).where(eq(this.schema.rolePermissions.roleId, user.roleId));
|
|
521
|
+
const perms = await this.db.select({ name: this.schema.permissions.name }).from(this.schema.users).leftJoin(this.schema.rolePermissions, eq(this.schema.users.roleId, this.schema.rolePermissions.roleId)).leftJoin(this.schema.permissions, eq(this.schema.rolePermissions.permissionId, this.schema.permissions.id)).where(eq(this.schema.users.id, userId));
|
|
290
522
|
return perms.map((p) => p.name).filter(Boolean);
|
|
291
523
|
}
|
|
292
524
|
/**
|
|
@@ -301,16 +533,13 @@ var AuthQueries = class {
|
|
|
301
533
|
* Returns { roleName: string | null, permissions: string[] }
|
|
302
534
|
*/
|
|
303
535
|
async getRoleAndPermissions(userId) {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
const [roleRow] = await this.db.select({ roleName: this.schema.roles.name }).from(this.schema.roles).where(eq(this.schema.roles.id, user.roleId)).limit(1);
|
|
310
|
-
const perms = await this.db.select({ name: this.schema.permissions.name }).from(this.schema.rolePermissions).leftJoin(this.schema.permissions, eq(this.schema.rolePermissions.permissionId, this.schema.permissions.id)).where(eq(this.schema.rolePermissions.roleId, user.roleId));
|
|
536
|
+
const rows = await this.db.select({
|
|
537
|
+
roleName: this.schema.roles.name,
|
|
538
|
+
permissionName: this.schema.permissions.name
|
|
539
|
+
}).from(this.schema.users).leftJoin(this.schema.roles, eq(this.schema.users.roleId, this.schema.roles.id)).leftJoin(this.schema.rolePermissions, eq(this.schema.users.roleId, this.schema.rolePermissions.roleId)).leftJoin(this.schema.permissions, eq(this.schema.rolePermissions.permissionId, this.schema.permissions.id)).where(eq(this.schema.users.id, userId));
|
|
311
540
|
return {
|
|
312
|
-
roleName:
|
|
313
|
-
permissions:
|
|
541
|
+
roleName: rows[0]?.roleName ?? null,
|
|
542
|
+
permissions: [...new Set(rows.map((row) => row.permissionName).filter(Boolean))]
|
|
314
543
|
};
|
|
315
544
|
}
|
|
316
545
|
/**
|
|
@@ -343,13 +572,13 @@ var ROLE_GROUPS = {
|
|
|
343
572
|
};
|
|
344
573
|
|
|
345
574
|
// src/users/UserRepository.ts
|
|
346
|
-
var
|
|
575
|
+
var __decorate3 = function(decorators, target, key, desc) {
|
|
347
576
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
348
577
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
349
578
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
350
579
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
351
580
|
};
|
|
352
|
-
var
|
|
581
|
+
var __metadata3 = function(k, v) {
|
|
353
582
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
354
583
|
};
|
|
355
584
|
var UserRepository = class UserRepository2 {
|
|
@@ -365,11 +594,12 @@ var UserRepository = class UserRepository2 {
|
|
|
365
594
|
return this.schema.roles;
|
|
366
595
|
}
|
|
367
596
|
/** Shared query helper */
|
|
597
|
+
queryHelper;
|
|
368
598
|
get q() {
|
|
369
|
-
return new AuthQueries(this.db, this.schema);
|
|
599
|
+
return this.queryHelper ??= new AuthQueries(this.db, this.schema);
|
|
370
600
|
}
|
|
371
|
-
async getAll() {
|
|
372
|
-
const allUsers = await this.db.select(this.q.userSelection()).from(this.users).leftJoin(this.roles, eq2(this.users.roleId, this.roles.id));
|
|
601
|
+
async getAll(limit = 50, offset = 0) {
|
|
602
|
+
const allUsers = await this.db.select(this.q.userSelection()).from(this.users).leftJoin(this.roles, eq2(this.users.roleId, this.roles.id)).limit(limit).offset(offset);
|
|
373
603
|
const permissionsByRole = await this.q.batchLoadPermissionsByRole();
|
|
374
604
|
return allUsers.map((user) => ({
|
|
375
605
|
...user,
|
|
@@ -377,13 +607,11 @@ var UserRepository = class UserRepository2 {
|
|
|
377
607
|
}));
|
|
378
608
|
}
|
|
379
609
|
async getById(id) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
permissions: await this.q.getUserPermissions(user.id)
|
|
386
|
-
};
|
|
610
|
+
return this.q.getUserWithPermissions(eq2(this.users.id, id));
|
|
611
|
+
}
|
|
612
|
+
async existsById(id) {
|
|
613
|
+
const [user] = await this.db.select({ id: this.users.id }).from(this.users).where(eq2(this.users.id, id)).limit(1);
|
|
614
|
+
return Boolean(user);
|
|
387
615
|
}
|
|
388
616
|
async getRawById(id) {
|
|
389
617
|
const [user] = await this.db.select().from(this.users).where(eq2(this.users.id, id)).limit(1);
|
|
@@ -411,8 +639,7 @@ var UserRepository = class UserRepository2 {
|
|
|
411
639
|
return updatedUser;
|
|
412
640
|
}
|
|
413
641
|
async incrementFailedAttempts(id) {
|
|
414
|
-
const
|
|
415
|
-
const [updatedUser] = await this.db.update(this.users).set({ failedLoginAttempts: (user?.failedLoginAttempts ?? 0) + 1 }).where(eq2(this.users.id, id)).returning();
|
|
642
|
+
const [updatedUser] = await this.db.update(this.users).set({ failedLoginAttempts: sql4`coalesce(${this.users.failedLoginAttempts}, 0) + 1` }).where(eq2(this.users.id, id)).returning();
|
|
416
643
|
return updatedUser;
|
|
417
644
|
}
|
|
418
645
|
async resetFailedAttempts(id) {
|
|
@@ -440,21 +667,7 @@ var UserRepository = class UserRepository2 {
|
|
|
440
667
|
return this.q.getRoleName(userId);
|
|
441
668
|
}
|
|
442
669
|
async findByPhone(phone) {
|
|
443
|
-
|
|
444
|
-
if (!user)
|
|
445
|
-
return void 0;
|
|
446
|
-
return {
|
|
447
|
-
...user,
|
|
448
|
-
permissions: await this.q.getUserPermissions(user.id)
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
async getUserPassword(email2) {
|
|
452
|
-
const [user] = await this.db.select({
|
|
453
|
-
id: this.users.id,
|
|
454
|
-
email: this.users.email,
|
|
455
|
-
password: this.users.password
|
|
456
|
-
}).from(this.users).where(eq2(this.users.email, email2)).limit(1);
|
|
457
|
-
return user?.password;
|
|
670
|
+
return this.q.getUserWithPermissions(eq2(this.users.phone, phone));
|
|
458
671
|
}
|
|
459
672
|
async getUserPermissions(userId) {
|
|
460
673
|
return this.q.getUserPermissions(userId);
|
|
@@ -464,15 +677,15 @@ var UserRepository = class UserRepository2 {
|
|
|
464
677
|
return updatedUser;
|
|
465
678
|
}
|
|
466
679
|
};
|
|
467
|
-
|
|
680
|
+
__decorate3([
|
|
468
681
|
DB(),
|
|
469
|
-
|
|
682
|
+
__metadata3("design:type", Object)
|
|
470
683
|
], UserRepository.prototype, "db", void 0);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
684
|
+
__decorate3([
|
|
685
|
+
Inject3(AUTH_SCHEMA),
|
|
686
|
+
__metadata3("design:type", Object)
|
|
474
687
|
], UserRepository.prototype, "schema", void 0);
|
|
475
|
-
UserRepository =
|
|
688
|
+
UserRepository = __decorate3([
|
|
476
689
|
Repository()
|
|
477
690
|
], UserRepository);
|
|
478
691
|
|
|
@@ -480,93 +693,16 @@ UserRepository = __decorate([
|
|
|
480
693
|
import { Injectable as Injectable2 } from "najm-core";
|
|
481
694
|
import { Err } from "najm-core";
|
|
482
695
|
import { I18n } from "najm-i18n";
|
|
483
|
-
|
|
484
|
-
// src/auth/EncryptionService.ts
|
|
485
|
-
import { Inject as Inject2, Injectable } from "najm-core";
|
|
486
|
-
import bcrypt from "bcryptjs";
|
|
487
|
-
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
488
|
-
var __decorate2 = function(decorators, target, key, desc) {
|
|
489
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
490
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
491
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
492
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
493
|
-
};
|
|
494
|
-
var __metadata2 = function(k, v) {
|
|
495
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
496
|
-
};
|
|
497
|
-
var __param = function(paramIndex, decorator) {
|
|
498
|
-
return function(target, key) {
|
|
499
|
-
decorator(target, key, paramIndex);
|
|
500
|
-
};
|
|
501
|
-
};
|
|
502
|
-
var REQUIRED_KEY_BYTES = 32;
|
|
503
|
-
function decodeKey(raw) {
|
|
504
|
-
if (/^[0-9a-fA-F]{64}$/.test(raw)) {
|
|
505
|
-
return Buffer.from(raw, "hex");
|
|
506
|
-
}
|
|
507
|
-
if (/^[A-Za-z0-9+/]+=*$/.test(raw) || /^[A-Za-z0-9_-]+=*$/.test(raw)) {
|
|
508
|
-
const decoded = Buffer.from(raw, raw.includes("-") || raw.includes("_") ? "base64url" : "base64");
|
|
509
|
-
if (decoded.length === REQUIRED_KEY_BYTES)
|
|
510
|
-
return decoded;
|
|
511
|
-
}
|
|
512
|
-
if (Buffer.byteLength(raw, "utf8") === REQUIRED_KEY_BYTES) {
|
|
513
|
-
return Buffer.from(raw, "utf8");
|
|
514
|
-
}
|
|
515
|
-
throw new Error(`Encryption key must decode to ${REQUIRED_KEY_BYTES} bytes. Provide hex (64 chars) or base64 (32 bytes decoded).`);
|
|
516
|
-
}
|
|
517
|
-
__name(decodeKey, "decodeKey");
|
|
518
|
-
var EncryptionService = class EncryptionService2 {
|
|
519
|
-
static {
|
|
520
|
-
__name(this, "EncryptionService");
|
|
521
|
-
}
|
|
522
|
-
encryptionKey;
|
|
523
|
-
constructor(encryptionKey) {
|
|
524
|
-
const raw = encryptionKey ?? process.env.NAJM_ENCRYPTION_KEY ?? "";
|
|
525
|
-
if (!raw) {
|
|
526
|
-
throw new Error("Encryption key missing. Set NAJM_ENCRYPTION_KEY (base64 or hex, 32 bytes) or pass auth({ encryptionKey })");
|
|
527
|
-
}
|
|
528
|
-
this.encryptionKey = decodeKey(raw);
|
|
529
|
-
}
|
|
530
|
-
async hashPassword(password) {
|
|
531
|
-
return bcrypt.hash(password, 10);
|
|
532
|
-
}
|
|
533
|
-
async comparePassword(password, hashedPassword) {
|
|
534
|
-
return bcrypt.compare(password, hashedPassword);
|
|
535
|
-
}
|
|
536
|
-
encrypt(plaintext) {
|
|
537
|
-
const iv = randomBytes(12);
|
|
538
|
-
const cipher = createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
539
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
540
|
-
const tag = cipher.getAuthTag();
|
|
541
|
-
return Buffer.concat([iv, tag, encrypted]).toString("base64url");
|
|
542
|
-
}
|
|
543
|
-
decrypt(ciphertext) {
|
|
544
|
-
const raw = Buffer.from(ciphertext, "base64url");
|
|
545
|
-
const iv = raw.subarray(0, 12);
|
|
546
|
-
const tag = raw.subarray(12, 28);
|
|
547
|
-
const encrypted = raw.subarray(28);
|
|
548
|
-
const decipher = createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
549
|
-
decipher.setAuthTag(tag);
|
|
550
|
-
return decipher.update(encrypted) + decipher.final("utf8");
|
|
551
|
-
}
|
|
552
|
-
};
|
|
553
|
-
EncryptionService = __decorate2([
|
|
554
|
-
Injectable(),
|
|
555
|
-
__param(0, Inject2(AUTH_ENCRYPTION_KEY)),
|
|
556
|
-
__metadata2("design:paramtypes", [String])
|
|
557
|
-
], EncryptionService);
|
|
558
|
-
|
|
559
|
-
// src/users/UserValidator.ts
|
|
560
|
-
var __decorate3 = function(decorators, target, key, desc) {
|
|
696
|
+
var __decorate4 = function(decorators, target, key, desc) {
|
|
561
697
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
562
698
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
563
699
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
564
700
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
565
701
|
};
|
|
566
|
-
var
|
|
702
|
+
var __metadata4 = function(k, v) {
|
|
567
703
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
568
704
|
};
|
|
569
|
-
var
|
|
705
|
+
var _a2;
|
|
570
706
|
var _b;
|
|
571
707
|
var UserValidator = class UserValidator2 {
|
|
572
708
|
static {
|
|
@@ -594,11 +730,11 @@ var UserValidator = class UserValidator2 {
|
|
|
594
730
|
* Check if user exists by ID
|
|
595
731
|
*/
|
|
596
732
|
async checkUserExists(id) {
|
|
597
|
-
const
|
|
598
|
-
if (!
|
|
733
|
+
const exists = await this.userRepository.existsById(id);
|
|
734
|
+
if (!exists) {
|
|
599
735
|
Err(this.t("errors.notFound"), 404);
|
|
600
736
|
}
|
|
601
|
-
return
|
|
737
|
+
return true;
|
|
602
738
|
}
|
|
603
739
|
/**
|
|
604
740
|
* Check if user exists by email
|
|
@@ -642,8 +778,7 @@ var UserValidator = class UserValidator2 {
|
|
|
642
778
|
async checkUserIdIsUnique(id) {
|
|
643
779
|
if (!id)
|
|
644
780
|
return;
|
|
645
|
-
|
|
646
|
-
if (existingUser) {
|
|
781
|
+
if (await this.userRepository.existsById(id)) {
|
|
647
782
|
Err(this.t("errors.idExists"), 409);
|
|
648
783
|
}
|
|
649
784
|
}
|
|
@@ -691,13 +826,13 @@ var UserValidator = class UserValidator2 {
|
|
|
691
826
|
}
|
|
692
827
|
}
|
|
693
828
|
};
|
|
694
|
-
|
|
829
|
+
__decorate4([
|
|
695
830
|
I18n("users"),
|
|
696
|
-
|
|
831
|
+
__metadata4("design:type", Object)
|
|
697
832
|
], UserValidator.prototype, "t", void 0);
|
|
698
|
-
UserValidator =
|
|
833
|
+
UserValidator = __decorate4([
|
|
699
834
|
Injectable2(),
|
|
700
|
-
|
|
835
|
+
__metadata4("design:paramtypes", [typeof (_a2 = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _a2 : Object, typeof (_b = typeof EncryptionService !== "undefined" && EncryptionService) === "function" ? _b : Object])
|
|
701
836
|
], UserValidator);
|
|
702
837
|
|
|
703
838
|
// src/roles/RoleService.ts
|
|
@@ -705,15 +840,15 @@ import { Injectable as Injectable4 } from "najm-core";
|
|
|
705
840
|
|
|
706
841
|
// src/roles/RoleRepository.ts
|
|
707
842
|
import { eq as eq3 } from "drizzle-orm";
|
|
708
|
-
import { Repository as Repository2, Inject as
|
|
843
|
+
import { Repository as Repository2, Inject as Inject4 } from "najm-core";
|
|
709
844
|
import { DB as DB2 } from "najm-database";
|
|
710
|
-
var
|
|
845
|
+
var __decorate5 = function(decorators, target, key, desc) {
|
|
711
846
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
712
847
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
713
848
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
714
849
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
715
850
|
};
|
|
716
|
-
var
|
|
851
|
+
var __metadata5 = function(k, v) {
|
|
717
852
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
718
853
|
};
|
|
719
854
|
var RoleRepository = class RoleRepository2 {
|
|
@@ -749,15 +884,15 @@ var RoleRepository = class RoleRepository2 {
|
|
|
749
884
|
return deletedRole;
|
|
750
885
|
}
|
|
751
886
|
};
|
|
752
|
-
|
|
887
|
+
__decorate5([
|
|
753
888
|
DB2(),
|
|
754
|
-
|
|
889
|
+
__metadata5("design:type", Object)
|
|
755
890
|
], RoleRepository.prototype, "db", void 0);
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
891
|
+
__decorate5([
|
|
892
|
+
Inject4(AUTH_SCHEMA),
|
|
893
|
+
__metadata5("design:type", Object)
|
|
759
894
|
], RoleRepository.prototype, "schema", void 0);
|
|
760
|
-
RoleRepository =
|
|
895
|
+
RoleRepository = __decorate5([
|
|
761
896
|
Repository2()
|
|
762
897
|
], RoleRepository);
|
|
763
898
|
|
|
@@ -765,16 +900,16 @@ RoleRepository = __decorate4([
|
|
|
765
900
|
import { Injectable as Injectable3 } from "najm-core";
|
|
766
901
|
import { I18n as I18n2 } from "najm-i18n";
|
|
767
902
|
import { Err as Err2 } from "najm-core";
|
|
768
|
-
var
|
|
903
|
+
var __decorate6 = function(decorators, target, key, desc) {
|
|
769
904
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
770
905
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
771
906
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
772
907
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
773
908
|
};
|
|
774
|
-
var
|
|
909
|
+
var __metadata6 = function(k, v) {
|
|
775
910
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
776
911
|
};
|
|
777
|
-
var
|
|
912
|
+
var _a3;
|
|
778
913
|
var RoleValidator = class RoleValidator2 {
|
|
779
914
|
static {
|
|
780
915
|
__name(this, "RoleValidator");
|
|
@@ -833,26 +968,26 @@ var RoleValidator = class RoleValidator2 {
|
|
|
833
968
|
return !!existingRole;
|
|
834
969
|
}
|
|
835
970
|
};
|
|
836
|
-
|
|
971
|
+
__decorate6([
|
|
837
972
|
I18n2("roles"),
|
|
838
|
-
|
|
973
|
+
__metadata6("design:type", Object)
|
|
839
974
|
], RoleValidator.prototype, "t", void 0);
|
|
840
|
-
RoleValidator =
|
|
975
|
+
RoleValidator = __decorate6([
|
|
841
976
|
Injectable3(),
|
|
842
|
-
|
|
977
|
+
__metadata6("design:paramtypes", [typeof (_a3 = typeof RoleRepository !== "undefined" && RoleRepository) === "function" ? _a3 : Object])
|
|
843
978
|
], RoleValidator);
|
|
844
979
|
|
|
845
980
|
// src/roles/RoleService.ts
|
|
846
|
-
var
|
|
981
|
+
var __decorate7 = function(decorators, target, key, desc) {
|
|
847
982
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
848
983
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
849
984
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
850
985
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
851
986
|
};
|
|
852
|
-
var
|
|
987
|
+
var __metadata7 = function(k, v) {
|
|
853
988
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
854
989
|
};
|
|
855
|
-
var
|
|
990
|
+
var _a4;
|
|
856
991
|
var _b2;
|
|
857
992
|
var RoleService = class RoleService2 {
|
|
858
993
|
static {
|
|
@@ -903,9 +1038,9 @@ var RoleService = class RoleService2 {
|
|
|
903
1038
|
return role?.id;
|
|
904
1039
|
}
|
|
905
1040
|
};
|
|
906
|
-
RoleService =
|
|
1041
|
+
RoleService = __decorate7([
|
|
907
1042
|
Injectable4(),
|
|
908
|
-
|
|
1043
|
+
__metadata7("design:paramtypes", [typeof (_a4 = typeof RoleRepository !== "undefined" && RoleRepository) === "function" ? _a4 : Object, typeof (_b2 = typeof RoleValidator !== "undefined" && RoleValidator) === "function" ? _b2 : Object])
|
|
909
1044
|
], RoleService);
|
|
910
1045
|
|
|
911
1046
|
// src/users/UserService.ts
|
|
@@ -929,7 +1064,7 @@ var parseSchema = /* @__PURE__ */ __name(async (schema, data) => {
|
|
|
929
1064
|
var clean = /* @__PURE__ */ __name((obj) => {
|
|
930
1065
|
const cleaned = {};
|
|
931
1066
|
for (const [key, value] of Object.entries(obj)) {
|
|
932
|
-
if (value !==
|
|
1067
|
+
if (value !== void 0 && value !== "") {
|
|
933
1068
|
cleaned[key] = value;
|
|
934
1069
|
}
|
|
935
1070
|
}
|
|
@@ -1010,13 +1145,13 @@ var isFile = /* @__PURE__ */ __name((img) => !!img && typeof img !== "string" &&
|
|
|
1010
1145
|
|
|
1011
1146
|
// src/users/UserService.ts
|
|
1012
1147
|
import { Err as Err4 } from "najm-core";
|
|
1013
|
-
var
|
|
1148
|
+
var __decorate8 = function(decorators, target, key, desc) {
|
|
1014
1149
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1015
1150
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1016
1151
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1017
1152
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1018
1153
|
};
|
|
1019
|
-
var
|
|
1154
|
+
var __metadata8 = function(k, v) {
|
|
1020
1155
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1021
1156
|
};
|
|
1022
1157
|
var __param2 = function(paramIndex, decorator) {
|
|
@@ -1024,7 +1159,7 @@ var __param2 = function(paramIndex, decorator) {
|
|
|
1024
1159
|
decorator(target, key, paramIndex);
|
|
1025
1160
|
};
|
|
1026
1161
|
};
|
|
1027
|
-
var
|
|
1162
|
+
var _a5;
|
|
1028
1163
|
var _b3;
|
|
1029
1164
|
var _c;
|
|
1030
1165
|
var _d;
|
|
@@ -1043,6 +1178,7 @@ var UserService = class UserService2 {
|
|
|
1043
1178
|
encryptionService;
|
|
1044
1179
|
i18nService;
|
|
1045
1180
|
authConfig;
|
|
1181
|
+
t;
|
|
1046
1182
|
constructor(roleValidator, roleService, userRepository, userValidator, encryptionService, i18nService, authConfig) {
|
|
1047
1183
|
this.roleValidator = roleValidator;
|
|
1048
1184
|
this.roleService = roleService;
|
|
@@ -1061,6 +1197,12 @@ var UserService = class UserService2 {
|
|
|
1061
1197
|
sanitizeUsers(users) {
|
|
1062
1198
|
return users.map((user) => this.sanitizeUser(user)).filter(Boolean);
|
|
1063
1199
|
}
|
|
1200
|
+
requireUser(user) {
|
|
1201
|
+
if (!user) {
|
|
1202
|
+
Err4(this.t("errors.notFound"), 404);
|
|
1203
|
+
}
|
|
1204
|
+
return user;
|
|
1205
|
+
}
|
|
1064
1206
|
async resolveUserRole(roleId, roleName) {
|
|
1065
1207
|
if (roleId) {
|
|
1066
1208
|
await this.roleValidator.checkRoleExists(roleId);
|
|
@@ -1082,14 +1224,15 @@ var UserService = class UserService2 {
|
|
|
1082
1224
|
}
|
|
1083
1225
|
return null;
|
|
1084
1226
|
}
|
|
1085
|
-
async getAll() {
|
|
1086
|
-
const
|
|
1227
|
+
async getAll(options = {}) {
|
|
1228
|
+
const limit = Math.min(Math.max(options.limit ?? 50, 1), 100);
|
|
1229
|
+
const offset = Math.max(options.offset ?? 0, 0);
|
|
1230
|
+
const users = await this.userRepository.getAll(limit, offset);
|
|
1087
1231
|
return this.sanitizeUsers(users);
|
|
1088
1232
|
}
|
|
1089
1233
|
async getById(id) {
|
|
1090
|
-
await this.userValidator.checkUserExists(id);
|
|
1091
1234
|
const user = await this.userRepository.getById(id);
|
|
1092
|
-
return this.sanitizeUser(user);
|
|
1235
|
+
return this.sanitizeUser(this.requireUser(user));
|
|
1093
1236
|
}
|
|
1094
1237
|
async getByEmail(email2) {
|
|
1095
1238
|
const user = await this.userValidator.checkUserExistsByEmail(email2);
|
|
@@ -1111,9 +1254,9 @@ var UserService = class UserService2 {
|
|
|
1111
1254
|
Err4("Password is required");
|
|
1112
1255
|
}
|
|
1113
1256
|
this.userValidator.validatePasswordStrength(password);
|
|
1114
|
-
let userId = id || nanoid4(
|
|
1257
|
+
let userId = id || nanoid4(10);
|
|
1115
1258
|
await this.userValidator.checkEmailUnique(data.email);
|
|
1116
|
-
await this.userValidator.checkUserIdIsUnique(
|
|
1259
|
+
await this.userValidator.checkUserIdIsUnique(userId);
|
|
1117
1260
|
const hashedPassword = await this.encryptionService.hashPassword(password);
|
|
1118
1261
|
const resolvedRoleId = await this.resolveUserRole(roleId, role);
|
|
1119
1262
|
const userDetails = {
|
|
@@ -1131,7 +1274,6 @@ var UserService = class UserService2 {
|
|
|
1131
1274
|
}
|
|
1132
1275
|
async update(id, data) {
|
|
1133
1276
|
const { password, image } = data;
|
|
1134
|
-
await this.userValidator.checkUserExists(id);
|
|
1135
1277
|
await this.userValidator.checkEmailUnique(data.email, id);
|
|
1136
1278
|
let hashedPassword;
|
|
1137
1279
|
if (password) {
|
|
@@ -1145,12 +1287,11 @@ var UserService = class UserService2 {
|
|
|
1145
1287
|
};
|
|
1146
1288
|
const cleanedUpdateData = clean(updateData);
|
|
1147
1289
|
const updatedUser = await this.userRepository.update(id, cleanedUpdateData);
|
|
1148
|
-
return this.sanitizeUser(updatedUser);
|
|
1290
|
+
return this.sanitizeUser(this.requireUser(updatedUser));
|
|
1149
1291
|
}
|
|
1150
1292
|
async delete(id) {
|
|
1151
|
-
await this.userValidator.checkUserExists(id);
|
|
1152
1293
|
const user = await this.userRepository.delete(id);
|
|
1153
|
-
return this.sanitizeUser(user);
|
|
1294
|
+
return this.sanitizeUser(this.requireUser(user));
|
|
1154
1295
|
}
|
|
1155
1296
|
async deleteAll() {
|
|
1156
1297
|
const deletedUsers = await this.userRepository.deleteAll();
|
|
@@ -1160,12 +1301,7 @@ var UserService = class UserService2 {
|
|
|
1160
1301
|
await this.userValidator.checkUserExists(id);
|
|
1161
1302
|
return await this.userRepository.getRoleNameById(id);
|
|
1162
1303
|
}
|
|
1163
|
-
async getPassword(email2) {
|
|
1164
|
-
await this.userValidator.checkUserExistsByEmail(email2);
|
|
1165
|
-
return await this.userRepository.getUserPassword(email2);
|
|
1166
|
-
}
|
|
1167
1304
|
async updateLastLogin(id) {
|
|
1168
|
-
await this.userValidator.checkUserExists(id);
|
|
1169
1305
|
await this.userRepository.updateLastLogin(id);
|
|
1170
1306
|
}
|
|
1171
1307
|
async incrementFailedAttempts(id) {
|
|
@@ -1179,15 +1315,13 @@ var UserService = class UserService2 {
|
|
|
1179
1315
|
await this.userRepository.setLockout(id, until);
|
|
1180
1316
|
}
|
|
1181
1317
|
async assignRole(id, roleId, roleName) {
|
|
1182
|
-
await this.userValidator.checkUserExists(id);
|
|
1183
1318
|
const resolvedRoleId = await this.resolveUserRole(roleId, roleName);
|
|
1184
1319
|
const updatedUser = await this.userRepository.update(id, { roleId: resolvedRoleId });
|
|
1185
|
-
return this.sanitizeUser(updatedUser);
|
|
1320
|
+
return this.sanitizeUser(this.requireUser(updatedUser));
|
|
1186
1321
|
}
|
|
1187
1322
|
async removeRole(id) {
|
|
1188
|
-
await this.userValidator.checkUserExists(id);
|
|
1189
1323
|
const updatedUser = await this.userRepository.update(id, { roleId: null });
|
|
1190
|
-
return this.sanitizeUser(updatedUser);
|
|
1324
|
+
return this.sanitizeUser(this.requireUser(updatedUser));
|
|
1191
1325
|
}
|
|
1192
1326
|
async seedAdminUser(config) {
|
|
1193
1327
|
if (!config?.email || !config?.password) {
|
|
@@ -1222,133 +1356,32 @@ var UserService = class UserService2 {
|
|
|
1222
1356
|
return this.i18nService.getCurrentLanguage();
|
|
1223
1357
|
}
|
|
1224
1358
|
};
|
|
1225
|
-
|
|
1359
|
+
__decorate8([
|
|
1360
|
+
I18n3("users"),
|
|
1361
|
+
__metadata8("design:type", Object)
|
|
1362
|
+
], UserService.prototype, "t", void 0);
|
|
1363
|
+
__decorate8([
|
|
1226
1364
|
Transaction(),
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1365
|
+
__metadata8("design:type", Function),
|
|
1366
|
+
__metadata8("design:paramtypes", [typeof (_g = typeof Record !== "undefined" && Record) === "function" ? _g : Object]),
|
|
1367
|
+
__metadata8("design:returntype", typeof (_h = typeof Promise !== "undefined" && Promise) === "function" ? _h : Object)
|
|
1230
1368
|
], UserService.prototype, "create", null);
|
|
1231
|
-
UserService =
|
|
1369
|
+
UserService = __decorate8([
|
|
1232
1370
|
Injectable5(),
|
|
1233
|
-
__param2(6,
|
|
1234
|
-
|
|
1371
|
+
__param2(6, Inject5(AUTH_CONFIG)),
|
|
1372
|
+
__metadata8("design:paramtypes", [typeof (_a5 = typeof RoleValidator !== "undefined" && RoleValidator) === "function" ? _a5 : Object, typeof (_b3 = typeof RoleService !== "undefined" && RoleService) === "function" ? _b3 : Object, typeof (_c = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _c : Object, typeof (_d = typeof UserValidator !== "undefined" && UserValidator) === "function" ? _d : Object, typeof (_e = typeof EncryptionService !== "undefined" && EncryptionService) === "function" ? _e : Object, typeof (_f = typeof I18nService !== "undefined" && I18nService) === "function" ? _f : Object, Object])
|
|
1235
1373
|
], UserService);
|
|
1236
1374
|
|
|
1237
|
-
// src/auth/CookieManager.ts
|
|
1238
|
-
import { Service, Inject as Inject5 } from "najm-core";
|
|
1239
|
-
import { CookieService } from "najm-cookies";
|
|
1240
|
-
import timestring from "timestring";
|
|
1241
|
-
var __decorate8 = function(decorators, target, key, desc) {
|
|
1242
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1243
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1244
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1245
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1246
|
-
};
|
|
1247
|
-
var __metadata8 = function(k, v) {
|
|
1248
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1249
|
-
};
|
|
1250
|
-
var _a5;
|
|
1251
|
-
var CookieManager = class CookieManager2 {
|
|
1252
|
-
static {
|
|
1253
|
-
__name(this, "CookieManager");
|
|
1254
|
-
}
|
|
1255
|
-
config;
|
|
1256
|
-
cookieService;
|
|
1257
|
-
get cookieName() {
|
|
1258
|
-
return this.config.refreshCookieName || "refreshToken";
|
|
1259
|
-
}
|
|
1260
|
-
get sessionCookieName() {
|
|
1261
|
-
return this.config.session.name;
|
|
1262
|
-
}
|
|
1263
|
-
get sessionMaxAge() {
|
|
1264
|
-
return this.config.session.maxAge;
|
|
1265
|
-
}
|
|
1266
|
-
get sessionSecret() {
|
|
1267
|
-
return this.config.session.secret || this.config.jwt.accessSecret;
|
|
1268
|
-
}
|
|
1269
|
-
// =========================================================================
|
|
1270
|
-
// Refresh Token Cookie
|
|
1271
|
-
// =========================================================================
|
|
1272
|
-
setRefreshToken(refreshToken) {
|
|
1273
|
-
const maxAge = timestring(this.config.jwt.refreshExpiresIn, "s");
|
|
1274
|
-
this.cookieService.set(this.cookieName, refreshToken, { maxAge });
|
|
1275
|
-
}
|
|
1276
|
-
clearRefreshToken() {
|
|
1277
|
-
this.cookieService.delete(this.cookieName);
|
|
1278
|
-
}
|
|
1279
|
-
getRefreshToken() {
|
|
1280
|
-
return this.cookieService.get(this.cookieName);
|
|
1281
|
-
}
|
|
1282
|
-
hasRefreshToken() {
|
|
1283
|
-
return this.cookieService.has(this.cookieName);
|
|
1284
|
-
}
|
|
1285
|
-
getCookieName() {
|
|
1286
|
-
return this.cookieName;
|
|
1287
|
-
}
|
|
1288
|
-
// =========================================================================
|
|
1289
|
-
// Session Cookie Cache (HMAC-signed, short-lived)
|
|
1290
|
-
// =========================================================================
|
|
1291
|
-
/**
|
|
1292
|
-
* Write a signed session cookie containing user data, roles, and permissions.
|
|
1293
|
-
* The cookie is HMAC-signed with the access secret so it is tamper-proof
|
|
1294
|
-
* but readable without a database query. Short TTL (5 min) ensures freshness.
|
|
1295
|
-
*/
|
|
1296
|
-
setSessionCookie(data) {
|
|
1297
|
-
const payload = { ...data, iat: Date.now() };
|
|
1298
|
-
this.cookieService.setSigned(this.sessionCookieName, JSON.stringify(payload), this.sessionSecret, {
|
|
1299
|
-
maxAge: this.sessionMaxAge,
|
|
1300
|
-
httpOnly: true,
|
|
1301
|
-
sameSite: "Lax",
|
|
1302
|
-
path: "/"
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1305
|
-
/**
|
|
1306
|
-
* Read and verify the session cookie. Returns null if missing, expired,
|
|
1307
|
-
* or signature verification fails.
|
|
1308
|
-
*/
|
|
1309
|
-
getSessionCookie() {
|
|
1310
|
-
const raw = this.cookieService.getSigned(this.sessionCookieName, this.sessionSecret);
|
|
1311
|
-
if (!raw)
|
|
1312
|
-
return null;
|
|
1313
|
-
try {
|
|
1314
|
-
const data = JSON.parse(raw);
|
|
1315
|
-
const age = Date.now() - data.iat;
|
|
1316
|
-
if (age > this.sessionMaxAge * 1e3)
|
|
1317
|
-
return null;
|
|
1318
|
-
return data;
|
|
1319
|
-
} catch {
|
|
1320
|
-
return null;
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
/**
|
|
1324
|
-
* Clear the session cookie (on logout, password change, etc.)
|
|
1325
|
-
*/
|
|
1326
|
-
clearSessionCookie() {
|
|
1327
|
-
this.cookieService.delete(this.sessionCookieName);
|
|
1328
|
-
}
|
|
1329
|
-
};
|
|
1330
|
-
__decorate8([
|
|
1331
|
-
Inject5(AUTH_CONFIG),
|
|
1332
|
-
__metadata8("design:type", Object)
|
|
1333
|
-
], CookieManager.prototype, "config", void 0);
|
|
1334
|
-
__decorate8([
|
|
1335
|
-
Inject5(CookieService),
|
|
1336
|
-
__metadata8("design:type", typeof (_a5 = typeof CookieService !== "undefined" && CookieService) === "function" ? _a5 : Object)
|
|
1337
|
-
], CookieManager.prototype, "cookieService", void 0);
|
|
1338
|
-
CookieManager = __decorate8([
|
|
1339
|
-
Service()
|
|
1340
|
-
], CookieManager);
|
|
1341
|
-
|
|
1342
1375
|
// src/tokens/TokenService.ts
|
|
1343
1376
|
import { Injectable as Injectable6, Inject as Inject7 } from "najm-core";
|
|
1344
|
-
import { I18n as
|
|
1377
|
+
import { I18n as I18n4 } from "najm-i18n";
|
|
1345
1378
|
import { CacheService } from "najm-cache";
|
|
1346
1379
|
import { createHash } from "crypto";
|
|
1347
1380
|
import jwt from "jsonwebtoken";
|
|
1348
1381
|
import { nanoid as nanoid5 } from "nanoid";
|
|
1349
1382
|
|
|
1350
1383
|
// src/tokens/TokenRepository.ts
|
|
1351
|
-
import { eq as eq4 } from "drizzle-orm";
|
|
1384
|
+
import { and, eq as eq4, isNull } from "drizzle-orm";
|
|
1352
1385
|
import { Repository as Repository3, Inject as Inject6 } from "najm-core";
|
|
1353
1386
|
import { DB as DB3 } from "najm-database";
|
|
1354
1387
|
var __decorate9 = function(decorators, target, key, desc) {
|
|
@@ -1372,12 +1405,10 @@ var TokenRepository = class TokenRepository2 {
|
|
|
1372
1405
|
get users() {
|
|
1373
1406
|
return this.schema.users;
|
|
1374
1407
|
}
|
|
1375
|
-
get roles() {
|
|
1376
|
-
return this.schema.roles;
|
|
1377
|
-
}
|
|
1378
1408
|
/** Shared query helper */
|
|
1409
|
+
queryHelper;
|
|
1379
1410
|
get q() {
|
|
1380
|
-
return new AuthQueries(this.db, this.schema);
|
|
1411
|
+
return this.queryHelper ??= new AuthQueries(this.db, this.schema);
|
|
1381
1412
|
}
|
|
1382
1413
|
async storeRefreshToken(tokenData) {
|
|
1383
1414
|
return await this.db.insert(this.tokens).values(tokenData).onConflictDoUpdate({
|
|
@@ -1392,8 +1423,16 @@ var TokenRepository = class TokenRepository2 {
|
|
|
1392
1423
|
}
|
|
1393
1424
|
}).returning();
|
|
1394
1425
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1426
|
+
/**
|
|
1427
|
+
* Claim the previous-token grace slot. Conditional on BOTH the stored
|
|
1428
|
+
* previousHash still matching the presented token AND previousUsedAt being
|
|
1429
|
+
* NULL. Gating on the hash (not just the flag) closes the rotation race: the
|
|
1430
|
+
* winner's rotation rewrites previousHash via storeRefreshToken, so a loser
|
|
1431
|
+
* whose UPDATE lands after that rotation no longer matches and gets zero
|
|
1432
|
+
* rows — exactly one caller ever claims the slot.
|
|
1433
|
+
*/
|
|
1434
|
+
async markPreviousUsed(userId, previousHash) {
|
|
1435
|
+
return await this.db.update(this.tokens).set({ previousUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and(eq4(this.tokens.userId, userId), eq4(this.tokens.previousHash, previousHash), isNull(this.tokens.previousUsedAt))).returning();
|
|
1397
1436
|
}
|
|
1398
1437
|
async getRefreshTokenWithFamily(userId) {
|
|
1399
1438
|
const [token] = await this.db.select().from(this.tokens).where(eq4(this.tokens.userId, userId));
|
|
@@ -1420,13 +1459,7 @@ var TokenRepository = class TokenRepository2 {
|
|
|
1420
1459
|
return this.q.getRoleAndPermissions(userId);
|
|
1421
1460
|
}
|
|
1422
1461
|
async getUser(userId) {
|
|
1423
|
-
|
|
1424
|
-
if (!user)
|
|
1425
|
-
return null;
|
|
1426
|
-
return {
|
|
1427
|
-
...user,
|
|
1428
|
-
permissions: await this.q.getUserPermissions(userId)
|
|
1429
|
-
};
|
|
1462
|
+
return await this.q.getUserWithPermissions(eq4(this.users.id, userId)) ?? null;
|
|
1430
1463
|
}
|
|
1431
1464
|
};
|
|
1432
1465
|
__decorate9([
|
|
@@ -1486,6 +1519,27 @@ var TokenService = class TokenService2 {
|
|
|
1486
1519
|
get sessionVersionPrefix() {
|
|
1487
1520
|
return "auth:session-version:";
|
|
1488
1521
|
}
|
|
1522
|
+
sessionVersionKey(userId) {
|
|
1523
|
+
return `${this.sessionVersionPrefix}${userId}`;
|
|
1524
|
+
}
|
|
1525
|
+
accessTokenTtlMs() {
|
|
1526
|
+
return timestring2(this.config.jwt.accessExpiresIn, "ms");
|
|
1527
|
+
}
|
|
1528
|
+
expiresAt(expiresIn) {
|
|
1529
|
+
return Math.floor((Date.now() + timestring2(expiresIn, "ms")) / 1e3);
|
|
1530
|
+
}
|
|
1531
|
+
async getCacheValues(keys) {
|
|
1532
|
+
const cache2 = this.cache;
|
|
1533
|
+
if (cache2.getMany)
|
|
1534
|
+
return cache2.getMany(keys);
|
|
1535
|
+
return Promise.all(keys.map((key) => cache2.get(key)));
|
|
1536
|
+
}
|
|
1537
|
+
parseSessionVersion(raw) {
|
|
1538
|
+
if (!raw)
|
|
1539
|
+
return 0;
|
|
1540
|
+
const parsed = Number(raw);
|
|
1541
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
1542
|
+
}
|
|
1489
1543
|
// ============ TOKEN VALIDATION ============
|
|
1490
1544
|
extractAccessToken(authorization) {
|
|
1491
1545
|
if (authorization?.startsWith("Bearer ")) {
|
|
@@ -1504,13 +1558,20 @@ var TokenService = class TokenService2 {
|
|
|
1504
1558
|
} catch {
|
|
1505
1559
|
Err5(this.t("errors.tokenVerificationFailed"));
|
|
1506
1560
|
}
|
|
1561
|
+
const sessionKey = this.sessionVersionKey(payload.userId);
|
|
1562
|
+
let activeSessionVersion;
|
|
1507
1563
|
if (payload.jti) {
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1564
|
+
const [blacklisted, sessionVersion] = await this.getCacheValues([
|
|
1565
|
+
`${this.blacklistPrefix}${payload.jti}`,
|
|
1566
|
+
sessionKey
|
|
1567
|
+
]);
|
|
1568
|
+
if (blacklisted !== null) {
|
|
1510
1569
|
Err5(this.t("errors.tokenRevoked"));
|
|
1511
1570
|
}
|
|
1571
|
+
activeSessionVersion = this.parseSessionVersion(sessionVersion);
|
|
1572
|
+
} else {
|
|
1573
|
+
activeSessionVersion = this.parseSessionVersion(await this.cache.get(sessionKey));
|
|
1512
1574
|
}
|
|
1513
|
-
const activeSessionVersion = await this.getUserSessionVersion(payload.userId);
|
|
1514
1575
|
const tokenSessionVersion = payload.sessionVersion ?? 0;
|
|
1515
1576
|
if (tokenSessionVersion !== activeSessionVersion) {
|
|
1516
1577
|
Err5(this.t("errors.tokenRevoked"));
|
|
@@ -1529,38 +1590,16 @@ var TokenService = class TokenService2 {
|
|
|
1529
1590
|
}
|
|
1530
1591
|
}
|
|
1531
1592
|
static PREVIOUS_GRACE_SECONDS = 120;
|
|
1532
|
-
/**
|
|
1533
|
-
* Validate a refresh token as an active session:
|
|
1534
|
-
* 1. Verify JWT signature and type claim
|
|
1535
|
-
* 2. Compare hash against current stored token
|
|
1536
|
-
* 3. If mismatch, check previous hash within short grace window
|
|
1537
|
-
* 4. Reject anything older or outside the grace window
|
|
1538
|
-
*/
|
|
1539
|
-
async validateRefreshSession(refreshToken) {
|
|
1540
|
-
const userId = this.verifyRefreshToken(refreshToken);
|
|
1541
|
-
const stored = await this.tokenRepository.getRefreshTokenWithFamily(userId);
|
|
1542
|
-
if (!stored) {
|
|
1543
|
-
Err5(this.t("errors.refreshTokenInvalid"));
|
|
1544
|
-
}
|
|
1545
|
-
const presentedHash = this.hashToken(refreshToken);
|
|
1546
|
-
if (presentedHash === stored.token) {
|
|
1547
|
-
return { userId };
|
|
1548
|
-
}
|
|
1549
|
-
const canRecover = stored.previousHash && presentedHash === stored.previousHash && stored.previousValidUntil && new Date(stored.previousValidUntil).getTime() > Date.now() && !stored.previousUsedAt;
|
|
1550
|
-
if (canRecover) {
|
|
1551
|
-
await this.tokenRepository.markPreviousUsed(userId);
|
|
1552
|
-
const tokens = await this.generateTokens(userId, stored.tokenFamily ?? void 0);
|
|
1553
|
-
return {
|
|
1554
|
-
userId,
|
|
1555
|
-
rotatedTokens: { refreshToken: tokens.refreshToken, tokenFamily: stored.tokenFamily }
|
|
1556
|
-
};
|
|
1557
|
-
}
|
|
1558
|
-
Err5(this.t("errors.refreshTokenInvalid"));
|
|
1559
|
-
}
|
|
1560
1593
|
/**
|
|
1561
1594
|
* Read the refresh cookie and return the userId it belongs to.
|
|
1562
1595
|
* Validates against current/previous token state with bounded recovery.
|
|
1563
1596
|
* Throws if the cookie is missing, invalid, or outside the grace window.
|
|
1597
|
+
*
|
|
1598
|
+
* Intentionally side-effect-free: unlike refreshTokens(), a mismatch here
|
|
1599
|
+
* does NOT revoke the suspect family. This is a read path (e.g. GET
|
|
1600
|
+
* /auth/me) — a stray stale cookie on a read must not be able to destroy
|
|
1601
|
+
* the active session. Reuse detection and revocation belong to the
|
|
1602
|
+
* rotation path only.
|
|
1564
1603
|
*/
|
|
1565
1604
|
async resolveUserFromCookie() {
|
|
1566
1605
|
const refreshToken = this.cookieManager.getRefreshToken();
|
|
@@ -1588,14 +1627,19 @@ var TokenService = class TokenService2 {
|
|
|
1588
1627
|
return null;
|
|
1589
1628
|
const token = this.extractAccessToken(auth2);
|
|
1590
1629
|
const { userId } = await this.verifyAccessToken(token);
|
|
1630
|
+
return this.getUserById(userId);
|
|
1631
|
+
}
|
|
1632
|
+
async getUserById(userId) {
|
|
1591
1633
|
const cacheKey = `auth:user:${userId}`;
|
|
1634
|
+
if (typeof this.cache.getOrSet === "function") {
|
|
1635
|
+
return this.cache.getOrSet(cacheKey, () => this.tokenRepository.getUser(userId), 3e4);
|
|
1636
|
+
}
|
|
1592
1637
|
const cached = await this.cache.get(cacheKey);
|
|
1593
1638
|
if (cached)
|
|
1594
1639
|
return JSON.parse(cached);
|
|
1595
1640
|
const user = await this.tokenRepository.getUser(userId);
|
|
1596
|
-
if (user)
|
|
1641
|
+
if (user)
|
|
1597
1642
|
await this.cache.set(cacheKey, JSON.stringify(user), 3e4);
|
|
1598
|
-
}
|
|
1599
1643
|
return user;
|
|
1600
1644
|
}
|
|
1601
1645
|
// ============ TOKEN GENERATION ============
|
|
@@ -1605,25 +1649,37 @@ var TokenService = class TokenService2 {
|
|
|
1605
1649
|
getTokenExpire(token) {
|
|
1606
1650
|
return jwt.decode(token)?.exp;
|
|
1607
1651
|
}
|
|
1652
|
+
decodeAccessToken(token) {
|
|
1653
|
+
return jwt.decode(token);
|
|
1654
|
+
}
|
|
1655
|
+
async signAccessToken(data) {
|
|
1656
|
+
const jti = nanoid5(16);
|
|
1657
|
+
const sessionVersion = await this.getUserSessionVersion(data.userId);
|
|
1658
|
+
const expiresAt = this.expiresAt(this.config.jwt.accessExpiresIn);
|
|
1659
|
+
if (sessionVersion > 0) {
|
|
1660
|
+
await this.cache.set(this.sessionVersionKey(data.userId), String(sessionVersion), this.accessTokenTtlMs());
|
|
1661
|
+
}
|
|
1662
|
+
const token = jwt.sign({ ...data, jti, sessionVersion, exp: expiresAt }, this.config.jwt.accessSecret);
|
|
1663
|
+
return { token, expiresAt };
|
|
1664
|
+
}
|
|
1608
1665
|
/**
|
|
1609
1666
|
* Generate access token with unique jti for blacklist support.
|
|
1610
1667
|
* Includes roles/permissions for client-side RBAC/PBAC.
|
|
1611
1668
|
*/
|
|
1612
1669
|
async generateAccessToken(data) {
|
|
1613
|
-
|
|
1614
|
-
const sessionVersion = await this.getUserSessionVersion(data.userId);
|
|
1615
|
-
return jwt.sign({ ...data, jti, sessionVersion }, this.config.jwt.accessSecret, {
|
|
1616
|
-
expiresIn: this.config.jwt.accessExpiresIn
|
|
1617
|
-
});
|
|
1670
|
+
return (await this.signAccessToken(data)).token;
|
|
1618
1671
|
}
|
|
1619
1672
|
/**
|
|
1620
1673
|
* Generate refresh token with unique jti
|
|
1621
1674
|
*/
|
|
1622
|
-
|
|
1675
|
+
signRefreshToken(data) {
|
|
1623
1676
|
const jti = nanoid5(16);
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
}
|
|
1677
|
+
const expiresAt = this.expiresAt(this.config.jwt.refreshExpiresIn);
|
|
1678
|
+
const token = jwt.sign({ ...data, jti, type: "refresh", exp: expiresAt }, this.config.jwt.refreshSecret);
|
|
1679
|
+
return { token, expiresAt };
|
|
1680
|
+
}
|
|
1681
|
+
generateRefreshToken(data) {
|
|
1682
|
+
return this.signRefreshToken(data).token;
|
|
1627
1683
|
}
|
|
1628
1684
|
async generateTokens(userId, tokenFamily) {
|
|
1629
1685
|
const family = tokenFamily ?? nanoid5(16);
|
|
@@ -1633,14 +1689,17 @@ var TokenService = class TokenService2 {
|
|
|
1633
1689
|
roles: roleName ? [roleName] : [],
|
|
1634
1690
|
permissions: permissions ?? []
|
|
1635
1691
|
};
|
|
1636
|
-
const
|
|
1637
|
-
const
|
|
1638
|
-
await this.storeRefreshToken(userId,
|
|
1692
|
+
const access = await this.signAccessToken(accessTokenData);
|
|
1693
|
+
const refresh = this.signRefreshToken({ userId });
|
|
1694
|
+
await this.storeRefreshToken(userId, refresh.token, family);
|
|
1639
1695
|
return {
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1696
|
+
userId,
|
|
1697
|
+
roles: accessTokenData.roles,
|
|
1698
|
+
permissions: accessTokenData.permissions,
|
|
1699
|
+
accessToken: access.token,
|
|
1700
|
+
refreshToken: refresh.token,
|
|
1701
|
+
accessTokenExpiresAt: access.expiresAt,
|
|
1702
|
+
refreshTokenExpiresAt: refresh.expiresAt
|
|
1644
1703
|
};
|
|
1645
1704
|
}
|
|
1646
1705
|
// ============ TOKEN BLACKLIST (Cache) ============
|
|
@@ -1666,7 +1725,7 @@ var TokenService = class TokenService2 {
|
|
|
1666
1725
|
async blacklistCurrentToken(token) {
|
|
1667
1726
|
try {
|
|
1668
1727
|
const decoded = jwt.decode(token);
|
|
1669
|
-
if (!decoded
|
|
1728
|
+
if (!decoded?.jti) {
|
|
1670
1729
|
return;
|
|
1671
1730
|
}
|
|
1672
1731
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -1717,9 +1776,13 @@ var TokenService = class TokenService2 {
|
|
|
1717
1776
|
}
|
|
1718
1777
|
const canRecover = stored.previousHash && presentedHash === stored.previousHash && stored.previousValidUntil && new Date(stored.previousValidUntil).getTime() > Date.now() && !stored.previousUsedAt;
|
|
1719
1778
|
if (canRecover) {
|
|
1720
|
-
await this.tokenRepository.markPreviousUsed(userId);
|
|
1779
|
+
const claimed = await this.tokenRepository.markPreviousUsed(userId, presentedHash);
|
|
1780
|
+
if (!claimed?.length) {
|
|
1781
|
+
Err5(this.t("errors.refreshTokenInvalid"));
|
|
1782
|
+
}
|
|
1721
1783
|
return this.generateTokens(userId, stored.tokenFamily ?? void 0);
|
|
1722
1784
|
}
|
|
1785
|
+
await this.revokeSuspectRefreshFamily(userId, stored.tokenFamily ?? null);
|
|
1723
1786
|
Err5(this.t("errors.refreshTokenInvalid"));
|
|
1724
1787
|
}
|
|
1725
1788
|
async revokeToken(userId) {
|
|
@@ -1727,10 +1790,26 @@ var TokenService = class TokenService2 {
|
|
|
1727
1790
|
}
|
|
1728
1791
|
async invalidateUserAccessTokens(userId) {
|
|
1729
1792
|
const nextVersion = await this.getUserSessionVersion(userId) + 1;
|
|
1730
|
-
await this.cache.set(
|
|
1793
|
+
await this.cache.set(this.sessionVersionKey(userId), String(nextVersion), this.accessTokenTtlMs());
|
|
1731
1794
|
await this.cache.del(`auth:user:${userId}`);
|
|
1732
1795
|
return nextVersion;
|
|
1733
1796
|
}
|
|
1797
|
+
async getUserFromCookie() {
|
|
1798
|
+
const userId = await this.resolveUserFromCookie();
|
|
1799
|
+
const user = await this.getUserById(userId);
|
|
1800
|
+
if (!user) {
|
|
1801
|
+
Err5(this.t("errors.refreshTokenInvalid"));
|
|
1802
|
+
}
|
|
1803
|
+
return user;
|
|
1804
|
+
}
|
|
1805
|
+
async revokeSuspectRefreshFamily(userId, tokenFamily) {
|
|
1806
|
+
await this.invalidateUserAccessTokens(userId);
|
|
1807
|
+
if (tokenFamily) {
|
|
1808
|
+
await this.tokenRepository.revokeByFamily(tokenFamily);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
await this.revokeToken(userId);
|
|
1812
|
+
}
|
|
1734
1813
|
/**
|
|
1735
1814
|
* Logout user - blacklist access token and revoke refresh token
|
|
1736
1815
|
*/
|
|
@@ -1787,11 +1866,7 @@ var TokenService = class TokenService2 {
|
|
|
1787
1866
|
return decoded.userId;
|
|
1788
1867
|
}
|
|
1789
1868
|
async getUserSessionVersion(userId) {
|
|
1790
|
-
|
|
1791
|
-
if (!raw)
|
|
1792
|
-
return 0;
|
|
1793
|
-
const parsed = Number(raw);
|
|
1794
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
1869
|
+
return this.parseSessionVersion(await this.cache.get(this.sessionVersionKey(userId)));
|
|
1795
1870
|
}
|
|
1796
1871
|
};
|
|
1797
1872
|
__decorate10([
|
|
@@ -1799,7 +1874,7 @@ __decorate10([
|
|
|
1799
1874
|
__metadata10("design:type", Object)
|
|
1800
1875
|
], TokenService.prototype, "config", void 0);
|
|
1801
1876
|
__decorate10([
|
|
1802
|
-
|
|
1877
|
+
I18n4("auth"),
|
|
1803
1878
|
__metadata10("design:type", Object)
|
|
1804
1879
|
], TokenService.prototype, "t", void 0);
|
|
1805
1880
|
TokenService = TokenService_1 = __decorate10([
|
|
@@ -1809,7 +1884,6 @@ TokenService = TokenService_1 = __decorate10([
|
|
|
1809
1884
|
|
|
1810
1885
|
// src/auth/AuthService.ts
|
|
1811
1886
|
import timestring3 from "timestring";
|
|
1812
|
-
import jwt2 from "jsonwebtoken";
|
|
1813
1887
|
var __decorate11 = function(decorators, target, key, desc) {
|
|
1814
1888
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1815
1889
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1819,34 +1893,33 @@ var __decorate11 = function(decorators, target, key, desc) {
|
|
|
1819
1893
|
var __metadata11 = function(k, v) {
|
|
1820
1894
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1821
1895
|
};
|
|
1822
|
-
var AuthService_1;
|
|
1823
1896
|
var _a7;
|
|
1824
1897
|
var _b5;
|
|
1825
1898
|
var _c3;
|
|
1826
1899
|
var _d2;
|
|
1827
1900
|
var _e2;
|
|
1828
1901
|
var _f2;
|
|
1902
|
+
var _g2;
|
|
1829
1903
|
var AuthService = class AuthService2 {
|
|
1830
1904
|
static {
|
|
1831
1905
|
__name(this, "AuthService");
|
|
1832
1906
|
}
|
|
1833
|
-
static {
|
|
1834
|
-
AuthService_1 = this;
|
|
1835
|
-
}
|
|
1836
1907
|
tokenService;
|
|
1837
1908
|
userService;
|
|
1838
1909
|
userValidator;
|
|
1910
|
+
encryptionService;
|
|
1839
1911
|
cookieManager;
|
|
1840
1912
|
i18nService;
|
|
1841
1913
|
emailService;
|
|
1842
1914
|
config;
|
|
1843
1915
|
t;
|
|
1844
1916
|
logger;
|
|
1845
|
-
|
|
1846
|
-
constructor(tokenService, userService, userValidator, cookieManager, i18nService, emailService) {
|
|
1917
|
+
dummyHash;
|
|
1918
|
+
constructor(tokenService, userService, userValidator, encryptionService, cookieManager, i18nService, emailService) {
|
|
1847
1919
|
this.tokenService = tokenService;
|
|
1848
1920
|
this.userService = userService;
|
|
1849
1921
|
this.userValidator = userValidator;
|
|
1922
|
+
this.encryptionService = encryptionService;
|
|
1850
1923
|
this.cookieManager = cookieManager;
|
|
1851
1924
|
this.i18nService = i18nService;
|
|
1852
1925
|
this.emailService = emailService;
|
|
@@ -1860,20 +1933,12 @@ var AuthService = class AuthService2 {
|
|
|
1860
1933
|
const durationMs = timestring3(this.config.lockout.duration, "ms");
|
|
1861
1934
|
return new Date(Date.now() + durationMs).toISOString();
|
|
1862
1935
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
const decoded = jwt2.decode(accessToken);
|
|
1870
|
-
return {
|
|
1871
|
-
roles: decoded?.roles ?? [],
|
|
1872
|
-
permissions: decoded?.permissions ?? []
|
|
1873
|
-
};
|
|
1874
|
-
} catch {
|
|
1875
|
-
return { roles: [], permissions: [] };
|
|
1876
|
-
}
|
|
1936
|
+
getDummyHash() {
|
|
1937
|
+
this.dummyHash ??= this.encryptionService.hashPassword("najm-auth-dummy-password");
|
|
1938
|
+
return this.dummyHash;
|
|
1939
|
+
}
|
|
1940
|
+
async warmupPasswordHash() {
|
|
1941
|
+
await this.getDummyHash();
|
|
1877
1942
|
}
|
|
1878
1943
|
async registerUser(body) {
|
|
1879
1944
|
return await this.userService.create(body);
|
|
@@ -1889,7 +1954,7 @@ var AuthService = class AuthService2 {
|
|
|
1889
1954
|
if (user && this.isLockoutActive(user.lockoutUntil)) {
|
|
1890
1955
|
Err6(this.t("errors.accountLocked"), 423);
|
|
1891
1956
|
}
|
|
1892
|
-
const storedHash = user?.password ??
|
|
1957
|
+
const storedHash = user?.password ?? await this.getDummyHash();
|
|
1893
1958
|
const isValid = await this.userValidator.comparePassword(password, storedHash);
|
|
1894
1959
|
if (!user || !isValid) {
|
|
1895
1960
|
if (user) {
|
|
@@ -1907,34 +1972,34 @@ var AuthService = class AuthService2 {
|
|
|
1907
1972
|
if ((user.failedLoginAttempts ?? 0) > 0 || user.lockoutUntil) {
|
|
1908
1973
|
await this.userService.resetFailedAttempts(user.id);
|
|
1909
1974
|
}
|
|
1910
|
-
const
|
|
1911
|
-
this.cookieManager.setRefreshToken(
|
|
1975
|
+
const generated = await this.tokenService.generateTokens(user.id);
|
|
1976
|
+
this.cookieManager.setRefreshToken(generated.refreshToken);
|
|
1912
1977
|
await this.userService.updateLastLogin(user.id);
|
|
1913
1978
|
const { password: _, failedLoginAttempts: __, lockoutUntil: ___, ...sanitized } = user;
|
|
1914
|
-
const { roles, permissions } =
|
|
1979
|
+
const { roles, permissions } = generated;
|
|
1915
1980
|
this.cookieManager.setSessionCookie({
|
|
1916
|
-
user: { id: sanitized.id, email: sanitized.email, name: sanitized.name, role: sanitized.role },
|
|
1981
|
+
user: { id: sanitized.id, email: sanitized.email, name: sanitized.name, role: sanitized.role, status: sanitized.status ?? void 0 },
|
|
1917
1982
|
roles,
|
|
1918
1983
|
permissions
|
|
1919
1984
|
});
|
|
1920
|
-
|
|
1985
|
+
const { userId: _userId, roles: _roles, permissions: _permissions, ...tokens } = generated;
|
|
1986
|
+
return { ...tokens, user: sanitized };
|
|
1921
1987
|
}
|
|
1922
1988
|
async refreshTokens() {
|
|
1923
|
-
const
|
|
1924
|
-
this.cookieManager.setRefreshToken(
|
|
1925
|
-
const user = await this.tokenService.
|
|
1989
|
+
const generated = await this.tokenService.refreshTokens();
|
|
1990
|
+
this.cookieManager.setRefreshToken(generated.refreshToken);
|
|
1991
|
+
const user = await this.tokenService.getUserById(generated.userId);
|
|
1926
1992
|
if (user) {
|
|
1927
|
-
const { roles, permissions } = this.decodeAccessToken(data.accessToken);
|
|
1928
1993
|
this.cookieManager.setSessionCookie({
|
|
1929
|
-
user: { id: user.id, email: user.email, name: user.name, role: user.role },
|
|
1930
|
-
roles,
|
|
1931
|
-
permissions
|
|
1994
|
+
user: { id: user.id, email: user.email, name: user.name, role: user.role, status: user.status ?? void 0 },
|
|
1995
|
+
roles: generated.roles,
|
|
1996
|
+
permissions: generated.permissions
|
|
1932
1997
|
});
|
|
1933
1998
|
}
|
|
1934
|
-
|
|
1999
|
+
const { userId: _userId, roles: _roles, permissions: _permissions, ...tokens } = generated;
|
|
2000
|
+
return tokens;
|
|
1935
2001
|
}
|
|
1936
2002
|
async logoutUser(userId, authorization) {
|
|
1937
|
-
await this.userValidator.checkUserExists(userId);
|
|
1938
2003
|
await this.tokenService.logout(userId, authorization);
|
|
1939
2004
|
this.cookieManager.clearRefreshToken();
|
|
1940
2005
|
this.cookieManager.clearSessionCookie();
|
|
@@ -1948,8 +2013,7 @@ var AuthService = class AuthService2 {
|
|
|
1948
2013
|
};
|
|
1949
2014
|
}
|
|
1950
2015
|
async getUserFromCookie() {
|
|
1951
|
-
const
|
|
1952
|
-
const user = await this.userService.getById(userId);
|
|
2016
|
+
const user = await this.tokenService.getUserFromCookie();
|
|
1953
2017
|
const lang = this.i18nService.getCurrentLanguage();
|
|
1954
2018
|
return { ...user, language: lang };
|
|
1955
2019
|
}
|
|
@@ -1958,10 +2022,7 @@ var AuthService = class AuthService2 {
|
|
|
1958
2022
|
* fall back to cookie when no Authorization header is present.
|
|
1959
2023
|
*
|
|
1960
2024
|
* Refreshes the session cookie cache only when authoritative roles/permissions
|
|
1961
|
-
* are available
|
|
1962
|
-
* fallback, we cannot reliably populate permissions without a DB round-trip,
|
|
1963
|
-
* so we leave the existing cache alone — it will be refreshed by the next
|
|
1964
|
-
* login/refresh cycle.
|
|
2025
|
+
* are available from the access token.
|
|
1965
2026
|
*/
|
|
1966
2027
|
async getMe(authorization) {
|
|
1967
2028
|
let result;
|
|
@@ -1971,8 +2032,8 @@ var AuthService = class AuthService2 {
|
|
|
1971
2032
|
if (user) {
|
|
1972
2033
|
const lang = this.i18nService.getCurrentLanguage();
|
|
1973
2034
|
result = { ...user, language: lang };
|
|
1974
|
-
const token = authorization.replace(/^Bearer\s+/i, "");
|
|
1975
|
-
cachePayload =
|
|
2035
|
+
const token = this.tokenService.decodeAccessToken(authorization.replace(/^Bearer\s+/i, ""));
|
|
2036
|
+
cachePayload = { roles: token?.roles ?? [], permissions: token?.permissions ?? [] };
|
|
1976
2037
|
} else {
|
|
1977
2038
|
result = await this.getUserFromCookie();
|
|
1978
2039
|
}
|
|
@@ -1981,7 +2042,7 @@ var AuthService = class AuthService2 {
|
|
|
1981
2042
|
}
|
|
1982
2043
|
if (cachePayload) {
|
|
1983
2044
|
this.cookieManager.setSessionCookie({
|
|
1984
|
-
user: { id: result.id, email: result.email, name: result.name, role: result.role },
|
|
2045
|
+
user: { id: result.id, email: result.email, name: result.name, role: result.role, status: result.status ?? void 0 },
|
|
1985
2046
|
roles: cachePayload.roles,
|
|
1986
2047
|
permissions: cachePayload.permissions
|
|
1987
2048
|
});
|
|
@@ -2037,16 +2098,16 @@ __decorate11([
|
|
|
2037
2098
|
__metadata11("design:type", Object)
|
|
2038
2099
|
], AuthService.prototype, "config", void 0);
|
|
2039
2100
|
__decorate11([
|
|
2040
|
-
|
|
2101
|
+
I18n5("auth"),
|
|
2041
2102
|
__metadata11("design:type", Object)
|
|
2042
2103
|
], AuthService.prototype, "t", void 0);
|
|
2043
2104
|
__decorate11([
|
|
2044
2105
|
Log(),
|
|
2045
2106
|
__metadata11("design:type", Object)
|
|
2046
2107
|
], AuthService.prototype, "logger", void 0);
|
|
2047
|
-
AuthService =
|
|
2108
|
+
AuthService = __decorate11([
|
|
2048
2109
|
Injectable7(),
|
|
2049
|
-
__metadata11("design:paramtypes", [typeof (_a7 = typeof TokenService !== "undefined" && TokenService) === "function" ? _a7 : Object, typeof (_b5 = typeof UserService !== "undefined" && UserService) === "function" ? _b5 : Object, typeof (_c3 = typeof UserValidator !== "undefined" && UserValidator) === "function" ? _c3 : Object, typeof (_d2 = typeof
|
|
2110
|
+
__metadata11("design:paramtypes", [typeof (_a7 = typeof TokenService !== "undefined" && TokenService) === "function" ? _a7 : Object, typeof (_b5 = typeof UserService !== "undefined" && UserService) === "function" ? _b5 : Object, typeof (_c3 = typeof UserValidator !== "undefined" && UserValidator) === "function" ? _c3 : Object, typeof (_d2 = typeof EncryptionService !== "undefined" && EncryptionService) === "function" ? _d2 : Object, typeof (_e2 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _e2 : Object, typeof (_f2 = typeof I18nService2 !== "undefined" && I18nService2) === "function" ? _f2 : Object, typeof (_g2 = typeof EmailService !== "undefined" && EmailService) === "function" ? _g2 : Object])
|
|
2050
2111
|
], AuthService);
|
|
2051
2112
|
|
|
2052
2113
|
// src/auth/AuthGuard.ts
|
|
@@ -2088,6 +2149,7 @@ var isAuth = createGuard(AuthGuard);
|
|
|
2088
2149
|
// src/auth/AuthController.ts
|
|
2089
2150
|
import { Validate } from "najm-validation";
|
|
2090
2151
|
import { RateLimit } from "najm-rate";
|
|
2152
|
+
import { createHash as createHash2 } from "crypto";
|
|
2091
2153
|
|
|
2092
2154
|
// src/users/UserDto.ts
|
|
2093
2155
|
import { z } from "zod";
|
|
@@ -2135,6 +2197,10 @@ var assignRoleParams = z.object({
|
|
|
2135
2197
|
userId: z.string().min(1, "User ID is required"),
|
|
2136
2198
|
roleId: z.string().min(1)
|
|
2137
2199
|
});
|
|
2200
|
+
var userListQuery = z.object({
|
|
2201
|
+
limit: z.coerce.number().int().min(1).max(100).default(50),
|
|
2202
|
+
offset: z.coerce.number().int().min(0).default(0)
|
|
2203
|
+
});
|
|
2138
2204
|
|
|
2139
2205
|
// src/auth/AuthController.ts
|
|
2140
2206
|
var __decorate13 = function(decorators, target, key, desc) {
|
|
@@ -2152,25 +2218,22 @@ var __param4 = function(paramIndex, decorator) {
|
|
|
2152
2218
|
};
|
|
2153
2219
|
};
|
|
2154
2220
|
var _a8;
|
|
2155
|
-
var
|
|
2156
|
-
var
|
|
2157
|
-
_configuredCookieName = name;
|
|
2158
|
-
}, "setConfiguredCookieName");
|
|
2159
|
-
var cookieFingerprint = /* @__PURE__ */ __name((cookieName) => (ctx) => {
|
|
2160
|
-
const name = cookieName ?? _configuredCookieName;
|
|
2221
|
+
var hashKeyPart = /* @__PURE__ */ __name((value) => createHash2("sha256").update(value).digest("base64url").slice(0, 32), "hashKeyPart");
|
|
2222
|
+
var cookieFingerprint = /* @__PURE__ */ __name(() => (ctx) => {
|
|
2161
2223
|
const ip = ctx.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? ctx.req.header("x-real-ip") ?? "unknown";
|
|
2162
|
-
const cookie = ctx.req.raw.headers.get("cookie");
|
|
2163
|
-
const
|
|
2164
|
-
const match = cookie?.match(new RegExp(`${escaped}=([^;]+)`));
|
|
2165
|
-
const fingerprint = match?.[1]?.slice(-8) ?? "none";
|
|
2224
|
+
const cookie = ctx.req.raw.headers.get("cookie") ?? "";
|
|
2225
|
+
const fingerprint = cookie ? hashKeyPart(cookie) : "none";
|
|
2166
2226
|
return `${ip}:${fingerprint}`;
|
|
2167
2227
|
}, "cookieFingerprint");
|
|
2168
2228
|
var ipAndEmail = /* @__PURE__ */ __name(async (ctx) => {
|
|
2169
2229
|
const ip = ctx.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? ctx.req.header("x-real-ip") ?? "unknown";
|
|
2170
2230
|
try {
|
|
2171
2231
|
const body = await ctx.req.json();
|
|
2172
|
-
if (body?.email)
|
|
2173
|
-
|
|
2232
|
+
if (body?.email && typeof body.email === "string") {
|
|
2233
|
+
const normalizedEmail = body.email.trim().toLowerCase();
|
|
2234
|
+
if (normalizedEmail)
|
|
2235
|
+
return `${ip}:${hashKeyPart(normalizedEmail)}`;
|
|
2236
|
+
}
|
|
2174
2237
|
} catch {
|
|
2175
2238
|
}
|
|
2176
2239
|
return ip;
|
|
@@ -2291,21 +2354,6 @@ AuthController = __decorate13([
|
|
|
2291
2354
|
__metadata13("design:paramtypes", [typeof (_a8 = typeof AuthService !== "undefined" && AuthService) === "function" ? _a8 : Object])
|
|
2292
2355
|
], AuthController);
|
|
2293
2356
|
|
|
2294
|
-
// src/auth/index.ts
|
|
2295
|
-
var auth_exports = {};
|
|
2296
|
-
__export(auth_exports, {
|
|
2297
|
-
AUTH_MODULE: () => AUTH_MODULE,
|
|
2298
|
-
AuthController: () => AuthController,
|
|
2299
|
-
AuthGuard: () => AuthGuard,
|
|
2300
|
-
AuthResolver: () => AuthResolver,
|
|
2301
|
-
AuthService: () => AuthService,
|
|
2302
|
-
CookieManager: () => CookieManager,
|
|
2303
|
-
EncryptionService: () => EncryptionService,
|
|
2304
|
-
isAuth: () => isAuth,
|
|
2305
|
-
runAsUser: () => runAsUser,
|
|
2306
|
-
setConfiguredCookieName: () => setConfiguredCookieName
|
|
2307
|
-
});
|
|
2308
|
-
|
|
2309
2357
|
// src/auth/AuthResolver.ts
|
|
2310
2358
|
import { APP, Container, DI, Inject as Inject9, LOGGER, Meta, Service as Service3 } from "najm-core";
|
|
2311
2359
|
import { USER, ROLE, PERMISSIONS } from "najm-guard";
|
|
@@ -2345,6 +2393,22 @@ var AuthResolver = class AuthResolver2 {
|
|
|
2345
2393
|
return false;
|
|
2346
2394
|
}
|
|
2347
2395
|
}
|
|
2396
|
+
async resolveFromSessionCookie() {
|
|
2397
|
+
try {
|
|
2398
|
+
const cookieManager = await this.container.resolve(CookieManager);
|
|
2399
|
+
const session = cookieManager.getSessionCookie();
|
|
2400
|
+
if (!session)
|
|
2401
|
+
return false;
|
|
2402
|
+
return {
|
|
2403
|
+
user: { ...session.user, permissions: session.permissions },
|
|
2404
|
+
role: session.user.role ?? session.roles[0],
|
|
2405
|
+
permissions: session.permissions
|
|
2406
|
+
};
|
|
2407
|
+
} catch (error) {
|
|
2408
|
+
this.log.warn("Session cookie verification failed", error);
|
|
2409
|
+
return false;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2348
2412
|
/**
|
|
2349
2413
|
* Resolve the current user from the refresh cookie (cookie-only flow,
|
|
2350
2414
|
* e.g. Next.js Server Components calling /auth/me with just the cookie).
|
|
@@ -2356,11 +2420,7 @@ var AuthResolver = class AuthResolver2 {
|
|
|
2356
2420
|
async resolveFromCookie() {
|
|
2357
2421
|
try {
|
|
2358
2422
|
const tokenService = await this.container.resolve(TokenService);
|
|
2359
|
-
const
|
|
2360
|
-
if (!userId)
|
|
2361
|
-
return false;
|
|
2362
|
-
const userService = await this.container.resolve(UserService);
|
|
2363
|
-
const user = await userService.getById(userId);
|
|
2423
|
+
const user = await tokenService.getUserFromCookie();
|
|
2364
2424
|
if (!user)
|
|
2365
2425
|
return false;
|
|
2366
2426
|
return {
|
|
@@ -2377,7 +2437,7 @@ var AuthResolver = class AuthResolver2 {
|
|
|
2377
2437
|
this.app.use("*", async (c, next) => {
|
|
2378
2438
|
const raw = c.req.header("authorization") ?? "";
|
|
2379
2439
|
const token = raw.replace(/^Bearer\s+/i, "").trim();
|
|
2380
|
-
const result = token ? await this.resolve(token) || await this.resolveFromCookie() : await this.resolveFromCookie();
|
|
2440
|
+
const result = token ? await this.resolve(token) || await this.resolveFromCookie() : await this.resolveFromSessionCookie() || await this.resolveFromCookie();
|
|
2381
2441
|
if (result) {
|
|
2382
2442
|
if (this.container.isActive()) {
|
|
2383
2443
|
this.container.set(USER, result.user);
|
|
@@ -2386,28 +2446,23 @@ var AuthResolver = class AuthResolver2 {
|
|
|
2386
2446
|
if (result.permissions !== void 0)
|
|
2387
2447
|
this.container.set(PERMISSIONS, result.permissions);
|
|
2388
2448
|
} else {
|
|
2389
|
-
const
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
nextStore.set(PERMISSIONS.key, result.permissions);
|
|
2398
|
-
als.store = nextStore;
|
|
2399
|
-
try {
|
|
2400
|
-
await next();
|
|
2401
|
-
return;
|
|
2402
|
-
} finally {
|
|
2403
|
-
als.store = previousStore;
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2449
|
+
const store = {
|
|
2450
|
+
[USER.key]: result.user
|
|
2451
|
+
};
|
|
2452
|
+
if (result.role !== void 0)
|
|
2453
|
+
store[ROLE.key] = result.role;
|
|
2454
|
+
if (result.permissions !== void 0)
|
|
2455
|
+
store[PERMISSIONS.key] = result.permissions;
|
|
2456
|
+
return await this.container.run(store, next);
|
|
2406
2457
|
}
|
|
2407
2458
|
}
|
|
2408
2459
|
await next();
|
|
2409
2460
|
});
|
|
2410
2461
|
}
|
|
2462
|
+
async onReady() {
|
|
2463
|
+
const authService = await this.container.resolve(AuthService);
|
|
2464
|
+
await authService.warmupPasswordHash();
|
|
2465
|
+
}
|
|
2411
2466
|
};
|
|
2412
2467
|
__decorate14([
|
|
2413
2468
|
DI(),
|
|
@@ -2476,13 +2531,14 @@ __export(users_exports, {
|
|
|
2476
2531
|
resetPasswordDto: () => resetPasswordDto,
|
|
2477
2532
|
updateUserDto: () => updateUserDto,
|
|
2478
2533
|
userIdInParam: () => userIdInParam,
|
|
2479
|
-
userIdParam: () => userIdParam
|
|
2534
|
+
userIdParam: () => userIdParam,
|
|
2535
|
+
userListQuery: () => userListQuery
|
|
2480
2536
|
});
|
|
2481
2537
|
|
|
2482
2538
|
// src/users/UserController.ts
|
|
2483
2539
|
import { Controller as Controller3 } from "najm-core";
|
|
2484
2540
|
import { Get as Get3, Post as Post3, Put as Put2, Delete as Delete2, ResMsg as ResMsg3 } from "najm-core";
|
|
2485
|
-
import { Params as Params2, Body as Body3 } from "najm-core";
|
|
2541
|
+
import { Params as Params2, Body as Body3, Query } from "najm-core";
|
|
2486
2542
|
|
|
2487
2543
|
// src/roles/index.ts
|
|
2488
2544
|
var roles_exports = {};
|
|
@@ -2734,8 +2790,8 @@ var UserController = class UserController2 {
|
|
|
2734
2790
|
constructor(userService) {
|
|
2735
2791
|
this.userService = userService;
|
|
2736
2792
|
}
|
|
2737
|
-
async getUsers() {
|
|
2738
|
-
return this.userService.getAll();
|
|
2793
|
+
async getUsers(query) {
|
|
2794
|
+
return this.userService.getAll(userListQuery.parse(query ?? {}));
|
|
2739
2795
|
}
|
|
2740
2796
|
async getLang() {
|
|
2741
2797
|
const language = await this.userService.getLang();
|
|
@@ -2775,9 +2831,11 @@ var UserController = class UserController2 {
|
|
|
2775
2831
|
__decorate17([
|
|
2776
2832
|
Get3(),
|
|
2777
2833
|
isAdmin(),
|
|
2834
|
+
Validate3({ query: userListQuery }),
|
|
2778
2835
|
ResMsg3("users.success.retrieved"),
|
|
2836
|
+
__param7(0, Query()),
|
|
2779
2837
|
__metadata17("design:type", Function),
|
|
2780
|
-
__metadata17("design:paramtypes", []),
|
|
2838
|
+
__metadata17("design:paramtypes", [Object]),
|
|
2781
2839
|
__metadata17("design:returntype", Promise)
|
|
2782
2840
|
], UserController.prototype, "getUsers", null);
|
|
2783
2841
|
__decorate17([
|
|
@@ -2912,7 +2970,7 @@ __export(permissions_exports, {
|
|
|
2912
2970
|
});
|
|
2913
2971
|
|
|
2914
2972
|
// src/permissions/PermissionRepository.ts
|
|
2915
|
-
import { eq as eq5, and } from "drizzle-orm";
|
|
2973
|
+
import { eq as eq5, and as and2 } from "drizzle-orm";
|
|
2916
2974
|
import { Repository as Repository4, Inject as Inject10 } from "najm-core";
|
|
2917
2975
|
import { DB as DB4 } from "najm-database";
|
|
2918
2976
|
var __decorate18 = function(decorators, target, key, desc) {
|
|
@@ -2986,11 +3044,11 @@ var PermissionRepository = class PermissionRepository2 {
|
|
|
2986
3044
|
return newRolePermission;
|
|
2987
3045
|
}
|
|
2988
3046
|
async removePermissionFromRole(roleId, permissionId) {
|
|
2989
|
-
const [deletedRolePermission] = await this.db.delete(this.rolePermissions).where(
|
|
3047
|
+
const [deletedRolePermission] = await this.db.delete(this.rolePermissions).where(and2(eq5(this.rolePermissions.roleId, roleId), eq5(this.rolePermissions.permissionId, permissionId))).returning();
|
|
2990
3048
|
return deletedRolePermission;
|
|
2991
3049
|
}
|
|
2992
3050
|
async checkRoleHasPermission(roleId, permissionId) {
|
|
2993
|
-
const [rolePermission] = await this.db.select().from(this.rolePermissions).where(
|
|
3051
|
+
const [rolePermission] = await this.db.select().from(this.rolePermissions).where(and2(eq5(this.rolePermissions.roleId, roleId), eq5(this.rolePermissions.permissionId, permissionId)));
|
|
2994
3052
|
return !!rolePermission;
|
|
2995
3053
|
}
|
|
2996
3054
|
async deleteAll() {
|
|
@@ -3080,7 +3138,7 @@ import { Injectable as Injectable10 } from "najm-core";
|
|
|
3080
3138
|
|
|
3081
3139
|
// src/permissions/PermissionValidator.ts
|
|
3082
3140
|
import { Injectable as Injectable9 } from "najm-core";
|
|
3083
|
-
import { I18n as
|
|
3141
|
+
import { I18n as I18n6 } from "najm-i18n";
|
|
3084
3142
|
import { Err as Err7 } from "najm-core";
|
|
3085
3143
|
var __decorate20 = function(decorators, target, key, desc) {
|
|
3086
3144
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -3160,7 +3218,7 @@ var PermissionValidator = class PermissionValidator2 {
|
|
|
3160
3218
|
}
|
|
3161
3219
|
};
|
|
3162
3220
|
__decorate20([
|
|
3163
|
-
|
|
3221
|
+
I18n6("permissions"),
|
|
3164
3222
|
__metadata20("design:type", Object)
|
|
3165
3223
|
], PermissionValidator.prototype, "t", void 0);
|
|
3166
3224
|
PermissionValidator = __decorate20([
|
|
@@ -3519,7 +3577,7 @@ var revokeTokenDto = z4.object({
|
|
|
3519
3577
|
});
|
|
3520
3578
|
|
|
3521
3579
|
// src/ownership/scopedOwnership.ts
|
|
3522
|
-
import { aliasedTable, eq as eq6, getTableColumns, sql as
|
|
3580
|
+
import { aliasedTable, eq as eq6, getTableColumns, sql as sql5 } from "drizzle-orm";
|
|
3523
3581
|
var DEFAULT_ADMIN_ROLES = ["admin"];
|
|
3524
3582
|
var DRIZZLE_NAME = /* @__PURE__ */ Symbol.for("drizzle:Name");
|
|
3525
3583
|
var DRIZZLE_BASE_NAME = /* @__PURE__ */ Symbol.for("drizzle:BaseName");
|
|
@@ -3656,7 +3714,7 @@ var OwnershipToken = class {
|
|
|
3656
3714
|
return query;
|
|
3657
3715
|
const rule = this._rules[role];
|
|
3658
3716
|
if (!rule)
|
|
3659
|
-
return query.where(
|
|
3717
|
+
return query.where(sql5`1 = 0`);
|
|
3660
3718
|
const { query: q, condition } = rule(uid, query);
|
|
3661
3719
|
return q.where(condition);
|
|
3662
3720
|
}
|
|
@@ -3671,7 +3729,7 @@ var OwnershipToken = class {
|
|
|
3671
3729
|
return { query, condition: null };
|
|
3672
3730
|
const rule = this._rules[role];
|
|
3673
3731
|
if (!rule)
|
|
3674
|
-
return { query, condition:
|
|
3732
|
+
return { query, condition: sql5`1 = 0` };
|
|
3675
3733
|
return rule(uid, query);
|
|
3676
3734
|
}
|
|
3677
3735
|
};
|
|
@@ -4030,7 +4088,7 @@ __name(Policy, "Policy");
|
|
|
4030
4088
|
|
|
4031
4089
|
// src/ownership/OwnedDecorator.ts
|
|
4032
4090
|
import "reflect-metadata";
|
|
4033
|
-
import { sql as
|
|
4091
|
+
import { sql as sql6, and as and3 } from "drizzle-orm";
|
|
4034
4092
|
import { Injectable as Injectable12, Inject as Inject12, DI as DI2, Container as Container2, REQUEST_ID } from "najm-core";
|
|
4035
4093
|
import { USER as USER2 } from "najm-guard";
|
|
4036
4094
|
var __decorate24 = function(decorators, target, key, desc) {
|
|
@@ -4100,7 +4158,7 @@ function Owned(token) {
|
|
|
4100
4158
|
return null;
|
|
4101
4159
|
if (filtered.length === 1)
|
|
4102
4160
|
return filtered[0];
|
|
4103
|
-
return
|
|
4161
|
+
return and3(...filtered);
|
|
4104
4162
|
}
|
|
4105
4163
|
__name(combineConditions, "combineConditions");
|
|
4106
4164
|
function applyQueryOptions(query, opts = {}, whereCondition) {
|
|
@@ -4119,7 +4177,7 @@ function Owned(token) {
|
|
|
4119
4177
|
return query;
|
|
4120
4178
|
const user = getUser(this);
|
|
4121
4179
|
if (!user)
|
|
4122
|
-
return query.where(
|
|
4180
|
+
return query.where(sql6`1 = 0`);
|
|
4123
4181
|
return token.applyScope(user.id, user.role, query);
|
|
4124
4182
|
};
|
|
4125
4183
|
},
|
|
@@ -4265,6 +4323,10 @@ var DEFAULT_JWT = {
|
|
|
4265
4323
|
refreshExpiresIn: process.env.REFRESH_EXPIRES_IN || "7d"
|
|
4266
4324
|
};
|
|
4267
4325
|
var mergeConfig = /* @__PURE__ */ __name((config) => {
|
|
4326
|
+
const bcryptRounds = config?.bcryptRounds ?? 10;
|
|
4327
|
+
if (!Number.isInteger(bcryptRounds) || bcryptRounds < 4 || bcryptRounds > 31) {
|
|
4328
|
+
throw new Error("auth.bcryptRounds must be an integer between 4 and 31");
|
|
4329
|
+
}
|
|
4268
4330
|
const finalConfig = {
|
|
4269
4331
|
jwt: {
|
|
4270
4332
|
...DEFAULT_JWT,
|
|
@@ -4280,6 +4342,7 @@ var mergeConfig = /* @__PURE__ */ __name((config) => {
|
|
|
4280
4342
|
maxAttempts: config?.lockout?.maxAttempts ?? 5,
|
|
4281
4343
|
duration: config?.lockout?.duration ?? "15m"
|
|
4282
4344
|
},
|
|
4345
|
+
bcryptRounds,
|
|
4283
4346
|
session: {
|
|
4284
4347
|
name: config?.session?.name ?? "najm.session",
|
|
4285
4348
|
maxAge: config?.session?.maxAge ?? 300,
|
|
@@ -4293,7 +4356,6 @@ var mergeConfig = /* @__PURE__ */ __name((config) => {
|
|
|
4293
4356
|
if (!finalConfig.jwt.refreshSecret) {
|
|
4294
4357
|
throw Err8.configRequired("auth", "JWT_REFRESH_SECRET");
|
|
4295
4358
|
}
|
|
4296
|
-
setConfiguredCookieName(finalConfig.refreshCookieName);
|
|
4297
4359
|
return finalConfig;
|
|
4298
4360
|
}, "mergeConfig");
|
|
4299
4361
|
var selectSchema = /* @__PURE__ */ __name((config) => {
|
|
@@ -4313,7 +4375,6 @@ var selectSchema = /* @__PURE__ */ __name((config) => {
|
|
|
4313
4375
|
var auth = /* @__PURE__ */ __name((config) => plugin("auth").version("1.0.0").depends(cache(), cookies(), i18n(), guards(), validation(config?.validation), rateLimit(config?.rateLimit), email()).requires("database").contributes(I18N_CONTRIBUTIONS, AUTH_LOCALES).services(auth_exports, users_exports, roles_exports, permissions_exports, tokens_exports, ScopeContext).config(AUTH_CONFIG, mergeConfig(config)).set(AUTH_SCHEMA, selectSchema(config)).set(AUTH_ENCRYPTION_KEY, config?.encryptionKey ?? null).build(), "auth");
|
|
4314
4376
|
|
|
4315
4377
|
// src/seed.ts
|
|
4316
|
-
import { hash } from "bcryptjs";
|
|
4317
4378
|
var toSeedId = /* @__PURE__ */ __name((prefix, value) => {
|
|
4318
4379
|
const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48);
|
|
4319
4380
|
return `${prefix}_${normalized || "item"}`;
|
|
@@ -4364,7 +4425,7 @@ var authSeed = /* @__PURE__ */ __name((config) => ({
|
|
|
4364
4425
|
const users = [
|
|
4365
4426
|
{
|
|
4366
4427
|
email: config.adminEmail,
|
|
4367
|
-
password: await
|
|
4428
|
+
password: await hashPassword(config.adminPass, config.bcryptRounds ?? 10),
|
|
4368
4429
|
roleId: adminRole.id,
|
|
4369
4430
|
emailVerified: true,
|
|
4370
4431
|
status: "active",
|
|
@@ -4384,7 +4445,7 @@ var authSeed = /* @__PURE__ */ __name((config) => ({
|
|
|
4384
4445
|
}
|
|
4385
4446
|
users.push({
|
|
4386
4447
|
email: user.email,
|
|
4387
|
-
password: await
|
|
4448
|
+
password: await hashPassword(user.password, config.bcryptRounds ?? 10),
|
|
4388
4449
|
roleId: role.id,
|
|
4389
4450
|
emailVerified: user.emailVerified ?? true,
|
|
4390
4451
|
status: user.status ?? "active",
|
|
@@ -4415,6 +4476,7 @@ async function seedAuthData(config) {
|
|
|
4415
4476
|
return await seeder.run(authSeed({
|
|
4416
4477
|
adminEmail: config.adminEmail,
|
|
4417
4478
|
adminPass: config.adminPassword,
|
|
4479
|
+
bcryptRounds: config.bcryptRounds,
|
|
4418
4480
|
roles: config.roles,
|
|
4419
4481
|
permissions: config.permissions,
|
|
4420
4482
|
additionalUsers: config.users
|
|
@@ -4536,7 +4598,6 @@ export {
|
|
|
4536
4598
|
rolesTable,
|
|
4537
4599
|
runAsUser,
|
|
4538
4600
|
seedAuthData,
|
|
4539
|
-
setConfiguredCookieName,
|
|
4540
4601
|
tokenIdParam,
|
|
4541
4602
|
tokenStatusEnum,
|
|
4542
4603
|
tokenTypeEnum,
|
|
@@ -4547,6 +4608,7 @@ export {
|
|
|
4547
4608
|
updateUserDto,
|
|
4548
4609
|
userIdInParam,
|
|
4549
4610
|
userIdParam,
|
|
4611
|
+
userListQuery,
|
|
4550
4612
|
userStatusEnum,
|
|
4551
4613
|
usersTable,
|
|
4552
4614
|
verifyTokenDto,
|