@endora-commerce/mod-admin-roles 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/admin/index.d.ts +47 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +27 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/backend/demo/reset.d.ts +20 -0
  8. package/dist/backend/demo/reset.d.ts.map +1 -0
  9. package/dist/backend/demo/reset.js +13 -0
  10. package/dist/backend/demo/reset.js.map +1 -0
  11. package/dist/backend/demo/rows.d.ts +64 -0
  12. package/dist/backend/demo/rows.d.ts.map +1 -0
  13. package/dist/backend/demo/rows.js +74 -0
  14. package/dist/backend/demo/rows.js.map +1 -0
  15. package/dist/backend/demo/seed.d.ts +20 -0
  16. package/dist/backend/demo/seed.d.ts.map +1 -0
  17. package/dist/backend/demo/seed.js +29 -0
  18. package/dist/backend/demo/seed.js.map +1 -0
  19. package/dist/backend/entities/admin-role.entity.d.ts +20 -0
  20. package/dist/backend/entities/admin-role.entity.d.ts.map +1 -0
  21. package/dist/backend/entities/admin-role.entity.js +65 -0
  22. package/dist/backend/entities/admin-role.entity.js.map +1 -0
  23. package/dist/backend/index.d.ts +96 -0
  24. package/dist/backend/index.d.ts.map +1 -0
  25. package/dist/backend/index.js +114 -0
  26. package/dist/backend/index.js.map +1 -0
  27. package/dist/backend/permission-inventory.d.ts +117 -0
  28. package/dist/backend/permission-inventory.d.ts.map +1 -0
  29. package/dist/backend/permission-inventory.js +585 -0
  30. package/dist/backend/permission-inventory.js.map +1 -0
  31. package/dist/backend/services/admin-role-ports.d.ts +39 -0
  32. package/dist/backend/services/admin-role-ports.d.ts.map +1 -0
  33. package/dist/backend/services/admin-role-ports.js +72 -0
  34. package/dist/backend/services/admin-role-ports.js.map +1 -0
  35. package/dist/backend/services/admin-role-service.d.ts +77 -0
  36. package/dist/backend/services/admin-role-service.d.ts.map +1 -0
  37. package/dist/backend/services/admin-role-service.js +219 -0
  38. package/dist/backend/services/admin-role-service.js.map +1 -0
  39. package/dist/backend/services/permission-catalogue.service.d.ts +130 -0
  40. package/dist/backend/services/permission-catalogue.service.d.ts.map +1 -0
  41. package/dist/backend/services/permission-catalogue.service.js +175 -0
  42. package/dist/backend/services/permission-catalogue.service.js.map +1 -0
  43. package/dist/backend/services/permission-service.d.ts +24 -0
  44. package/dist/backend/services/permission-service.d.ts.map +1 -0
  45. package/dist/backend/services/permission-service.js +40 -0
  46. package/dist/backend/services/permission-service.js.map +1 -0
  47. package/dist/manifest.d.ts +169 -0
  48. package/dist/manifest.d.ts.map +1 -0
  49. package/dist/manifest.js +192 -0
  50. package/dist/manifest.js.map +1 -0
  51. package/docs/admin_roles.md +38 -0
  52. package/i18n/en.json +10 -0
  53. package/i18n/pl.json +10 -0
  54. package/package.json +73 -0
  55. package/tailwind.css +14 -0
@@ -0,0 +1,72 @@
1
+ import { AdminRole } from '../entities/admin-role.entity.js';
2
+ import { listSystemRoleCodes, registerSystemRoleCode, } from './admin-role-service.js';
3
+ /**
4
+ * The published face of `admin_roles` (feature 075, Phase P).
5
+ *
6
+ * Fourteen inbound sites: `admin_users` reads a role beside its user and
7
+ * enforces `requiresTwoFactor` at login, `admin_actions` filters the command
8
+ * palette by a user's permissions, `blog` protects the role it seeds, and the
9
+ * dev seed creates one.
10
+ *
11
+ * `findByCode` is the method with no direct predecessor. `admin_users`'
12
+ * service and the dev seed both look a role up by code with
13
+ * `em.findOne(AdminRole, { code })`, which is the read it replaces.
14
+ */
15
+ export class AdminRoleReadService {
16
+ emFactory;
17
+ constructor(emFactory) {
18
+ this.emFactory = emFactory;
19
+ }
20
+ async findByCode(code) {
21
+ const role = await this.emFactory().findOne(AdminRole, { code });
22
+ return role ? toAdminRoleRecord(role) : null;
23
+ }
24
+ }
25
+ /**
26
+ * The full role port, adapting `AdminRoleService` and adding the by-code
27
+ * lookup. The service arrives as a getter so the adapter resolves it per call
28
+ * rather than capturing it.
29
+ */
30
+ export function createAdminRolePort(emFactory, getService) {
31
+ const read = new AdminRoleReadService(emFactory);
32
+ return {
33
+ async list() {
34
+ return (await getService().list()).map(toAdminRoleRecord);
35
+ },
36
+ async getById(id) {
37
+ return toAdminRoleRecord(await getService().getById(id));
38
+ },
39
+ findByCode: (code) => read.findByCode(code),
40
+ async upsertByCode(input) {
41
+ return toAdminRoleRecord(await getService().upsertByCode(input));
42
+ },
43
+ remove: (id) => getService().remove(id),
44
+ };
45
+ }
46
+ /**
47
+ * The contribution seam for deletion-protected role codes.
48
+ *
49
+ * Deliberately a plain object over the module-level set rather than anything
50
+ * gated: `blog` registers its seeded code from a boot hook, and a gate would
51
+ * both throw during composition and — worse — let an operator delete a
52
+ * protected role by switching its owner off for a moment. See the port's
53
+ * contract for the full reasoning.
54
+ */
55
+ export function createSystemRoleCodePort() {
56
+ return {
57
+ register: (code) => registerSystemRoleCode(code),
58
+ list: () => listSystemRoleCodes(),
59
+ };
60
+ }
61
+ export function toAdminRoleRecord(role) {
62
+ return {
63
+ id: role.id,
64
+ code: role.code,
65
+ name: role.name,
66
+ permissions: role.permissions ?? [],
67
+ requiresTwoFactor: role.requiresTwoFactor,
68
+ createdAt: role.createdAt,
69
+ updatedAt: role.updatedAt,
70
+ };
71
+ }
72
+ //# sourceMappingURL=admin-role-ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-role-ports.js","sourceRoot":"","sources":["../../../src/backend/services/admin-role-ports.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAC7D,OAAO,EAEL,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,oBAAoB;IACF;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,UAAU,CAAC,IAAY;QAC3B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QACjE,OAAO,IAAI,CAAC,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,SAA8B,EAC9B,UAAkC;IAElC,MAAM,IAAI,GAAG,IAAI,oBAAoB,CAAC,SAAS,CAAC,CAAC;IACjD,OAAO;QACL,KAAK,CAAC,IAAI;YACR,OAAO,CAAC,MAAM,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QAC5D,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,EAAE;YACd,OAAO,iBAAiB,CAAC,MAAM,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;QAC3D,CAAC;QACD,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAC3C,KAAK,CAAC,YAAY,CAAC,KAAK;YACtB,OAAO,iBAAiB,CAAC,MAAM,UAAU,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;QACnE,CAAC;QACD,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;KACxC,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO;QACL,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC;QAChD,IAAI,EAAE,GAAG,EAAE,CAAC,mBAAmB,EAAE;KAClC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAe;IAC/C,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,EAAE;QACnC,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;QACzC,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,77 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type AdminUserReadPort } from '@endora-commerce/contracts';
3
+ import { HttpError } from '@endora-commerce/platform/http';
4
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
5
+ import { AdminRole } from '../entities/admin-role.entity.js';
6
+ import type { PermissionCatalogueService } from './permission-catalogue.service.js';
7
+ export interface UpsertAdminRoleInput {
8
+ code: string;
9
+ name: string;
10
+ permissions: string[];
11
+ requiresTwoFactor?: boolean;
12
+ }
13
+ /**
14
+ * Module-level registration for seeded role codes that should be
15
+ * deletion-protected. Idempotent. Exposed as a top-level function so
16
+ * modules can call it from their plugin without holding an
17
+ * AdminRoleService instance.
18
+ */
19
+ export declare function registerSystemRoleCode(code: string): void;
20
+ /**
21
+ * The protected codes, in registration order. Published through
22
+ * `SystemRoleCodePort` (feature 075, Phase P) so a contributor can read back
23
+ * what the platform is protecting without reaching into this file.
24
+ */
25
+ export declare function listSystemRoleCodes(): readonly string[];
26
+ /** Test helper — clears the protected-codes registry between suites. */
27
+ export declare function _resetSystemRoleCodesForTests(): void;
28
+ /**
29
+ * The refusal a role deletion earns from its assignees, or `null` when it has
30
+ * none (issue #168).
31
+ *
32
+ * Exported and pure so the sentence's `{placeholders}` can be checked against
33
+ * the `details` a real refusal carries — the agreement issue #161 found nothing
34
+ * was checking, one code family over. `check:error-translations` sees that a
35
+ * code *has* a sentence; only a test that renders one can see that the sentence
36
+ * has anything to fill it.
37
+ *
38
+ * The token in `details.code` chooses the sentence, and the count fills it.
39
+ * Both populations refuse, and they are separated because the remedy differs:
40
+ * reassign a live admin; restore-and-reassign or purge a deleted one.
41
+ */
42
+ export declare function roleInUseRefusal(live: number, deleted: number): HttpError | null;
43
+ /**
44
+ * The refusal a seeded role's deletion earns, with the role's own code on it.
45
+ *
46
+ * Exported and pure for the reason {@link roleInUseRefusal} is: the sentence
47
+ * that replaces this message carries a `{role}` placeholder, and only a test
48
+ * that renders a **real** refusal against the bundle can see that the
49
+ * placeholder has something to fill it (issue #161, and !1181's finding that a
50
+ * translated sentence with no placeholder loses an interpolated value silently).
51
+ *
52
+ * `details.role`, never `details.code`: the latter is the refusal token the
53
+ * envelope keys `errors.<CODE>.<token>` on, so putting a role code there would
54
+ * send it looking for a sentence named after the role.
55
+ */
56
+ export declare function protectedRoleRefusal(code: string): HttpError;
57
+ export declare class AdminRoleService {
58
+ #private;
59
+ private readonly emFactory;
60
+ private readonly permissionCatalogue;
61
+ /** `adminUserReadPort` — who still holds a role (feature 075, Phase C). */
62
+ private readonly adminUsers;
63
+ private readonly auditLog?;
64
+ constructor(emFactory: () => EntityManager, permissionCatalogue: PermissionCatalogueService,
65
+ /** `adminUserReadPort` — who still holds a role (feature 075, Phase C). */
66
+ adminUsers: AdminUserReadPort, auditLog?: AuditPort | undefined);
67
+ /**
68
+ * Register a role code as system-protected. Called by modules at
69
+ * plugin startup for their seeded roles. Idempotent.
70
+ */
71
+ registerSystemRoleCode(code: string): void;
72
+ list(): Promise<AdminRole[]>;
73
+ getById(id: string): Promise<AdminRole>;
74
+ upsertByCode(input: UpsertAdminRoleInput): Promise<AdminRole>;
75
+ remove(id: string): Promise<void>;
76
+ }
77
+ //# sourceMappingURL=admin-role-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-role-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/admin-role-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAe,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAE3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAC7D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AA0BpF,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAWD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,SAAS,MAAM,EAAE,CAEvD;AAED,wEAAwE;AACxE,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAmBhF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAO5D;AAED,qBAAa,gBAAgB;;IAEzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,mBAAmB;IACpC,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAJT,SAAS,EAAE,MAAM,aAAa,EAC9B,mBAAmB,EAAE,0BAA0B;IAChE,2EAA2E;IAC1D,UAAU,EAAE,iBAAiB,EAC7B,QAAQ,CAAC,EAAE,SAAS,YAAA;IASvC;;;OAGG;IACH,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIpC,IAAI,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAK5B,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAOvC,YAAY,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,SAAS,CAAC;IAwC7D,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAoDxC"}
@@ -0,0 +1,219 @@
1
+ import { UniqueConstraintViolationException } from '@mikro-orm/core';
2
+ import { ERROR_CODES } from '@endora-commerce/contracts';
3
+ import { HttpError } from '@endora-commerce/platform/http';
4
+ import { recordAuditFromContext } from '@endora-commerce/platform/commands';
5
+ import { AdminRole } from '../entities/admin-role.entity.js';
6
+ /**
7
+ * AdminRoleService (T193). CRUD over AdminRole rows. Invariants:
8
+ *
9
+ * - permissions referenced in `permissions` MUST be in the merged assignable
10
+ * catalogue (`PermissionCatalogueService`), OR be the wildcard `*` which
11
+ * grants full access. When `*` is present the permission set is normalised
12
+ * to exactly `['*']` (a role is either "full access" or an explicit list).
13
+ * - the bootstrap `platform_admin` role is locked to `['*']`: it always has
14
+ * full access and its permission set cannot be downgraded from the UI.
15
+ * - a Role with assigned AdminUsers cannot be deleted (409); reassign or
16
+ * deactivate the users first. A **soft-deleted** admin still counts
17
+ * (issue #168): restoring the account restores its role, so the assignment
18
+ * survives the deletion and the role is still in use.
19
+ */
20
+ /** Wildcard permission — grants access to every gated admin route. */
21
+ const WILDCARD = '*';
22
+ /**
23
+ * The bootstrap super-admin role. Its permission set is forced to the wildcard
24
+ * on every upsert so it can never be locked out of the panel.
25
+ */
26
+ const PLATFORM_ADMIN_CODE = 'platform_admin';
27
+ /**
28
+ * Codes of seeded roles that other modules register as
29
+ * deletion-protected. Modules call `registerSystemRoleCode(...)` from
30
+ * their plugin to add their seeded codes to this set; once registered,
31
+ * `remove` refuses to delete a role whose `code` is in the set with a
32
+ * 409 ADMIN_ROLE_PROTECTED envelope (feature 016 / FR-025).
33
+ */
34
+ const SYSTEM_ROLE_CODES = new Set();
35
+ /**
36
+ * Module-level registration for seeded role codes that should be
37
+ * deletion-protected. Idempotent. Exposed as a top-level function so
38
+ * modules can call it from their plugin without holding an
39
+ * AdminRoleService instance.
40
+ */
41
+ export function registerSystemRoleCode(code) {
42
+ SYSTEM_ROLE_CODES.add(code);
43
+ }
44
+ /**
45
+ * The protected codes, in registration order. Published through
46
+ * `SystemRoleCodePort` (feature 075, Phase P) so a contributor can read back
47
+ * what the platform is protecting without reaching into this file.
48
+ */
49
+ export function listSystemRoleCodes() {
50
+ return [...SYSTEM_ROLE_CODES];
51
+ }
52
+ /** Test helper — clears the protected-codes registry between suites. */
53
+ export function _resetSystemRoleCodesForTests() {
54
+ SYSTEM_ROLE_CODES.clear();
55
+ }
56
+ /**
57
+ * The refusal a role deletion earns from its assignees, or `null` when it has
58
+ * none (issue #168).
59
+ *
60
+ * Exported and pure so the sentence's `{placeholders}` can be checked against
61
+ * the `details` a real refusal carries — the agreement issue #161 found nothing
62
+ * was checking, one code family over. `check:error-translations` sees that a
63
+ * code *has* a sentence; only a test that renders one can see that the sentence
64
+ * has anything to fill it.
65
+ *
66
+ * The token in `details.code` chooses the sentence, and the count fills it.
67
+ * Both populations refuse, and they are separated because the remedy differs:
68
+ * reassign a live admin; restore-and-reassign or purge a deleted one.
69
+ */
70
+ export function roleInUseRefusal(live, deleted) {
71
+ if (live > 0) {
72
+ return new HttpError(409, ERROR_CODES.ADMIN_ROLE_IN_USE, `Cannot delete role: ${live} admin user(s) still assigned.`, { code: 'assigned', assigned: live });
73
+ }
74
+ if (deleted > 0) {
75
+ return new HttpError(409, ERROR_CODES.ADMIN_ROLE_IN_USE, `Cannot delete role: ${deleted} deleted admin account(s) still hold it, ` +
76
+ `and restoring one restores the assignment.`, { code: 'assigned_to_deleted', deleted });
77
+ }
78
+ return null;
79
+ }
80
+ /**
81
+ * The refusal a seeded role's deletion earns, with the role's own code on it.
82
+ *
83
+ * Exported and pure for the reason {@link roleInUseRefusal} is: the sentence
84
+ * that replaces this message carries a `{role}` placeholder, and only a test
85
+ * that renders a **real** refusal against the bundle can see that the
86
+ * placeholder has something to fill it (issue #161, and !1181's finding that a
87
+ * translated sentence with no placeholder loses an interpolated value silently).
88
+ *
89
+ * `details.role`, never `details.code`: the latter is the refusal token the
90
+ * envelope keys `errors.<CODE>.<token>` on, so putting a role code there would
91
+ * send it looking for a sentence named after the role.
92
+ */
93
+ export function protectedRoleRefusal(code) {
94
+ return new HttpError(409, ERROR_CODES.ADMIN_ROLE_PROTECTED, `Cannot delete the system-protected role "${code}". Modules' seeded roles are immutable.`, { role: code });
95
+ }
96
+ export class AdminRoleService {
97
+ emFactory;
98
+ permissionCatalogue;
99
+ adminUsers;
100
+ auditLog;
101
+ constructor(emFactory, permissionCatalogue,
102
+ /** `adminUserReadPort` — who still holds a role (feature 075, Phase C). */
103
+ adminUsers, auditLog) {
104
+ this.emFactory = emFactory;
105
+ this.permissionCatalogue = permissionCatalogue;
106
+ this.adminUsers = adminUsers;
107
+ this.auditLog = auditLog;
108
+ }
109
+ #audit(em, action, objectId, stateBefore, stateAfter) {
110
+ if (this.auditLog) {
111
+ recordAuditFromContext(this.auditLog, em, { action, objectType: 'admin_role', objectId, stateBefore, stateAfter });
112
+ }
113
+ }
114
+ /**
115
+ * Register a role code as system-protected. Called by modules at
116
+ * plugin startup for their seeded roles. Idempotent.
117
+ */
118
+ registerSystemRoleCode(code) {
119
+ SYSTEM_ROLE_CODES.add(code);
120
+ }
121
+ async list() {
122
+ const em = this.emFactory();
123
+ return em.find(AdminRole, {}, { orderBy: { name: 'asc' } });
124
+ }
125
+ async getById(id) {
126
+ const em = this.emFactory();
127
+ const row = await em.findOne(AdminRole, { id });
128
+ if (!row)
129
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Admin role not found.');
130
+ return row;
131
+ }
132
+ async upsertByCode(input) {
133
+ // The platform_admin role is always full-access; ignore any narrower
134
+ // payload so the bootstrap super-admin can never be downgraded. Every
135
+ // other role may opt into the wildcard or carry an explicit list.
136
+ const permissions = input.code === PLATFORM_ADMIN_CODE
137
+ ? [WILDCARD]
138
+ : this.#normalizePermissions(input.permissions);
139
+ const em = this.emFactory();
140
+ let role = await em.findOne(AdminRole, { code: input.code });
141
+ if (role) {
142
+ role.name = input.name;
143
+ role.permissions = permissions;
144
+ role.requiresTwoFactor = input.requiresTwoFactor ?? role.requiresTwoFactor;
145
+ this.#audit(em, 'admin_role.upsert', role.id, null, { code: role.code, name: role.name });
146
+ await em.flush();
147
+ return role;
148
+ }
149
+ role = em.create(AdminRole, {
150
+ code: input.code,
151
+ name: input.name,
152
+ permissions,
153
+ requiresTwoFactor: input.requiresTwoFactor ?? false,
154
+ });
155
+ this.#audit(em, 'admin_role.upsert', role.id, null, { code: role.code, name: role.name });
156
+ try {
157
+ await em.persistAndFlush(role);
158
+ }
159
+ catch (err) {
160
+ if (err instanceof UniqueConstraintViolationException) {
161
+ throw new HttpError(409, ERROR_CODES.ADMIN_ROLE_CODE_TAKEN, 'A role with that code already exists.');
162
+ }
163
+ throw err;
164
+ }
165
+ return role;
166
+ }
167
+ async remove(id) {
168
+ const em = this.emFactory();
169
+ const role = await em.findOne(AdminRole, { id });
170
+ if (!role)
171
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Admin role not found.');
172
+ if (SYSTEM_ROLE_CODES.has(role.code))
173
+ throw protectedRoleRefusal(role.code);
174
+ // Every assignee a restore returns, live or binned (issue #168).
175
+ //
176
+ // A soft delete does not release the assignment: the row keeps its
177
+ // `admin_role_id`, and restoring the account restores the role with it. So
178
+ // counting live assignees only made the guard pass on a role that was still
179
+ // held — and the delete it let through hit `admin_users_admin_role_fk`
180
+ // (`on delete restrict`) and answered 500. The operator was told the role
181
+ // was unused, confirmed, and got a server error.
182
+ //
183
+ // The two populations are still counted apart, because the remedy differs:
184
+ // reassign a live admin, purge or restore-and-reassign a deleted one. Each
185
+ // refusal carries its token in `details.code` — that is what keys the
186
+ // sentence — and its count, which the sentence interpolates.
187
+ const assignees = await this.adminUsers.listByRoleId(role.id);
188
+ const live = assignees.filter((admin) => admin.deletedAt === null).length;
189
+ const refusal = roleInUseRefusal(live, assignees.length - live);
190
+ if (refusal)
191
+ throw refusal;
192
+ this.#audit(em, 'admin_role.delete', role.id, { code: role.code }, null);
193
+ await em.removeAndFlush(role);
194
+ }
195
+ /**
196
+ * Validate an explicit permission list and collapse the wildcard. A list
197
+ * containing `*` is "full access" — it normalises to exactly `['*']` and the
198
+ * catalogue check is skipped. Otherwise every code must exist in the merged
199
+ * catalogue, else a 400 VALIDATION_FAILED is raised.
200
+ *
201
+ * Issue #213 — validated against the **known** vocabulary, not the grantable
202
+ * set. Since the grantable set became presence-filtered, the two differ
203
+ * exactly while a module is off, and validating against the narrower one
204
+ * would refuse to save any role that still holds a switched-off module's
205
+ * code. Off is non-destructive and reversible; a 400 on an unrelated rename
206
+ * is neither. See `PermissionCatalogueService.listKnownCodes`.
207
+ */
208
+ #normalizePermissions(permissions) {
209
+ if (permissions.includes(WILDCARD))
210
+ return [WILDCARD];
211
+ const known = new Set(this.permissionCatalogue.listKnownCodes());
212
+ const invalid = permissions.filter((p) => !known.has(p));
213
+ if (invalid.length > 0) {
214
+ throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, `Unknown permission(s): ${invalid.join(', ')}`);
215
+ }
216
+ return permissions;
217
+ }
218
+ }
219
+ //# sourceMappingURL=admin-role-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-role-service.js","sourceRoot":"","sources":["../../../src/backend/services/admin-role-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ import { type ModuleManifest, type PermissionCatalogueEntry } from '@endora-commerce/contracts';
2
+ export interface PermissionCatalogueServiceOptions {
3
+ registryEntries: ReadonlyArray<{
4
+ manifest: ModuleManifest;
5
+ }>;
6
+ /**
7
+ * **The effective presence of a module — both axes, not one** (issue #213).
8
+ *
9
+ * This used to be `getEnabledModuleIds`, wired by both composition roots to
10
+ * `registryCache.enabledIds()`. That set is the **platform** axis alone: an
11
+ * operator deactivation leaves the id in it, so every one of the 65 modules
12
+ * kept contributing its permission codes to `/admin-roles` after the operator
13
+ * switched it off — the exact conflation Constitution XVII exists to prevent,
14
+ * shipped.
15
+ *
16
+ * The catalogue answers "may an operator grant this today?", and presence is
17
+ * the conjunction of platform availability and operator activation, so this
18
+ * predicate is `effectiveState.isPresent` and nothing narrower. It is spelled
19
+ * out here rather than left to the wiring because the two axes are the thing
20
+ * people conflate, and a predicate named after one of them invites it back.
21
+ *
22
+ * **The two off states are not distinguished on this surface, deliberately.**
23
+ * Principle XVII item 5 asks a platform-*unavailable* module to render as
24
+ * absent or blocked-with-a-reason rather than merely "switched off" — that
25
+ * distinction is about a module's *own* surfaces, where an operator can act on
26
+ * the reason. `/admin-roles` renders a flat list of grantable codes: it has no
27
+ * per-module row to carry a reason, and no action an operator could take from
28
+ * there differs between the two cases. Absent is the whole answer. The screen
29
+ * that owes the distinction is `/platform/modules`, and it renders it.
30
+ *
31
+ * Omitted ⇒ every registered module counts as present. That is the
32
+ * pre-composition path (the CI helper below, unit tests), not a fall-open: a
33
+ * presence read before the registry cache is loaded throws
34
+ * `ModulePresenceNotLoadedError` rather than answering, so an unwired
35
+ * catalogue cannot be mistaken for a loaded one that found nothing.
36
+ */
37
+ isModulePresent?: (moduleId: string) => boolean;
38
+ }
39
+ /**
40
+ * Merges the core `PERMISSION_CATALOGUE` with the `permissions` arrays of every
41
+ * registered module manifest. Powers `GET /admin/permissions` and role upsert
42
+ * validation (feature 026).
43
+ *
44
+ * **There is no memo, and that is the design** (issue #213). `listAssignable()`
45
+ * cached its result into a field that only an explicit `invalidate()` cleared,
46
+ * and the two things that had to call it — a Redis pub/sub listener in each
47
+ * composition root — were a duplicated wiring step rather than a property of the
48
+ * service. That is the shape of issue #33 (an admin "clear cache" action that
49
+ * missed the per-process LRU) and issue #45 (an invalidation that depended on
50
+ * subscriber registration order) for the third time. Now that the value tracks a
51
+ * Setting an operator flips from the Admin UI at runtime, a per-process memo can
52
+ * only be wrong between the flip and whatever drops it.
53
+ *
54
+ * Nothing is lost by dropping it: every input is already in memory — the
55
+ * manifests are fixed at composition and the presence predicate reads the
56
+ * registry cache, which does its own refreshing — so a call is a map build and a
57
+ * sort over a few hundred rows, on two admin-only paths. There is no ordering
58
+ * trap to test because there is no invalidation to order.
59
+ */
60
+ export declare class PermissionCatalogueService {
61
+ #private;
62
+ private readonly options;
63
+ private readonly isModulePresent;
64
+ constructor(options: PermissionCatalogueServiceOptions);
65
+ /**
66
+ * The grantable set: what `/admin-roles` renders and what an operator may
67
+ * newly grant. Presence-filtered, so a module that is off contributes nothing
68
+ * (Constitution XVII item 5).
69
+ */
70
+ listAssignable(): PermissionCatalogueEntry[];
71
+ listAssignableCodes(): string[];
72
+ /**
73
+ * Every code the platform knows, present or not — the **vocabulary**, not the
74
+ * grantable set.
75
+ *
76
+ * Role upsert validates against this rather than against `listAssignable()`,
77
+ * and the difference is what keeps "off is non-destructive and reversible"
78
+ * true. An "Editor" role holds `blog.read`; the operator switches `blog` off
79
+ * and then renames the role. Validating the submitted list against the
80
+ * grantable set would answer 400 `Unknown permission(s): blog.read` — the
81
+ * operator either loses the role or silently drops a grant that must come back
82
+ * when `blog` does. A code of an absent module grants access to nothing anyway:
83
+ * the routes behind it answer 503 `MODULE_DISABLED` at their own seam.
84
+ *
85
+ * This is what the role editor already relies on: `AdminRolesPage` seeds its
86
+ * selection from the **role's** codes and renders a checkbox only for the ones
87
+ * the catalogue offers, so an absent module's grant is carried through the
88
+ * round trip untouched and simply has no checkbox to un-tick while the module
89
+ * is off. Narrowing this method would turn that into a silent revocation.
90
+ */
91
+ listKnownCodes(): string[];
92
+ /**
93
+ * Every code the platform knows, mapped to the modules whose presence keeps
94
+ * it grantable — {@link MergedRow.owners}, published.
95
+ *
96
+ * D-173's sweep reads it, and it is a method rather than a derivation of its
97
+ * own because the whole question is *"does `/admin-roles` still offer this
98
+ * code when its owner is switched off?"* — a question only the merge this
99
+ * class performs can answer. A second implementation over
100
+ * `PERMISSION_CATALOGUE` plus the manifests would be right on the day it was
101
+ * written and would drift the first time the merge learns a rule: a shared
102
+ * code with two owners, or a catalogue row a manifest re-declares, is exactly
103
+ * where the two would disagree, and exactly the shape the sweep exists for.
104
+ */
105
+ listOwnersByCode(): Map<string, ReadonlySet<string>>;
106
+ /**
107
+ * The codes `moduleId` contributes, whether or not it is present.
108
+ *
109
+ * Exists for the off-state harness: proving a module's codes leave the
110
+ * grantable set needs to know which codes are its, and deriving that from the
111
+ * `module` display field would silently measure nothing for `_lifecycle`.
112
+ */
113
+ listOwnedCodes(moduleId: string): string[];
114
+ /**
115
+ * Every declared requirement, as `code -> the codes a role holding it needs`
116
+ * (D-175, feature 080 T057).
117
+ *
118
+ * The **vocabulary's** view, not the grantable set's: the advisory has to
119
+ * survive its target's owner being switched off, or it would go quiet at
120
+ * exactly the moment an operator most needs to know why a screen came back
121
+ * incomplete. Empty requirement sets are omitted, so `size` is the number of
122
+ * codes that declare one.
123
+ */
124
+ listRequirementsByCode(): Map<string, readonly string[]>;
125
+ }
126
+ /** Test / CI helper — build assignable codes without wiring composition. */
127
+ export declare function listAssignablePermissionCodes(registryEntries: ReadonlyArray<{
128
+ manifest: ModuleManifest;
129
+ }>, presentModuleIds?: readonly string[]): string[];
130
+ //# sourceMappingURL=permission-catalogue.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-catalogue.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/permission-catalogue.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;AAEpC,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,aAAa,CAAC;QAAE,QAAQ,EAAE,cAAc,CAAA;KAAE,CAAC,CAAC;IAC7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;CACjD;AAqBD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,0BAA0B;;IAGzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgC;gBAEnC,OAAO,EAAE,iCAAiC;IAIvE;;;;OAIG;IACH,cAAc,IAAI,wBAAwB,EAAE;IAM5C,mBAAmB,IAAI,MAAM,EAAE;IAI/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,cAAc,IAAI,MAAM,EAAE;IAI1B;;;;;;;;;;;;OAYG;IACH,gBAAgB,IAAI,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAIpD;;;;;;OAMG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE;IAwE1C;;;;;;;;;OASG;IACH,sBAAsB,IAAI,GAAG,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CAOzD;AAED,4EAA4E;AAC5E,wBAAgB,6BAA6B,CAC3C,eAAe,EAAE,aAAa,CAAC;IAAE,QAAQ,EAAE,cAAc,CAAA;CAAE,CAAC,EAC5D,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,GACnC,MAAM,EAAE,CAMV"}