@astralibx/staff-engine 0.2.0 → 0.2.2
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 +35 -0
- package/dist/index.cjs +170 -140
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +234 -17
- package/dist/index.d.ts +234 -17
- package/dist/index.mjs +169 -141
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,6 +70,20 @@ app.listen(3000);
|
|
|
70
70
|
### Permissions
|
|
71
71
|
|
|
72
72
|
- **Runtime-configurable groups via API** -- `POST /permission-groups` creates a named group with permission entries. Groups have `groupId`, `label`, `sortOrder`, and an array of entries (key, label, type). No redeploy required to define new permissions.
|
|
73
|
+
|
|
74
|
+
> **Note:** Permission groups use `label` for display text and `groupId` for the unique identifier -- not `name`.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
await engine.permissions.createGroup({
|
|
78
|
+
groupId: 'chat-management', // unique identifier, kebab-case
|
|
79
|
+
label: 'Chat Management', // display text shown in UI
|
|
80
|
+
permissions: [
|
|
81
|
+
{ key: 'chat:view', label: 'View chats', type: 'view' },
|
|
82
|
+
{ key: 'chat:edit', label: 'Edit chats', type: 'edit' },
|
|
83
|
+
],
|
|
84
|
+
sortOrder: 1,
|
|
85
|
+
});
|
|
86
|
+
```
|
|
73
87
|
- **Edit-to-view cascade** -- when granting a permission ending in `.edit`, the corresponding `.view` key is automatically required. The engine validates this on `PUT /:id/permissions`.
|
|
74
88
|
- **Permission cache (Redis or in-memory)** -- each staff member's resolved permission list is cached after the first lookup. TTL is `permissionCacheTtlMs` (default 5 min). Cache is invalidated immediately on `updatePermissions` or `updateStatus`.
|
|
75
89
|
- **Owner bypasses all checks** -- `requirePermission` middleware skips the permission check entirely when `req.user.role === 'owner'`.
|
|
@@ -122,6 +136,27 @@ The factory function returns a single `StaffEngine` object:
|
|
|
122
136
|
| `engine.models` | Mongoose models (`Staff`, `PermissionGroup`) |
|
|
123
137
|
| `engine.destroy()` | Flush permission cache and clean up resources |
|
|
124
138
|
|
|
139
|
+
## Seeding Data
|
|
140
|
+
|
|
141
|
+
Schema factory functions are exported so you can seed data or run scripts without creating a full engine instance:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { createStaffModel, createPermissionGroupModel } from '@astralibx/staff-engine';
|
|
145
|
+
|
|
146
|
+
const Staff = createStaffModel(connection);
|
|
147
|
+
const PermissionGroup = createPermissionGroupModel(connection);
|
|
148
|
+
|
|
149
|
+
await PermissionGroup.create({
|
|
150
|
+
groupId: 'admin',
|
|
151
|
+
label: 'Admin',
|
|
152
|
+
permissions: [
|
|
153
|
+
{ key: 'chat:view', label: 'View chats', type: 'view' },
|
|
154
|
+
{ key: 'chat:edit', label: 'Edit chats', type: 'edit' },
|
|
155
|
+
],
|
|
156
|
+
sortOrder: 1,
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
125
160
|
## Redis Key Prefix (Required for Multi-Project Deployments)
|
|
126
161
|
|
|
127
162
|
> **WARNING:** If multiple projects share the same Redis server, you MUST set a unique `keyPrefix` per project. Without this, rate limiter state and permission cache entries will collide across projects.
|
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var zod = require('zod');
|
|
4
3
|
var core = require('@astralibx/core');
|
|
5
4
|
var staffTypes = require('@astralibx/staff-types');
|
|
6
5
|
var mongoose = require('mongoose');
|
|
6
|
+
var zod = require('zod');
|
|
7
7
|
var jwt2 = require('jsonwebtoken');
|
|
8
8
|
var express = require('express');
|
|
9
9
|
|
|
@@ -403,6 +403,50 @@ function validatePermissionPairs(permissions, allGroups) {
|
|
|
403
403
|
throw new InvalidPermissionError(missingViewKeys);
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
|
+
var StaffEngineConfigSchema = zod.z.object({
|
|
407
|
+
db: zod.z.object({
|
|
408
|
+
connection: zod.z.unknown().refine((v) => v !== void 0 && v !== null, {
|
|
409
|
+
message: "db.connection is required"
|
|
410
|
+
}),
|
|
411
|
+
collectionPrefix: zod.z.string().optional()
|
|
412
|
+
}),
|
|
413
|
+
redis: zod.z.object({
|
|
414
|
+
connection: zod.z.unknown(),
|
|
415
|
+
keyPrefix: zod.z.string().optional()
|
|
416
|
+
}).optional(),
|
|
417
|
+
logger: zod.z.object({
|
|
418
|
+
info: zod.z.function(),
|
|
419
|
+
warn: zod.z.function(),
|
|
420
|
+
error: zod.z.function()
|
|
421
|
+
}).optional(),
|
|
422
|
+
tenantId: zod.z.string().optional(),
|
|
423
|
+
auth: zod.z.object({
|
|
424
|
+
jwtSecret: zod.z.string().min(1),
|
|
425
|
+
staffTokenExpiry: zod.z.string().optional(),
|
|
426
|
+
ownerTokenExpiry: zod.z.string().optional(),
|
|
427
|
+
permissionCacheTtlMs: zod.z.number().int().positive().optional()
|
|
428
|
+
}),
|
|
429
|
+
adapters: zod.z.object({
|
|
430
|
+
hashPassword: zod.z.function(),
|
|
431
|
+
comparePassword: zod.z.function()
|
|
432
|
+
}),
|
|
433
|
+
hooks: zod.z.object({
|
|
434
|
+
onStaffCreated: zod.z.function().optional(),
|
|
435
|
+
onLogin: zod.z.function().optional(),
|
|
436
|
+
onLoginFailed: zod.z.function().optional(),
|
|
437
|
+
onPermissionsChanged: zod.z.function().optional(),
|
|
438
|
+
onStatusChanged: zod.z.function().optional(),
|
|
439
|
+
onMetric: zod.z.function().optional()
|
|
440
|
+
}).optional(),
|
|
441
|
+
options: zod.z.object({
|
|
442
|
+
requireEmailUniqueness: zod.z.boolean().optional(),
|
|
443
|
+
allowSelfPasswordChange: zod.z.boolean().optional(),
|
|
444
|
+
rateLimiter: zod.z.object({
|
|
445
|
+
windowMs: zod.z.number().int().positive().optional(),
|
|
446
|
+
maxAttempts: zod.z.number().int().positive().optional()
|
|
447
|
+
}).optional()
|
|
448
|
+
}).optional()
|
|
449
|
+
});
|
|
406
450
|
|
|
407
451
|
// src/services/staff.service.ts
|
|
408
452
|
var StaffService = class {
|
|
@@ -411,105 +455,22 @@ var StaffService = class {
|
|
|
411
455
|
adapters;
|
|
412
456
|
hooks;
|
|
413
457
|
permissionCache;
|
|
414
|
-
rateLimiter;
|
|
415
458
|
logger;
|
|
416
459
|
tenantId;
|
|
417
|
-
jwtSecret;
|
|
418
|
-
staffTokenExpiry;
|
|
419
|
-
ownerTokenExpiry;
|
|
420
460
|
requireEmailUniqueness;
|
|
421
|
-
allowSelfPasswordChange;
|
|
422
461
|
constructor(deps) {
|
|
423
462
|
this.Staff = deps.Staff;
|
|
424
463
|
this.PermissionGroup = deps.PermissionGroup;
|
|
425
464
|
this.adapters = deps.adapters;
|
|
426
465
|
this.hooks = deps.hooks;
|
|
427
466
|
this.permissionCache = deps.permissionCache;
|
|
428
|
-
this.rateLimiter = deps.rateLimiter;
|
|
429
467
|
this.logger = deps.logger;
|
|
430
468
|
this.tenantId = deps.tenantId;
|
|
431
|
-
this.jwtSecret = deps.jwtSecret;
|
|
432
|
-
this.staffTokenExpiry = deps.staffTokenExpiry;
|
|
433
|
-
this.ownerTokenExpiry = deps.ownerTokenExpiry;
|
|
434
469
|
this.requireEmailUniqueness = deps.requireEmailUniqueness;
|
|
435
|
-
this.allowSelfPasswordChange = deps.allowSelfPasswordChange;
|
|
436
470
|
}
|
|
437
471
|
get tenantFilter() {
|
|
438
472
|
return this.tenantId ? { tenantId: this.tenantId } : {};
|
|
439
473
|
}
|
|
440
|
-
generateToken(staffId, role) {
|
|
441
|
-
const expiresIn = role === staffTypes.STAFF_ROLE.Owner ? this.ownerTokenExpiry : this.staffTokenExpiry;
|
|
442
|
-
return jwt2__default.default.sign({ staffId, role }, this.jwtSecret, { expiresIn });
|
|
443
|
-
}
|
|
444
|
-
async setupOwner(data) {
|
|
445
|
-
const count = await this.Staff.countDocuments(this.tenantFilter);
|
|
446
|
-
if (count > 0) throw new SetupError();
|
|
447
|
-
const hashedPassword = await this.adapters.hashPassword(data.password);
|
|
448
|
-
let staff;
|
|
449
|
-
try {
|
|
450
|
-
const doc = await this.Staff.create({
|
|
451
|
-
name: data.name,
|
|
452
|
-
email: data.email.toLowerCase().trim(),
|
|
453
|
-
password: hashedPassword,
|
|
454
|
-
role: staffTypes.STAFF_ROLE.Owner,
|
|
455
|
-
status: staffTypes.STAFF_STATUS.Active,
|
|
456
|
-
permissions: [],
|
|
457
|
-
...this.tenantFilter
|
|
458
|
-
});
|
|
459
|
-
staff = doc.toObject();
|
|
460
|
-
} catch (err) {
|
|
461
|
-
if (err && typeof err === "object" && "code" in err && err.code === 11e3) {
|
|
462
|
-
throw new SetupError();
|
|
463
|
-
}
|
|
464
|
-
throw err;
|
|
465
|
-
}
|
|
466
|
-
const token = this.generateToken(staff._id.toString(), staffTypes.STAFF_ROLE.Owner);
|
|
467
|
-
this.logger.info("Owner setup complete", { staffId: staff._id.toString() });
|
|
468
|
-
this.hooks.onStaffCreated?.(staff);
|
|
469
|
-
this.hooks.onMetric?.({ name: "staff_setup_complete", value: 1 });
|
|
470
|
-
return { staff, token };
|
|
471
|
-
}
|
|
472
|
-
async login(email, password, ip) {
|
|
473
|
-
if (ip) {
|
|
474
|
-
const limit = await this.rateLimiter.checkLimit(ip);
|
|
475
|
-
if (!limit.allowed) {
|
|
476
|
-
this.hooks.onLoginFailed?.(email, ip);
|
|
477
|
-
throw new RateLimitError(limit.retryAfterMs);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
const staff = await this.Staff.findOne({
|
|
481
|
-
email: email.toLowerCase().trim(),
|
|
482
|
-
...this.tenantFilter
|
|
483
|
-
}).select("+password");
|
|
484
|
-
if (!staff) {
|
|
485
|
-
if (ip) await this.rateLimiter.recordAttempt(ip);
|
|
486
|
-
this.hooks.onLoginFailed?.(email, ip);
|
|
487
|
-
throw new AuthenticationError(ERROR_CODE.InvalidCredentials);
|
|
488
|
-
}
|
|
489
|
-
const valid = await this.adapters.comparePassword(password, staff.password);
|
|
490
|
-
if (!valid) {
|
|
491
|
-
if (ip) await this.rateLimiter.recordAttempt(ip);
|
|
492
|
-
this.hooks.onLoginFailed?.(email, ip);
|
|
493
|
-
throw new AuthenticationError(ERROR_CODE.InvalidCredentials);
|
|
494
|
-
}
|
|
495
|
-
if (staff.status === staffTypes.STAFF_STATUS.Inactive) {
|
|
496
|
-
throw new AuthenticationError(ERROR_CODE.AccountInactive, ERROR_MESSAGE.AccountInactive);
|
|
497
|
-
}
|
|
498
|
-
if (staff.status === staffTypes.STAFF_STATUS.Pending) {
|
|
499
|
-
throw new AuthenticationError(ERROR_CODE.AccountPending, ERROR_MESSAGE.AccountPending);
|
|
500
|
-
}
|
|
501
|
-
staff.lastLoginAt = /* @__PURE__ */ new Date();
|
|
502
|
-
if (ip) staff.lastLoginIp = ip;
|
|
503
|
-
await staff.save();
|
|
504
|
-
if (ip) await this.rateLimiter.reset(ip);
|
|
505
|
-
const token = this.generateToken(staff._id.toString(), staff.role);
|
|
506
|
-
this.hooks.onLogin?.(staff.toObject(), ip);
|
|
507
|
-
this.hooks.onMetric?.({ name: "staff_login", value: 1, labels: { role: staff.role } });
|
|
508
|
-
this.logger.info("Staff login", { staffId: staff._id.toString() });
|
|
509
|
-
const staffObj = staff.toObject();
|
|
510
|
-
delete staffObj.password;
|
|
511
|
-
return { staff: staffObj, token };
|
|
512
|
-
}
|
|
513
474
|
async create(data) {
|
|
514
475
|
if (this.requireEmailUniqueness) {
|
|
515
476
|
const existing = await this.Staff.findOne({
|
|
@@ -610,6 +571,106 @@ var StaffService = class {
|
|
|
610
571
|
this.logger.info("Staff status updated", { staffId, oldStatus, newStatus: status });
|
|
611
572
|
return staff.toObject();
|
|
612
573
|
}
|
|
574
|
+
};
|
|
575
|
+
var AuthService = class {
|
|
576
|
+
Staff;
|
|
577
|
+
adapters;
|
|
578
|
+
hooks;
|
|
579
|
+
rateLimiter;
|
|
580
|
+
logger;
|
|
581
|
+
tenantId;
|
|
582
|
+
jwtSecret;
|
|
583
|
+
staffTokenExpiry;
|
|
584
|
+
ownerTokenExpiry;
|
|
585
|
+
allowSelfPasswordChange;
|
|
586
|
+
constructor(deps) {
|
|
587
|
+
this.Staff = deps.Staff;
|
|
588
|
+
this.adapters = deps.adapters;
|
|
589
|
+
this.hooks = deps.hooks;
|
|
590
|
+
this.rateLimiter = deps.rateLimiter;
|
|
591
|
+
this.logger = deps.logger;
|
|
592
|
+
this.tenantId = deps.tenantId;
|
|
593
|
+
this.jwtSecret = deps.jwtSecret;
|
|
594
|
+
this.staffTokenExpiry = deps.staffTokenExpiry;
|
|
595
|
+
this.ownerTokenExpiry = deps.ownerTokenExpiry;
|
|
596
|
+
this.allowSelfPasswordChange = deps.allowSelfPasswordChange;
|
|
597
|
+
}
|
|
598
|
+
get tenantFilter() {
|
|
599
|
+
return this.tenantId ? { tenantId: this.tenantId } : {};
|
|
600
|
+
}
|
|
601
|
+
generateToken(staffId, role) {
|
|
602
|
+
const expiresIn = role === staffTypes.STAFF_ROLE.Owner ? this.ownerTokenExpiry : this.staffTokenExpiry;
|
|
603
|
+
return jwt2__default.default.sign({ staffId, role }, this.jwtSecret, { expiresIn });
|
|
604
|
+
}
|
|
605
|
+
async setupOwner(data) {
|
|
606
|
+
const count = await this.Staff.countDocuments(this.tenantFilter);
|
|
607
|
+
if (count > 0) throw new SetupError();
|
|
608
|
+
const hashedPassword = await this.adapters.hashPassword(data.password);
|
|
609
|
+
let staff;
|
|
610
|
+
try {
|
|
611
|
+
const doc = await this.Staff.create({
|
|
612
|
+
name: data.name,
|
|
613
|
+
email: data.email.toLowerCase().trim(),
|
|
614
|
+
password: hashedPassword,
|
|
615
|
+
role: staffTypes.STAFF_ROLE.Owner,
|
|
616
|
+
status: staffTypes.STAFF_STATUS.Active,
|
|
617
|
+
permissions: [],
|
|
618
|
+
...this.tenantFilter
|
|
619
|
+
});
|
|
620
|
+
staff = doc.toObject();
|
|
621
|
+
} catch (err) {
|
|
622
|
+
if (err && typeof err === "object" && "code" in err && err.code === 11e3) {
|
|
623
|
+
throw new SetupError();
|
|
624
|
+
}
|
|
625
|
+
throw err;
|
|
626
|
+
}
|
|
627
|
+
const token = this.generateToken(staff._id.toString(), staffTypes.STAFF_ROLE.Owner);
|
|
628
|
+
this.logger.info("Owner setup complete", { staffId: staff._id.toString() });
|
|
629
|
+
this.hooks.onStaffCreated?.(staff);
|
|
630
|
+
this.hooks.onMetric?.({ name: "staff_setup_complete", value: 1 });
|
|
631
|
+
return { staff, token };
|
|
632
|
+
}
|
|
633
|
+
async login(email, password, ip) {
|
|
634
|
+
if (ip) {
|
|
635
|
+
const limit = await this.rateLimiter.checkLimit(ip);
|
|
636
|
+
if (!limit.allowed) {
|
|
637
|
+
this.hooks.onLoginFailed?.(email, ip);
|
|
638
|
+
throw new RateLimitError(limit.retryAfterMs);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const staff = await this.Staff.findOne({
|
|
642
|
+
email: email.toLowerCase().trim(),
|
|
643
|
+
...this.tenantFilter
|
|
644
|
+
}).select("+password");
|
|
645
|
+
if (!staff) {
|
|
646
|
+
if (ip) await this.rateLimiter.recordAttempt(ip);
|
|
647
|
+
this.hooks.onLoginFailed?.(email, ip);
|
|
648
|
+
throw new AuthenticationError(ERROR_CODE.InvalidCredentials);
|
|
649
|
+
}
|
|
650
|
+
const valid = await this.adapters.comparePassword(password, staff.password);
|
|
651
|
+
if (!valid) {
|
|
652
|
+
if (ip) await this.rateLimiter.recordAttempt(ip);
|
|
653
|
+
this.hooks.onLoginFailed?.(email, ip);
|
|
654
|
+
throw new AuthenticationError(ERROR_CODE.InvalidCredentials);
|
|
655
|
+
}
|
|
656
|
+
if (staff.status === staffTypes.STAFF_STATUS.Inactive) {
|
|
657
|
+
throw new AuthenticationError(ERROR_CODE.AccountInactive, ERROR_MESSAGE.AccountInactive);
|
|
658
|
+
}
|
|
659
|
+
if (staff.status === staffTypes.STAFF_STATUS.Pending) {
|
|
660
|
+
throw new AuthenticationError(ERROR_CODE.AccountPending, ERROR_MESSAGE.AccountPending);
|
|
661
|
+
}
|
|
662
|
+
staff.lastLoginAt = /* @__PURE__ */ new Date();
|
|
663
|
+
if (ip) staff.lastLoginIp = ip;
|
|
664
|
+
await staff.save();
|
|
665
|
+
if (ip) await this.rateLimiter.reset(ip);
|
|
666
|
+
const token = this.generateToken(staff._id.toString(), staff.role);
|
|
667
|
+
this.hooks.onLogin?.(staff.toObject(), ip);
|
|
668
|
+
this.hooks.onMetric?.({ name: "staff_login", value: 1, labels: { role: staff.role } });
|
|
669
|
+
this.logger.info("Staff login", { staffId: staff._id.toString() });
|
|
670
|
+
const staffObj = staff.toObject();
|
|
671
|
+
delete staffObj.password;
|
|
672
|
+
return { staff: staffObj, token };
|
|
673
|
+
}
|
|
613
674
|
async resetPassword(staffId, newPassword) {
|
|
614
675
|
const staff = await this.Staff.findOne({ _id: staffId, ...this.tenantFilter });
|
|
615
676
|
if (!staff) throw new StaffNotFoundError(staffId);
|
|
@@ -637,11 +698,11 @@ function createAuthMiddleware(jwtSecret, permissionCache, StaffModel, logger, te
|
|
|
637
698
|
if (!payload.staffId || !payload.role) return null;
|
|
638
699
|
const filter = { _id: payload.staffId };
|
|
639
700
|
if (tenantId) filter.tenantId = tenantId;
|
|
640
|
-
const staff = await StaffModel.findOne(filter).select("status role").lean();
|
|
701
|
+
const staff = await StaffModel.findOne(filter).select("name email status role").lean();
|
|
641
702
|
if (!staff) return null;
|
|
642
703
|
if (staff.status !== staffTypes.STAFF_STATUS.Active) return null;
|
|
643
704
|
const permissions = await permissionCache.get(payload.staffId);
|
|
644
|
-
return { staffId: payload.staffId, role: staff.role, permissions };
|
|
705
|
+
return { staffId: payload.staffId, name: staff.name, email: staff.email, role: staff.role, permissions };
|
|
645
706
|
} catch {
|
|
646
707
|
return null;
|
|
647
708
|
}
|
|
@@ -762,11 +823,11 @@ function handleStaffError(res, error, logger) {
|
|
|
762
823
|
}
|
|
763
824
|
|
|
764
825
|
// src/routes/auth.routes.ts
|
|
765
|
-
function createAuthRoutes(staffService, auth, logger, allowSelfPasswordChange) {
|
|
826
|
+
function createAuthRoutes(staffService, authService, auth, logger, allowSelfPasswordChange) {
|
|
766
827
|
const router = express.Router();
|
|
767
828
|
router.post("/setup", async (req, res) => {
|
|
768
829
|
try {
|
|
769
|
-
const result = await
|
|
830
|
+
const result = await authService.setupOwner(req.body);
|
|
770
831
|
core.sendSuccess(res, result, 201);
|
|
771
832
|
} catch (error) {
|
|
772
833
|
handleStaffError(res, error, logger);
|
|
@@ -776,7 +837,7 @@ function createAuthRoutes(staffService, auth, logger, allowSelfPasswordChange) {
|
|
|
776
837
|
try {
|
|
777
838
|
const { email, password } = req.body;
|
|
778
839
|
const ip = req.ip || req.socket.remoteAddress || "";
|
|
779
|
-
const result = await
|
|
840
|
+
const result = await authService.login(email, password, ip);
|
|
780
841
|
core.sendSuccess(res, result);
|
|
781
842
|
} catch (error) {
|
|
782
843
|
handleStaffError(res, error, logger);
|
|
@@ -796,7 +857,7 @@ function createAuthRoutes(staffService, auth, logger, allowSelfPasswordChange) {
|
|
|
796
857
|
try {
|
|
797
858
|
const user = req.user;
|
|
798
859
|
const { oldPassword, newPassword } = req.body;
|
|
799
|
-
await
|
|
860
|
+
await authService.changeOwnPassword(user.staffId, oldPassword, newPassword);
|
|
800
861
|
core.sendSuccess(res, { message: "Password changed successfully" });
|
|
801
862
|
} catch (error) {
|
|
802
863
|
handleStaffError(res, error, logger);
|
|
@@ -805,7 +866,7 @@ function createAuthRoutes(staffService, auth, logger, allowSelfPasswordChange) {
|
|
|
805
866
|
}
|
|
806
867
|
return router;
|
|
807
868
|
}
|
|
808
|
-
function createStaffRoutes(staffService, logger) {
|
|
869
|
+
function createStaffRoutes(staffService, logger, authService) {
|
|
809
870
|
const router = express.Router();
|
|
810
871
|
router.get("/", async (req, res) => {
|
|
811
872
|
try {
|
|
@@ -861,7 +922,10 @@ function createStaffRoutes(staffService, logger) {
|
|
|
861
922
|
});
|
|
862
923
|
router.put("/:staffId/password", async (req, res) => {
|
|
863
924
|
try {
|
|
864
|
-
|
|
925
|
+
if (!authService) {
|
|
926
|
+
throw new Error("AuthService not available");
|
|
927
|
+
}
|
|
928
|
+
await authService.resetPassword(req.params["staffId"], req.body.password);
|
|
865
929
|
core.sendSuccess(res, { message: "Password reset successfully" });
|
|
866
930
|
} catch (error) {
|
|
867
931
|
handleStaffError(res, error, logger);
|
|
@@ -909,55 +973,11 @@ function createPermissionGroupRoutes(permissionService, auth, logger) {
|
|
|
909
973
|
// src/routes/index.ts
|
|
910
974
|
function createRoutes(services, auth, logger, allowSelfPasswordChange) {
|
|
911
975
|
const router = express.Router();
|
|
912
|
-
router.use("/", createAuthRoutes(services.staff, auth, logger, allowSelfPasswordChange));
|
|
913
|
-
router.use("/", auth.verifyToken, auth.ownerOnly, createStaffRoutes(services.staff, logger));
|
|
976
|
+
router.use("/", createAuthRoutes(services.staff, services.auth, auth, logger, allowSelfPasswordChange));
|
|
977
|
+
router.use("/", auth.verifyToken, auth.ownerOnly, createStaffRoutes(services.staff, logger, services.auth));
|
|
914
978
|
router.use("/permission-groups", createPermissionGroupRoutes(services.permissions, auth, logger));
|
|
915
979
|
return router;
|
|
916
980
|
}
|
|
917
|
-
var StaffEngineConfigSchema = zod.z.object({
|
|
918
|
-
db: zod.z.object({
|
|
919
|
-
connection: zod.z.unknown().refine((v) => v !== void 0 && v !== null, {
|
|
920
|
-
message: "db.connection is required"
|
|
921
|
-
}),
|
|
922
|
-
collectionPrefix: zod.z.string().optional()
|
|
923
|
-
}),
|
|
924
|
-
redis: zod.z.object({
|
|
925
|
-
connection: zod.z.unknown(),
|
|
926
|
-
keyPrefix: zod.z.string().optional()
|
|
927
|
-
}).optional(),
|
|
928
|
-
logger: zod.z.object({
|
|
929
|
-
info: zod.z.function(),
|
|
930
|
-
warn: zod.z.function(),
|
|
931
|
-
error: zod.z.function()
|
|
932
|
-
}).optional(),
|
|
933
|
-
tenantId: zod.z.string().optional(),
|
|
934
|
-
auth: zod.z.object({
|
|
935
|
-
jwtSecret: zod.z.string().min(1),
|
|
936
|
-
staffTokenExpiry: zod.z.string().optional(),
|
|
937
|
-
ownerTokenExpiry: zod.z.string().optional(),
|
|
938
|
-
permissionCacheTtlMs: zod.z.number().int().positive().optional()
|
|
939
|
-
}),
|
|
940
|
-
adapters: zod.z.object({
|
|
941
|
-
hashPassword: zod.z.function(),
|
|
942
|
-
comparePassword: zod.z.function()
|
|
943
|
-
}),
|
|
944
|
-
hooks: zod.z.object({
|
|
945
|
-
onStaffCreated: zod.z.function().optional(),
|
|
946
|
-
onLogin: zod.z.function().optional(),
|
|
947
|
-
onLoginFailed: zod.z.function().optional(),
|
|
948
|
-
onPermissionsChanged: zod.z.function().optional(),
|
|
949
|
-
onStatusChanged: zod.z.function().optional(),
|
|
950
|
-
onMetric: zod.z.function().optional()
|
|
951
|
-
}).optional(),
|
|
952
|
-
options: zod.z.object({
|
|
953
|
-
requireEmailUniqueness: zod.z.boolean().optional(),
|
|
954
|
-
allowSelfPasswordChange: zod.z.boolean().optional(),
|
|
955
|
-
rateLimiter: zod.z.object({
|
|
956
|
-
windowMs: zod.z.number().int().positive().optional(),
|
|
957
|
-
maxAttempts: zod.z.number().int().positive().optional()
|
|
958
|
-
}).optional()
|
|
959
|
-
}).optional()
|
|
960
|
-
});
|
|
961
981
|
function createStaffEngine(config) {
|
|
962
982
|
const parseResult = StaffEngineConfigSchema.safeParse(config);
|
|
963
983
|
if (!parseResult.success) {
|
|
@@ -1012,13 +1032,20 @@ function createStaffEngine(config) {
|
|
|
1012
1032
|
adapters: config.adapters,
|
|
1013
1033
|
hooks: config.hooks ?? {},
|
|
1014
1034
|
permissionCache,
|
|
1035
|
+
logger,
|
|
1036
|
+
tenantId: config.tenantId,
|
|
1037
|
+
requireEmailUniqueness: resolvedOptions.requireEmailUniqueness
|
|
1038
|
+
});
|
|
1039
|
+
const authService = new AuthService({
|
|
1040
|
+
Staff: StaffModel,
|
|
1041
|
+
adapters: config.adapters,
|
|
1042
|
+
hooks: config.hooks ?? {},
|
|
1015
1043
|
rateLimiter,
|
|
1016
1044
|
logger,
|
|
1017
1045
|
tenantId: config.tenantId,
|
|
1018
1046
|
jwtSecret: resolvedAuth.jwtSecret,
|
|
1019
1047
|
staffTokenExpiry: resolvedAuth.staffTokenExpiry,
|
|
1020
1048
|
ownerTokenExpiry: resolvedAuth.ownerTokenExpiry,
|
|
1021
|
-
requireEmailUniqueness: resolvedOptions.requireEmailUniqueness,
|
|
1022
1049
|
allowSelfPasswordChange: resolvedOptions.allowSelfPasswordChange
|
|
1023
1050
|
});
|
|
1024
1051
|
const auth = createAuthMiddleware(
|
|
@@ -1029,7 +1056,7 @@ function createStaffEngine(config) {
|
|
|
1029
1056
|
config.tenantId
|
|
1030
1057
|
);
|
|
1031
1058
|
const routes = createRoutes(
|
|
1032
|
-
{ staff: staffService, permissions: permissionService },
|
|
1059
|
+
{ staff: staffService, auth: authService, permissions: permissionService },
|
|
1033
1060
|
auth,
|
|
1034
1061
|
logger,
|
|
1035
1062
|
resolvedOptions.allowSelfPasswordChange
|
|
@@ -1042,6 +1069,7 @@ function createStaffEngine(config) {
|
|
|
1042
1069
|
routes,
|
|
1043
1070
|
auth,
|
|
1044
1071
|
staff: staffService,
|
|
1072
|
+
authService,
|
|
1045
1073
|
permissions: permissionService,
|
|
1046
1074
|
models: { Staff: StaffModel, PermissionGroup: PermissionGroupModel },
|
|
1047
1075
|
destroy
|
|
@@ -1057,6 +1085,7 @@ Object.defineProperty(exports, "DEFAULT_OPTIONS", {
|
|
|
1057
1085
|
get: function () { return staffTypes.DEFAULT_OPTIONS; }
|
|
1058
1086
|
});
|
|
1059
1087
|
exports.AlxStaffError = AlxStaffError;
|
|
1088
|
+
exports.AuthService = AuthService;
|
|
1060
1089
|
exports.AuthenticationError = AuthenticationError;
|
|
1061
1090
|
exports.AuthorizationError = AuthorizationError;
|
|
1062
1091
|
exports.DEFAULTS = DEFAULTS;
|
|
@@ -1073,6 +1102,7 @@ exports.PermissionService = PermissionService;
|
|
|
1073
1102
|
exports.RateLimitError = RateLimitError;
|
|
1074
1103
|
exports.RateLimiterService = RateLimiterService;
|
|
1075
1104
|
exports.SetupError = SetupError;
|
|
1105
|
+
exports.StaffEngineConfigSchema = StaffEngineConfigSchema;
|
|
1076
1106
|
exports.StaffNotFoundError = StaffNotFoundError;
|
|
1077
1107
|
exports.StaffService = StaffService;
|
|
1078
1108
|
exports.TokenError = TokenError;
|