@astralibx/staff-engine 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
- import { z } from 'zod';
2
1
  import { AlxError, sendError, noopLogger, sendSuccess } from '@astralibx/core';
3
2
  export { sendSuccess } from '@astralibx/core';
4
3
  import { PERMISSION_TYPE_VALUES, STAFF_STATUS, STAFF_STATUS_VALUES, STAFF_ROLE_VALUES, PERMISSION_TYPE, STAFF_ROLE, DEFAULT_OPTIONS } from '@astralibx/staff-types';
5
4
  export { DEFAULT_OPTIONS } from '@astralibx/staff-types';
6
5
  import { Schema } from 'mongoose';
6
+ import { z } from 'zod';
7
7
  import jwt2 from 'jsonwebtoken';
8
8
  import { Router } from 'express';
9
9
 
@@ -399,6 +399,50 @@ function validatePermissionPairs(permissions, allGroups) {
399
399
  throw new InvalidPermissionError(missingViewKeys);
400
400
  }
401
401
  }
402
+ var StaffEngineConfigSchema = z.object({
403
+ db: z.object({
404
+ connection: z.unknown().refine((v) => v !== void 0 && v !== null, {
405
+ message: "db.connection is required"
406
+ }),
407
+ collectionPrefix: z.string().optional()
408
+ }),
409
+ redis: z.object({
410
+ connection: z.unknown(),
411
+ keyPrefix: z.string().optional()
412
+ }).optional(),
413
+ logger: z.object({
414
+ info: z.function(),
415
+ warn: z.function(),
416
+ error: z.function()
417
+ }).optional(),
418
+ tenantId: z.string().optional(),
419
+ auth: z.object({
420
+ jwtSecret: z.string().min(1),
421
+ staffTokenExpiry: z.string().optional(),
422
+ ownerTokenExpiry: z.string().optional(),
423
+ permissionCacheTtlMs: z.number().int().positive().optional()
424
+ }),
425
+ adapters: z.object({
426
+ hashPassword: z.function(),
427
+ comparePassword: z.function()
428
+ }),
429
+ hooks: z.object({
430
+ onStaffCreated: z.function().optional(),
431
+ onLogin: z.function().optional(),
432
+ onLoginFailed: z.function().optional(),
433
+ onPermissionsChanged: z.function().optional(),
434
+ onStatusChanged: z.function().optional(),
435
+ onMetric: z.function().optional()
436
+ }).optional(),
437
+ options: z.object({
438
+ requireEmailUniqueness: z.boolean().optional(),
439
+ allowSelfPasswordChange: z.boolean().optional(),
440
+ rateLimiter: z.object({
441
+ windowMs: z.number().int().positive().optional(),
442
+ maxAttempts: z.number().int().positive().optional()
443
+ }).optional()
444
+ }).optional()
445
+ });
402
446
 
403
447
  // src/services/staff.service.ts
404
448
  var StaffService = class {
@@ -407,105 +451,22 @@ var StaffService = class {
407
451
  adapters;
408
452
  hooks;
409
453
  permissionCache;
410
- rateLimiter;
411
454
  logger;
412
455
  tenantId;
413
- jwtSecret;
414
- staffTokenExpiry;
415
- ownerTokenExpiry;
416
456
  requireEmailUniqueness;
417
- allowSelfPasswordChange;
418
457
  constructor(deps) {
419
458
  this.Staff = deps.Staff;
420
459
  this.PermissionGroup = deps.PermissionGroup;
421
460
  this.adapters = deps.adapters;
422
461
  this.hooks = deps.hooks;
423
462
  this.permissionCache = deps.permissionCache;
424
- this.rateLimiter = deps.rateLimiter;
425
463
  this.logger = deps.logger;
426
464
  this.tenantId = deps.tenantId;
427
- this.jwtSecret = deps.jwtSecret;
428
- this.staffTokenExpiry = deps.staffTokenExpiry;
429
- this.ownerTokenExpiry = deps.ownerTokenExpiry;
430
465
  this.requireEmailUniqueness = deps.requireEmailUniqueness;
431
- this.allowSelfPasswordChange = deps.allowSelfPasswordChange;
432
466
  }
433
467
  get tenantFilter() {
434
468
  return this.tenantId ? { tenantId: this.tenantId } : {};
435
469
  }
436
- generateToken(staffId, role) {
437
- const expiresIn = role === STAFF_ROLE.Owner ? this.ownerTokenExpiry : this.staffTokenExpiry;
438
- return jwt2.sign({ staffId, role }, this.jwtSecret, { expiresIn });
439
- }
440
- async setupOwner(data) {
441
- const count = await this.Staff.countDocuments(this.tenantFilter);
442
- if (count > 0) throw new SetupError();
443
- const hashedPassword = await this.adapters.hashPassword(data.password);
444
- let staff;
445
- try {
446
- const doc = await this.Staff.create({
447
- name: data.name,
448
- email: data.email.toLowerCase().trim(),
449
- password: hashedPassword,
450
- role: STAFF_ROLE.Owner,
451
- status: STAFF_STATUS.Active,
452
- permissions: [],
453
- ...this.tenantFilter
454
- });
455
- staff = doc.toObject();
456
- } catch (err) {
457
- if (err && typeof err === "object" && "code" in err && err.code === 11e3) {
458
- throw new SetupError();
459
- }
460
- throw err;
461
- }
462
- const token = this.generateToken(staff._id.toString(), STAFF_ROLE.Owner);
463
- this.logger.info("Owner setup complete", { staffId: staff._id.toString() });
464
- this.hooks.onStaffCreated?.(staff);
465
- this.hooks.onMetric?.({ name: "staff_setup_complete", value: 1 });
466
- return { staff, token };
467
- }
468
- async login(email, password, ip) {
469
- if (ip) {
470
- const limit = await this.rateLimiter.checkLimit(ip);
471
- if (!limit.allowed) {
472
- this.hooks.onLoginFailed?.(email, ip);
473
- throw new RateLimitError(limit.retryAfterMs);
474
- }
475
- }
476
- const staff = await this.Staff.findOne({
477
- email: email.toLowerCase().trim(),
478
- ...this.tenantFilter
479
- }).select("+password");
480
- if (!staff) {
481
- if (ip) await this.rateLimiter.recordAttempt(ip);
482
- this.hooks.onLoginFailed?.(email, ip);
483
- throw new AuthenticationError(ERROR_CODE.InvalidCredentials);
484
- }
485
- const valid = await this.adapters.comparePassword(password, staff.password);
486
- if (!valid) {
487
- if (ip) await this.rateLimiter.recordAttempt(ip);
488
- this.hooks.onLoginFailed?.(email, ip);
489
- throw new AuthenticationError(ERROR_CODE.InvalidCredentials);
490
- }
491
- if (staff.status === STAFF_STATUS.Inactive) {
492
- throw new AuthenticationError(ERROR_CODE.AccountInactive, ERROR_MESSAGE.AccountInactive);
493
- }
494
- if (staff.status === STAFF_STATUS.Pending) {
495
- throw new AuthenticationError(ERROR_CODE.AccountPending, ERROR_MESSAGE.AccountPending);
496
- }
497
- staff.lastLoginAt = /* @__PURE__ */ new Date();
498
- if (ip) staff.lastLoginIp = ip;
499
- await staff.save();
500
- if (ip) await this.rateLimiter.reset(ip);
501
- const token = this.generateToken(staff._id.toString(), staff.role);
502
- this.hooks.onLogin?.(staff.toObject(), ip);
503
- this.hooks.onMetric?.({ name: "staff_login", value: 1, labels: { role: staff.role } });
504
- this.logger.info("Staff login", { staffId: staff._id.toString() });
505
- const staffObj = staff.toObject();
506
- delete staffObj.password;
507
- return { staff: staffObj, token };
508
- }
509
470
  async create(data) {
510
471
  if (this.requireEmailUniqueness) {
511
472
  const existing = await this.Staff.findOne({
@@ -606,6 +567,106 @@ var StaffService = class {
606
567
  this.logger.info("Staff status updated", { staffId, oldStatus, newStatus: status });
607
568
  return staff.toObject();
608
569
  }
570
+ };
571
+ var AuthService = class {
572
+ Staff;
573
+ adapters;
574
+ hooks;
575
+ rateLimiter;
576
+ logger;
577
+ tenantId;
578
+ jwtSecret;
579
+ staffTokenExpiry;
580
+ ownerTokenExpiry;
581
+ allowSelfPasswordChange;
582
+ constructor(deps) {
583
+ this.Staff = deps.Staff;
584
+ this.adapters = deps.adapters;
585
+ this.hooks = deps.hooks;
586
+ this.rateLimiter = deps.rateLimiter;
587
+ this.logger = deps.logger;
588
+ this.tenantId = deps.tenantId;
589
+ this.jwtSecret = deps.jwtSecret;
590
+ this.staffTokenExpiry = deps.staffTokenExpiry;
591
+ this.ownerTokenExpiry = deps.ownerTokenExpiry;
592
+ this.allowSelfPasswordChange = deps.allowSelfPasswordChange;
593
+ }
594
+ get tenantFilter() {
595
+ return this.tenantId ? { tenantId: this.tenantId } : {};
596
+ }
597
+ generateToken(staffId, role) {
598
+ const expiresIn = role === STAFF_ROLE.Owner ? this.ownerTokenExpiry : this.staffTokenExpiry;
599
+ return jwt2.sign({ staffId, role }, this.jwtSecret, { expiresIn });
600
+ }
601
+ async setupOwner(data) {
602
+ const count = await this.Staff.countDocuments(this.tenantFilter);
603
+ if (count > 0) throw new SetupError();
604
+ const hashedPassword = await this.adapters.hashPassword(data.password);
605
+ let staff;
606
+ try {
607
+ const doc = await this.Staff.create({
608
+ name: data.name,
609
+ email: data.email.toLowerCase().trim(),
610
+ password: hashedPassword,
611
+ role: STAFF_ROLE.Owner,
612
+ status: STAFF_STATUS.Active,
613
+ permissions: [],
614
+ ...this.tenantFilter
615
+ });
616
+ staff = doc.toObject();
617
+ } catch (err) {
618
+ if (err && typeof err === "object" && "code" in err && err.code === 11e3) {
619
+ throw new SetupError();
620
+ }
621
+ throw err;
622
+ }
623
+ const token = this.generateToken(staff._id.toString(), STAFF_ROLE.Owner);
624
+ this.logger.info("Owner setup complete", { staffId: staff._id.toString() });
625
+ this.hooks.onStaffCreated?.(staff);
626
+ this.hooks.onMetric?.({ name: "staff_setup_complete", value: 1 });
627
+ return { staff, token };
628
+ }
629
+ async login(email, password, ip) {
630
+ if (ip) {
631
+ const limit = await this.rateLimiter.checkLimit(ip);
632
+ if (!limit.allowed) {
633
+ this.hooks.onLoginFailed?.(email, ip);
634
+ throw new RateLimitError(limit.retryAfterMs);
635
+ }
636
+ }
637
+ const staff = await this.Staff.findOne({
638
+ email: email.toLowerCase().trim(),
639
+ ...this.tenantFilter
640
+ }).select("+password");
641
+ if (!staff) {
642
+ if (ip) await this.rateLimiter.recordAttempt(ip);
643
+ this.hooks.onLoginFailed?.(email, ip);
644
+ throw new AuthenticationError(ERROR_CODE.InvalidCredentials);
645
+ }
646
+ const valid = await this.adapters.comparePassword(password, staff.password);
647
+ if (!valid) {
648
+ if (ip) await this.rateLimiter.recordAttempt(ip);
649
+ this.hooks.onLoginFailed?.(email, ip);
650
+ throw new AuthenticationError(ERROR_CODE.InvalidCredentials);
651
+ }
652
+ if (staff.status === STAFF_STATUS.Inactive) {
653
+ throw new AuthenticationError(ERROR_CODE.AccountInactive, ERROR_MESSAGE.AccountInactive);
654
+ }
655
+ if (staff.status === STAFF_STATUS.Pending) {
656
+ throw new AuthenticationError(ERROR_CODE.AccountPending, ERROR_MESSAGE.AccountPending);
657
+ }
658
+ staff.lastLoginAt = /* @__PURE__ */ new Date();
659
+ if (ip) staff.lastLoginIp = ip;
660
+ await staff.save();
661
+ if (ip) await this.rateLimiter.reset(ip);
662
+ const token = this.generateToken(staff._id.toString(), staff.role);
663
+ this.hooks.onLogin?.(staff.toObject(), ip);
664
+ this.hooks.onMetric?.({ name: "staff_login", value: 1, labels: { role: staff.role } });
665
+ this.logger.info("Staff login", { staffId: staff._id.toString() });
666
+ const staffObj = staff.toObject();
667
+ delete staffObj.password;
668
+ return { staff: staffObj, token };
669
+ }
609
670
  async resetPassword(staffId, newPassword) {
610
671
  const staff = await this.Staff.findOne({ _id: staffId, ...this.tenantFilter });
611
672
  if (!staff) throw new StaffNotFoundError(staffId);
@@ -758,11 +819,11 @@ function handleStaffError(res, error, logger) {
758
819
  }
759
820
 
760
821
  // src/routes/auth.routes.ts
761
- function createAuthRoutes(staffService, auth, logger, allowSelfPasswordChange) {
822
+ function createAuthRoutes(staffService, authService, auth, logger, allowSelfPasswordChange) {
762
823
  const router = Router();
763
824
  router.post("/setup", async (req, res) => {
764
825
  try {
765
- const result = await staffService.setupOwner(req.body);
826
+ const result = await authService.setupOwner(req.body);
766
827
  sendSuccess(res, result, 201);
767
828
  } catch (error) {
768
829
  handleStaffError(res, error, logger);
@@ -772,7 +833,7 @@ function createAuthRoutes(staffService, auth, logger, allowSelfPasswordChange) {
772
833
  try {
773
834
  const { email, password } = req.body;
774
835
  const ip = req.ip || req.socket.remoteAddress || "";
775
- const result = await staffService.login(email, password, ip);
836
+ const result = await authService.login(email, password, ip);
776
837
  sendSuccess(res, result);
777
838
  } catch (error) {
778
839
  handleStaffError(res, error, logger);
@@ -792,7 +853,7 @@ function createAuthRoutes(staffService, auth, logger, allowSelfPasswordChange) {
792
853
  try {
793
854
  const user = req.user;
794
855
  const { oldPassword, newPassword } = req.body;
795
- await staffService.changeOwnPassword(user.staffId, oldPassword, newPassword);
856
+ await authService.changeOwnPassword(user.staffId, oldPassword, newPassword);
796
857
  sendSuccess(res, { message: "Password changed successfully" });
797
858
  } catch (error) {
798
859
  handleStaffError(res, error, logger);
@@ -801,7 +862,7 @@ function createAuthRoutes(staffService, auth, logger, allowSelfPasswordChange) {
801
862
  }
802
863
  return router;
803
864
  }
804
- function createStaffRoutes(staffService, logger) {
865
+ function createStaffRoutes(staffService, logger, authService) {
805
866
  const router = Router();
806
867
  router.get("/", async (req, res) => {
807
868
  try {
@@ -857,7 +918,10 @@ function createStaffRoutes(staffService, logger) {
857
918
  });
858
919
  router.put("/:staffId/password", async (req, res) => {
859
920
  try {
860
- await staffService.resetPassword(req.params["staffId"], req.body.password);
921
+ if (!authService) {
922
+ throw new Error("AuthService not available");
923
+ }
924
+ await authService.resetPassword(req.params["staffId"], req.body.password);
861
925
  sendSuccess(res, { message: "Password reset successfully" });
862
926
  } catch (error) {
863
927
  handleStaffError(res, error, logger);
@@ -905,55 +969,11 @@ function createPermissionGroupRoutes(permissionService, auth, logger) {
905
969
  // src/routes/index.ts
906
970
  function createRoutes(services, auth, logger, allowSelfPasswordChange) {
907
971
  const router = Router();
908
- router.use("/", createAuthRoutes(services.staff, auth, logger, allowSelfPasswordChange));
909
- router.use("/", auth.verifyToken, auth.ownerOnly, createStaffRoutes(services.staff, logger));
972
+ router.use("/", createAuthRoutes(services.staff, services.auth, auth, logger, allowSelfPasswordChange));
973
+ router.use("/", auth.verifyToken, auth.ownerOnly, createStaffRoutes(services.staff, logger, services.auth));
910
974
  router.use("/permission-groups", createPermissionGroupRoutes(services.permissions, auth, logger));
911
975
  return router;
912
976
  }
913
- var StaffEngineConfigSchema = z.object({
914
- db: z.object({
915
- connection: z.unknown().refine((v) => v !== void 0 && v !== null, {
916
- message: "db.connection is required"
917
- }),
918
- collectionPrefix: z.string().optional()
919
- }),
920
- redis: z.object({
921
- connection: z.unknown(),
922
- keyPrefix: z.string().optional()
923
- }).optional(),
924
- logger: z.object({
925
- info: z.function(),
926
- warn: z.function(),
927
- error: z.function()
928
- }).optional(),
929
- tenantId: z.string().optional(),
930
- auth: z.object({
931
- jwtSecret: z.string().min(1),
932
- staffTokenExpiry: z.string().optional(),
933
- ownerTokenExpiry: z.string().optional(),
934
- permissionCacheTtlMs: z.number().int().positive().optional()
935
- }),
936
- adapters: z.object({
937
- hashPassword: z.function(),
938
- comparePassword: z.function()
939
- }),
940
- hooks: z.object({
941
- onStaffCreated: z.function().optional(),
942
- onLogin: z.function().optional(),
943
- onLoginFailed: z.function().optional(),
944
- onPermissionsChanged: z.function().optional(),
945
- onStatusChanged: z.function().optional(),
946
- onMetric: z.function().optional()
947
- }).optional(),
948
- options: z.object({
949
- requireEmailUniqueness: z.boolean().optional(),
950
- allowSelfPasswordChange: z.boolean().optional(),
951
- rateLimiter: z.object({
952
- windowMs: z.number().int().positive().optional(),
953
- maxAttempts: z.number().int().positive().optional()
954
- }).optional()
955
- }).optional()
956
- });
957
977
  function createStaffEngine(config) {
958
978
  const parseResult = StaffEngineConfigSchema.safeParse(config);
959
979
  if (!parseResult.success) {
@@ -1008,13 +1028,20 @@ function createStaffEngine(config) {
1008
1028
  adapters: config.adapters,
1009
1029
  hooks: config.hooks ?? {},
1010
1030
  permissionCache,
1031
+ logger,
1032
+ tenantId: config.tenantId,
1033
+ requireEmailUniqueness: resolvedOptions.requireEmailUniqueness
1034
+ });
1035
+ const authService = new AuthService({
1036
+ Staff: StaffModel,
1037
+ adapters: config.adapters,
1038
+ hooks: config.hooks ?? {},
1011
1039
  rateLimiter,
1012
1040
  logger,
1013
1041
  tenantId: config.tenantId,
1014
1042
  jwtSecret: resolvedAuth.jwtSecret,
1015
1043
  staffTokenExpiry: resolvedAuth.staffTokenExpiry,
1016
1044
  ownerTokenExpiry: resolvedAuth.ownerTokenExpiry,
1017
- requireEmailUniqueness: resolvedOptions.requireEmailUniqueness,
1018
1045
  allowSelfPasswordChange: resolvedOptions.allowSelfPasswordChange
1019
1046
  });
1020
1047
  const auth = createAuthMiddleware(
@@ -1025,7 +1052,7 @@ function createStaffEngine(config) {
1025
1052
  config.tenantId
1026
1053
  );
1027
1054
  const routes = createRoutes(
1028
- { staff: staffService, permissions: permissionService },
1055
+ { staff: staffService, auth: authService, permissions: permissionService },
1029
1056
  auth,
1030
1057
  logger,
1031
1058
  resolvedOptions.allowSelfPasswordChange
@@ -1038,12 +1065,13 @@ function createStaffEngine(config) {
1038
1065
  routes,
1039
1066
  auth,
1040
1067
  staff: staffService,
1068
+ authService,
1041
1069
  permissions: permissionService,
1042
1070
  models: { Staff: StaffModel, PermissionGroup: PermissionGroupModel },
1043
1071
  destroy
1044
1072
  };
1045
1073
  }
1046
1074
 
1047
- export { AlxStaffError, AuthenticationError, AuthorizationError, DEFAULTS, DEFAULT_AUTH, DuplicateError, ERROR_CODE, ERROR_MESSAGE, GroupNotFoundError, InvalidConfigError, InvalidPermissionError, LastOwnerError, PermissionCacheService, PermissionService, RateLimitError, RateLimiterService, SetupError, StaffNotFoundError, StaffService, TokenError, createAuthMiddleware, createPermissionGroupModel, createRoutes, createStaffEngine, createStaffModel, handleStaffError, validatePermissionPairs };
1075
+ export { AlxStaffError, AuthService, AuthenticationError, AuthorizationError, DEFAULTS, DEFAULT_AUTH, DuplicateError, ERROR_CODE, ERROR_MESSAGE, GroupNotFoundError, InvalidConfigError, InvalidPermissionError, LastOwnerError, PermissionCacheService, PermissionService, RateLimitError, RateLimiterService, SetupError, StaffEngineConfigSchema, StaffNotFoundError, StaffService, TokenError, createAuthMiddleware, createPermissionGroupModel, createRoutes, createStaffEngine, createStaffModel, handleStaffError, validatePermissionPairs };
1048
1076
  //# sourceMappingURL=index.mjs.map
1049
1077
  //# sourceMappingURL=index.mjs.map