@headless-lms/core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/events.d.ts +39 -21
- package/dist/assets/events.js +1 -1
- package/dist/assets/events.js.map +1 -1
- package/dist/assets/file-urls.d.ts +20 -0
- package/dist/assets/file-urls.js +25 -0
- package/dist/assets/file-urls.js.map +1 -0
- package/dist/assets/index.d.ts +5 -4
- package/dist/assets/index.js +3 -2
- package/dist/assets/model.d.ts +2 -2
- package/dist/assets/ports.d.ts +45 -15
- package/dist/assets/service.d.ts +39 -7
- package/dist/assets/service.js +291 -38
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.d.ts +13 -24
- package/dist/automations/actions.js +168 -43
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +2 -1
- package/dist/automations/catalog.js +229 -13
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.d.ts +14 -0
- package/dist/automations/community-derivations.js +439 -0
- package/dist/automations/community-derivations.js.map +1 -0
- package/dist/automations/community-mail.d.ts +112 -0
- package/dist/automations/community-mail.js +90 -0
- package/dist/automations/community-mail.js.map +1 -0
- package/dist/automations/community-notifications.d.ts +16 -0
- package/dist/automations/community-notifications.js +84 -0
- package/dist/automations/community-notifications.js.map +1 -0
- package/dist/automations/events.d.ts +28 -0
- package/dist/automations/index.d.ts +4 -2
- package/dist/automations/index.js +3 -2
- package/dist/automations/model.d.ts +10 -3
- package/dist/automations/model.js +15 -2
- package/dist/automations/model.js.map +1 -1
- package/dist/automations/ports.d.ts +5 -1
- package/dist/automations/schedule.d.ts +6 -0
- package/dist/automations/schedule.js +41 -0
- package/dist/automations/schedule.js.map +1 -0
- package/dist/automations/seeds.d.ts +17 -0
- package/dist/automations/seeds.js +235 -0
- package/dist/automations/seeds.js.map +1 -0
- package/dist/automations/service.d.ts +22 -3
- package/dist/automations/service.js +306 -43
- package/dist/automations/service.js.map +1 -1
- package/dist/content/events.d.ts +631 -19
- package/dist/content/events.js +44 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +4 -4
- package/dist/content/model.d.ts +2 -2
- package/dist/content/ports.d.ts +71 -18
- package/dist/content/service.d.ts +57 -18
- package/dist/content/service.js +502 -23
- package/dist/content/service.js.map +1 -1
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +10 -0
- package/dist/discussion/authors.js +23 -0
- package/dist/discussion/authors.js.map +1 -0
- package/dist/discussion/chats.d.ts +104 -0
- package/dist/discussion/chats.js +798 -0
- package/dist/discussion/chats.js.map +1 -0
- package/dist/discussion/events.d.ts +619 -56
- package/dist/discussion/events.js +106 -3
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +7 -3
- package/dist/discussion/index.js +4 -1
- package/dist/discussion/ports.d.ts +288 -27
- package/dist/discussion/ports.js +6 -1
- package/dist/discussion/ports.js.map +1 -0
- package/dist/discussion/posts.d.ts +103 -0
- package/dist/discussion/posts.js +723 -0
- package/dist/discussion/posts.js.map +1 -0
- package/dist/discussion/service.d.ts +32 -10
- package/dist/discussion/service.js +293 -78
- package/dist/discussion/service.js.map +1 -1
- package/dist/entitlements/events.d.ts +178 -11
- package/dist/entitlements/events.js +28 -1
- package/dist/entitlements/events.js.map +1 -1
- package/dist/entitlements/index.d.ts +5 -4
- package/dist/entitlements/index.js +2 -1
- package/dist/entitlements/model.d.ts +3 -2
- package/dist/entitlements/ports.d.ts +124 -11
- package/dist/entitlements/rules.d.ts +12 -0
- package/dist/entitlements/rules.js +17 -0
- package/dist/entitlements/rules.js.map +1 -0
- package/dist/entitlements/service.d.ts +54 -3
- package/dist/entitlements/service.js +472 -2
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/access.d.ts +6 -0
- package/dist/identity/access.js +7 -0
- package/dist/identity/access.js.map +1 -0
- package/dist/identity/index.d.ts +6 -4
- package/dist/identity/index.js +3 -1
- package/dist/identity/model.d.ts +2 -1
- package/dist/identity/notifications.d.ts +27 -0
- package/dist/identity/notifications.js +101 -0
- package/dist/identity/notifications.js.map +1 -0
- package/dist/identity/ports.d.ts +73 -3
- package/dist/identity/service.d.ts +14 -3
- package/dist/identity/service.js +62 -5
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/model.js +1 -1
- package/dist/integrations/model.js.map +1 -1
- package/dist/integrations/service.js +2 -2
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/access-scopes.d.ts +19 -0
- package/dist/organizations/access-scopes.js +89 -0
- package/dist/organizations/access-scopes.js.map +1 -0
- package/dist/organizations/events.d.ts +74 -32
- package/dist/organizations/events.js +6 -0
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +7 -6
- package/dist/organizations/index.js +4 -3
- package/dist/organizations/members.d.ts +8 -5
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/ports.d.ts +14 -11
- package/dist/organizations/roles.d.ts +6 -10
- package/dist/organizations/roles.js +19 -7
- package/dist/organizations/roles.js.map +1 -1
- package/dist/organizations/service.d.ts +20 -11
- package/dist/organizations/service.js +157 -47
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +1 -10
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +6 -1
- package/dist/progress/service.d.ts +3 -1
- package/dist/progress/service.js +6 -2
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +4 -0
- package/dist/reporting/admin/index.js +2 -0
- package/dist/reporting/admin/model.d.ts +108 -0
- package/dist/reporting/admin/ports.d.ts +27 -0
- package/dist/reporting/admin/service.d.ts +22 -0
- package/dist/reporting/admin/service.js +38 -0
- package/dist/reporting/admin/service.js.map +1 -0
- package/dist/reporting/member/index.d.ts +5 -0
- package/dist/reporting/member/index.js +2 -0
- package/dist/reporting/member/model.d.ts +124 -0
- package/dist/reporting/member/ports.d.ts +45 -0
- package/dist/reporting/member/service.d.ts +73 -0
- package/dist/reporting/member/service.js +286 -0
- package/dist/reporting/member/service.js.map +1 -0
- package/dist/shared/actor.d.ts +9 -0
- package/dist/shared/errors.d.ts +7 -1
- package/dist/shared/errors.js +10 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +11 -0
- package/dist/shared/id.js +12 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +4 -1
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/network.d.ts +10 -0
- package/dist/shared/network.js +211 -0
- package/dist/shared/network.js.map +1 -0
- package/dist/types/assets.d.ts +2 -2
- package/dist/types/automations.d.ts +10 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +11 -3
- package/dist/types/email-templates.d.ts +121 -0
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +48 -2
- package/dist/types/index.d.ts +9 -9
- package/dist/types/ports.d.ts +6 -0
- package/dist/types/schemas/assets.d.ts +65 -37
- package/dist/types/schemas/assets.js +17 -12
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +18 -1
- package/dist/types/schemas/automations.js +15 -6
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +335 -3
- package/dist/types/schemas/content.js +139 -3
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +370 -3
- package/dist/types/schemas/discussion.js +199 -4
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +36 -2
- package/dist/types/schemas/entitlements.js +23 -1
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +111 -1
- package/dist/types/schemas/identity.js +55 -2
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +8 -8
- package/dist/types/schemas/index.js +8 -8
- package/dist/types/schemas/organizations.d.ts +8 -0
- package/dist/types/schemas/organizations.js +2 -0
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/shared.d.ts +3 -1
- package/dist/types/schemas/shared.js +5 -1
- package/dist/types/schemas/shared.js.map +1 -1
- package/package.json +2 -2
- package/dist/reporting/courses/index.d.ts +0 -4
- package/dist/reporting/courses/index.js +0 -2
- package/dist/reporting/courses/model.d.ts +0 -24
- package/dist/reporting/courses/ports.d.ts +0 -13
- package/dist/reporting/courses/service.d.ts +0 -19
- package/dist/reporting/courses/service.js +0 -20
- package/dist/reporting/courses/service.js.map +0 -1
- package/dist/reporting/dashboard/index.d.ts +0 -4
- package/dist/reporting/dashboard/index.js +0 -2
- package/dist/reporting/dashboard/model.d.ts +0 -14
- package/dist/reporting/dashboard/ports.d.ts +0 -13
- package/dist/reporting/dashboard/service.d.ts +0 -19
- package/dist/reporting/dashboard/service.js +0 -20
- package/dist/reporting/dashboard/service.js.map +0 -1
- package/dist/reporting/learn/index.d.ts +0 -5
- package/dist/reporting/learn/index.js +0 -2
- package/dist/reporting/learn/model.d.ts +0 -23
- package/dist/reporting/learn/ports.d.ts +0 -43
- package/dist/reporting/learn/ports.js +0 -1
- package/dist/reporting/learn/service.d.ts +0 -49
- package/dist/reporting/learn/service.js +0 -116
- package/dist/reporting/learn/service.js.map +0 -1
- package/dist/reporting/students/index.d.ts +0 -4
- package/dist/reporting/students/index.js +0 -2
- package/dist/reporting/students/model.d.ts +0 -51
- package/dist/reporting/students/model.js +0 -1
- package/dist/reporting/students/ports.d.ts +0 -15
- package/dist/reporting/students/ports.js +0 -1
- package/dist/reporting/students/service.d.ts +0 -20
- package/dist/reporting/students/service.js +0 -23
- package/dist/reporting/students/service.js.map +0 -1
- /package/dist/reporting/{courses → admin}/model.js +0 -0
- /package/dist/reporting/{courses → admin}/ports.js +0 -0
- /package/dist/reporting/{dashboard → member}/model.js +0 -0
- /package/dist/reporting/{dashboard → member}/ports.js +0 -0
- /package/dist/{reporting/learn/model.js → shared/actor.js} +0 -0
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
import { Role } from "../types/schemas/organizations.js";
|
|
2
2
|
import "../types/index.js";
|
|
3
3
|
//#region src/organizations/roles.d.ts
|
|
4
|
-
declare const ROLES: readonly ["owner", "admin", "instructor", "student"];
|
|
5
|
-
|
|
6
|
-
* The roles that operate the back office. `student` belongs to the org but
|
|
7
|
-
* never appears on a staff surface, so the member-management types are typed
|
|
8
|
-
* with this narrower union — the exclusion is a compiler guarantee, not a
|
|
9
|
-
* convention a query has to remember.
|
|
10
|
-
*/
|
|
11
|
-
type StaffRole = Exclude<Role, 'student'>;
|
|
4
|
+
declare const ROLES: readonly ["owner", "admin", "instructor", "moderator", "student"];
|
|
5
|
+
type StaffRole = 'owner' | 'admin' | 'instructor';
|
|
12
6
|
declare const STAFF_ROLES: readonly ["owner", "admin", "instructor"];
|
|
7
|
+
type TeamRole = Exclude<Role, 'student'>;
|
|
13
8
|
declare function isRole(value: string): value is Role;
|
|
14
9
|
declare function isStaffRole(value: string): value is StaffRole;
|
|
15
10
|
declare function parseRole(value: string): Role;
|
|
16
|
-
type Permission = 'manage_billing' | 'manage_org_settings' | 'manage_users' | 'create_course' | 'edit_assigned_course' | 'view_student_progress' | 'consume_content';
|
|
11
|
+
type Permission = 'manage_billing' | 'manage_org_settings' | 'manage_users' | 'create_course' | 'edit_assigned_course' | 'view_student_progress' | 'moderate' | 'consume_content';
|
|
17
12
|
type Capability = true | 'assigned' | 'enrolled';
|
|
18
13
|
declare function capability(role: Role, permission: Permission): Capability | false;
|
|
14
|
+
declare function canModerate(role: Role): boolean;
|
|
19
15
|
declare function normalizeRole(raw: string): Role;
|
|
20
16
|
declare const STUDENT_ROLE = "student";
|
|
21
17
|
//#endregion
|
|
22
|
-
export { Capability, Permission, ROLES, type Role, STAFF_ROLES, STUDENT_ROLE, StaffRole, capability, isRole, isStaffRole, normalizeRole, parseRole };
|
|
18
|
+
export { Capability, Permission, ROLES, type Role, STAFF_ROLES, STUDENT_ROLE, StaffRole, TeamRole, canModerate, capability, isRole, isStaffRole, normalizeRole, parseRole };
|
|
23
19
|
//# sourceMappingURL=roles.d.ts.map
|
|
@@ -3,6 +3,7 @@ const ROLES = [
|
|
|
3
3
|
"owner",
|
|
4
4
|
"admin",
|
|
5
5
|
"instructor",
|
|
6
|
+
"moderator",
|
|
6
7
|
"student"
|
|
7
8
|
];
|
|
8
9
|
const STAFF_ROLES = [
|
|
@@ -27,28 +28,39 @@ const MATRIX = {
|
|
|
27
28
|
manage_users: true,
|
|
28
29
|
create_course: true,
|
|
29
30
|
edit_assigned_course: true,
|
|
30
|
-
view_student_progress: true
|
|
31
|
+
view_student_progress: true,
|
|
32
|
+
moderate: true
|
|
31
33
|
},
|
|
32
34
|
admin: {
|
|
33
35
|
manage_org_settings: true,
|
|
34
36
|
manage_users: true,
|
|
35
37
|
create_course: true,
|
|
36
38
|
edit_assigned_course: true,
|
|
37
|
-
view_student_progress: true
|
|
39
|
+
view_student_progress: true,
|
|
40
|
+
moderate: true
|
|
38
41
|
},
|
|
39
42
|
instructor: {
|
|
40
43
|
edit_assigned_course: "assigned",
|
|
41
|
-
view_student_progress: "assigned"
|
|
44
|
+
view_student_progress: "assigned",
|
|
45
|
+
moderate: true
|
|
46
|
+
},
|
|
47
|
+
moderator: {
|
|
48
|
+
moderate: true,
|
|
49
|
+
consume_content: "enrolled"
|
|
42
50
|
},
|
|
43
51
|
student: { consume_content: "enrolled" }
|
|
44
52
|
};
|
|
45
53
|
function capability(role, permission) {
|
|
46
54
|
return MATRIX[role][permission] ?? false;
|
|
47
55
|
}
|
|
56
|
+
function canModerate(role) {
|
|
57
|
+
return capability(role, "moderate") === true;
|
|
58
|
+
}
|
|
48
59
|
const RANK = {
|
|
49
|
-
owner:
|
|
50
|
-
admin:
|
|
51
|
-
instructor:
|
|
60
|
+
owner: 4,
|
|
61
|
+
admin: 3,
|
|
62
|
+
instructor: 2,
|
|
63
|
+
moderator: 1,
|
|
52
64
|
student: 0
|
|
53
65
|
};
|
|
54
66
|
function normalizeRole(raw) {
|
|
@@ -65,6 +77,6 @@ function normalizeRole(raw) {
|
|
|
65
77
|
}
|
|
66
78
|
const STUDENT_ROLE = "student";
|
|
67
79
|
//#endregion
|
|
68
|
-
export { ROLES, STAFF_ROLES, STUDENT_ROLE, capability, isRole, isStaffRole, normalizeRole, parseRole };
|
|
80
|
+
export { ROLES, STAFF_ROLES, STUDENT_ROLE, canModerate, capability, isRole, isStaffRole, normalizeRole, parseRole };
|
|
69
81
|
|
|
70
82
|
//# sourceMappingURL=roles.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"roles.js","names":[],"sources":["../../src/organizations/roles.ts"],"sourcesContent":["// organizations context — roles and the authorization matrix.\n// Defined in code (no DB enum). The DB stores role as text; the domain narrows\n// it to Role and answers authorization questions here. The Role type itself is\n// owned by @headless-lms/core/types; RANK/MATRIX below are Record<Role, …>, so the\n// compiler forces this file to cover every role the published type declares.\nimport type { Role } from '../types/index.js';\n\nexport type { Role };\nexport const ROLES = ['owner'
|
|
1
|
+
{"version":3,"file":"roles.js","names":[],"sources":["../../src/organizations/roles.ts"],"sourcesContent":["// organizations context — roles and the authorization matrix.\n// Defined in code (no DB enum). The DB stores role as text; the domain narrows\n// it to Role and answers authorization questions here. The Role type itself is\n// owned by @headless-lms/core/types; RANK/MATRIX below are Record<Role, …>, so the\n// compiler forces this file to cover every role the published type declares.\nimport type { Role } from '../types/index.js';\n\nexport type { Role };\nexport const ROLES = [\n 'owner',\n 'admin',\n 'instructor',\n 'moderator',\n 'student',\n] as const satisfies readonly Role[];\n\nexport type StaffRole = 'owner' | 'admin' | 'instructor';\nexport const STAFF_ROLES = ['owner', 'admin', 'instructor'] as const satisfies readonly StaffRole[];\n\nexport type TeamRole = Exclude<Role, 'student'>;\n\nexport function isRole(value: string): value is Role {\n return (ROLES as readonly string[]).includes(value);\n}\n\nexport function isStaffRole(value: string): value is StaffRole {\n return (STAFF_ROLES as readonly string[]).includes(value);\n}\n\nexport function parseRole(value: string): Role {\n if (!isRole(value)) {\n throw new Error(`unknown role: ${value}`);\n }\n return value;\n}\n\nexport type Permission =\n | 'manage_billing'\n | 'manage_org_settings'\n | 'manage_users'\n | 'create_course'\n | 'edit_assigned_course'\n | 'view_student_progress'\n | 'moderate'\n | 'consume_content';\n\n// Unconditional (true), course-scoped (\"assigned\"), or enrollment-scoped\n// (\"enrolled\" — access owned by entitlements). Absent ⇒ denied.\n//\n// \"assigned\" is a granted-but-narrowed answer: the role holds the permission,\n// but only over some subset of courses. Nothing narrows it today — there is no\n// course-assignment store — so callers must decide what a narrowed grant means\n// for them. `authorize` treats it as denied; `tools.ts` treats it as granted.\nexport type Capability = true | 'assigned' | 'enrolled';\n\nconst MATRIX: Record<Role, Partial<Record<Permission, Capability>>> = {\n owner: {\n manage_billing: true,\n manage_org_settings: true,\n manage_users: true,\n create_course: true,\n edit_assigned_course: true,\n view_student_progress: true,\n moderate: true,\n },\n admin: {\n manage_org_settings: true,\n manage_users: true,\n create_course: true,\n edit_assigned_course: true,\n view_student_progress: true,\n moderate: true,\n },\n instructor: {\n edit_assigned_course: 'assigned',\n view_student_progress: 'assigned',\n moderate: true,\n },\n moderator: {\n moderate: true,\n consume_content: 'enrolled',\n },\n student: {\n consume_content: 'enrolled',\n },\n};\n\nexport function capability(role: Role, permission: Permission): Capability | false {\n return MATRIX[role][permission] ?? false;\n}\n\nexport function canModerate(role: Role): boolean {\n return capability(role, 'moderate') === true;\n}\n\n// Better Auth's role model is broader than ours: it keeps a built-in `member`\n// role and allows comma-joined multi-role strings. Normalize any incoming role\n// string to a single domain Role before persisting: `member` -> `instructor`, a\n// multi-role string -> its highest-privilege known role, unknown -> `instructor`.\nconst RANK: Record<Role, number> = { owner: 4, admin: 3, instructor: 2, moderator: 1, student: 0 };\n\nexport function normalizeRole(raw: string): Role {\n let best: Role = 'instructor';\n let bestRank = -1;\n for (const token of raw.split(',').map((t) => t.trim())) {\n const r: Role | null = isRole(token) ? token : token === 'member' ? 'instructor' : null;\n if (r && RANK[r] > bestRank) {\n best = r;\n bestRank = RANK[r];\n }\n }\n return best;\n}\n\n// Invites carry either a staff Role or this marker: the invited account is\n// a portal student, not an org member. Owned by the organizations domain — the\n// invite provider adapter reuses it to branch links, emails, and grants.\nexport const STUDENT_ROLE = 'student';\n"],"mappings":";AAQA,MAAa,QAAQ;CACnB;CACA;CACA;CACA;CACA;AACF;AAGA,MAAa,cAAc;CAAC;CAAS;CAAS;AAAY;AAI1D,SAAgB,OAAO,OAA8B;CACnD,OAAQ,MAA4B,SAAS,KAAK;AACpD;AAEA,SAAgB,YAAY,OAAmC;CAC7D,OAAQ,YAAkC,SAAS,KAAK;AAC1D;AAEA,SAAgB,UAAU,OAAqB;CAC7C,IAAI,CAAC,OAAO,KAAK,GACf,MAAM,IAAI,MAAM,iBAAiB,OAAO;CAE1C,OAAO;AACT;AAqBA,MAAM,SAAgE;CACpE,OAAO;EACL,gBAAgB;EAChB,qBAAqB;EACrB,cAAc;EACd,eAAe;EACf,sBAAsB;EACtB,uBAAuB;EACvB,UAAU;CACZ;CACA,OAAO;EACL,qBAAqB;EACrB,cAAc;EACd,eAAe;EACf,sBAAsB;EACtB,uBAAuB;EACvB,UAAU;CACZ;CACA,YAAY;EACV,sBAAsB;EACtB,uBAAuB;EACvB,UAAU;CACZ;CACA,WAAW;EACT,UAAU;EACV,iBAAiB;CACnB;CACA,SAAS,EACP,iBAAiB,WACnB;AACF;AAEA,SAAgB,WAAW,MAAY,YAA4C;CACjF,OAAO,OAAO,KAAK,CAAC,eAAe;AACrC;AAEA,SAAgB,YAAY,MAAqB;CAC/C,OAAO,WAAW,MAAM,UAAU,MAAM;AAC1C;AAMA,MAAM,OAA6B;CAAE,OAAO;CAAG,OAAO;CAAG,YAAY;CAAG,WAAW;CAAG,SAAS;AAAE;AAEjG,SAAgB,cAAc,KAAmB;CAC/C,IAAI,OAAa;CACjB,IAAI,WAAW;CACf,KAAK,MAAM,SAAS,IAAI,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,GAAG;EACvD,MAAM,IAAiB,OAAO,KAAK,IAAI,QAAQ,UAAU,WAAW,eAAe;EACnF,IAAI,KAAK,KAAK,KAAK,UAAU;GAC3B,OAAO;GACP,WAAW,KAAK;EAClB;CACF;CACA,OAAO;AACT;AAKA,MAAa,eAAe"}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Logger } from "../types/ports.js";
|
|
2
2
|
import { AcceptInviteInput, AddOrgUserInput, CreateInviteInput, Invite, OrgUser, Organization, Role, UpdateOrgUserInput } from "../types/schemas/organizations.js";
|
|
3
3
|
import "../shared/ports.js";
|
|
4
|
-
import {
|
|
5
|
-
import { IdentityService } from "../identity/ports.js";
|
|
6
|
-
import "../identity/index.js";
|
|
4
|
+
import { Actor } from "../shared/actor.js";
|
|
7
5
|
import "./model.js";
|
|
8
6
|
import "./roles.js";
|
|
9
|
-
import { Member, MembersQuery, Page } from "./members.js";
|
|
10
|
-
import { CreateOrganizationInput, ResendStudentInviteInput, UpdateOrganizationInput
|
|
7
|
+
import { Member, MemberRoleChange, MembersQuery, Page } from "./members.js";
|
|
8
|
+
import { CreateOrganizationInput, ResendStudentInviteInput, UpdateOrganizationInput } from "./types.js";
|
|
9
|
+
import { IdentityService } from "../identity/ports.js";
|
|
10
|
+
import { Mailer } from "../shared/mailer.js";
|
|
11
|
+
import "../identity/index.js";
|
|
11
12
|
import { MemberWriteContext, MembersRepository, OrgAdmin, OrganizationService, OrganizationsRepository, OrganizationsUnitOfWork } from "./ports.js";
|
|
12
13
|
//#region src/organizations/service.d.ts
|
|
13
14
|
interface InviteUrls {
|
|
@@ -33,6 +34,7 @@ declare class OrganizationServiceImpl implements OrganizationService {
|
|
|
33
34
|
private readonly logger;
|
|
34
35
|
private readonly mailer?;
|
|
35
36
|
private readonly inviteUrls?;
|
|
37
|
+
private readonly memberWrites;
|
|
36
38
|
constructor(params: OrganizationServiceParams);
|
|
37
39
|
createOrganization(input: CreateOrganizationInput): Promise<Organization>;
|
|
38
40
|
updateOrganization(id: string, input: UpdateOrganizationInput): Promise<Organization>;
|
|
@@ -40,23 +42,30 @@ declare class OrganizationServiceImpl implements OrganizationService {
|
|
|
40
42
|
addOrgUser({ orgId, userId, role }: AddOrgUserInput): Promise<OrgUser>;
|
|
41
43
|
removeOrgUser(orgId: string, userId: string): Promise<OrgUser>;
|
|
42
44
|
updateOrgUser(orgId: string, userId: string, patch: UpdateOrgUserInput): Promise<OrgUser>;
|
|
43
|
-
createInvite(input: CreateInviteInput): Promise<Invite>;
|
|
44
|
-
|
|
45
|
-
resendStudentInvite(input: ResendStudentInviteInput): Promise<void>;
|
|
45
|
+
createInvite(actor: Actor, input: CreateInviteInput): Promise<Invite>;
|
|
46
|
+
resendStudentInvite(actor: Actor, input: ResendStudentInviteInput): Promise<void>;
|
|
46
47
|
peekInvite(token: string): Promise<Invite | null>;
|
|
47
48
|
acceptInvite(input: AcceptInviteInput): Promise<OrgUser>;
|
|
48
49
|
private acceptStudentInvite;
|
|
49
50
|
private acceptStaffInvite;
|
|
51
|
+
getOrgUserById(orgId: string, id: string): Promise<OrgUser | null>;
|
|
50
52
|
getOrgUser(orgId: string, userId: string): Promise<OrgUser | null>;
|
|
51
|
-
deleteOrgUser(orgId: string, id: string): Promise<OrgUser>;
|
|
53
|
+
deleteOrgUser(orgId: string, actor: Actor, id: string): Promise<OrgUser>;
|
|
52
54
|
getOrgUsersForUser(userId: string): Promise<OrgUser[]>;
|
|
53
55
|
getById(id: string): Promise<Organization | null>;
|
|
54
56
|
private sendInviteEmail;
|
|
55
57
|
getByExternalId(externalId: string): Promise<Organization | null>;
|
|
56
58
|
getBySlug(slug: string): Promise<Organization | null>;
|
|
59
|
+
staffOrgUserIds(orgId: string): Promise<string[]>;
|
|
57
60
|
listMembers(orgId: string, query: MembersQuery): Promise<Page<Member>>;
|
|
58
|
-
updateMemberRole(ctx: MemberWriteContext, id: string, role: Role): Promise<
|
|
59
|
-
|
|
61
|
+
updateMemberRole(ctx: MemberWriteContext, actor: Actor, id: string, role: Role): Promise<MemberRoleChange | null>;
|
|
62
|
+
private demoteModerator;
|
|
63
|
+
private promoteStudent;
|
|
64
|
+
private asActor;
|
|
65
|
+
private memberWrite;
|
|
66
|
+
private assertManagesUsers;
|
|
67
|
+
private assertNotOwner;
|
|
68
|
+
removeMember(ctx: MemberWriteContext, actor: Actor, id: string): Promise<boolean>;
|
|
60
69
|
}
|
|
61
70
|
//#endregion
|
|
62
71
|
export { InviteUrls, OrganizationServiceImpl, OrganizationServiceParams };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { noopLogger } from "../shared/logger.js";
|
|
2
1
|
import { genId } from "../shared/id.js";
|
|
3
|
-
import { NotFoundError } from "../shared/errors.js";
|
|
4
|
-
import {
|
|
2
|
+
import { ForbiddenError, NotFoundError } from "../shared/errors.js";
|
|
3
|
+
import { noopLogger } from "../shared/logger.js";
|
|
4
|
+
import { STAFF_ROLES, STUDENT_ROLE, capability } from "./roles.js";
|
|
5
5
|
import { InviteError, OrganizationRuleError } from "./members.js";
|
|
6
6
|
import { organizationEvents } from "./events.js";
|
|
7
7
|
import { generateInviteToken, hashInviteToken } from "../shared/invite-token.js";
|
|
8
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
9
|
//#region src/organizations/service.ts
|
|
9
10
|
const INVITE_TTL_MS = 10080 * 60 * 1e3;
|
|
10
11
|
function toMember(r) {
|
|
@@ -29,6 +30,7 @@ var OrganizationServiceImpl = class {
|
|
|
29
30
|
logger;
|
|
30
31
|
mailer;
|
|
31
32
|
inviteUrls;
|
|
33
|
+
memberWrites = new AsyncLocalStorage();
|
|
32
34
|
constructor(params) {
|
|
33
35
|
this.repo = params.repo;
|
|
34
36
|
this.membersRepo = params.membersRepo;
|
|
@@ -90,6 +92,10 @@ var OrganizationServiceImpl = class {
|
|
|
90
92
|
return deleted;
|
|
91
93
|
}
|
|
92
94
|
async addOrgUser({ orgId, userId, role }) {
|
|
95
|
+
if (await this.repo.findOrgUser(orgId, userId)) return this.updateOrgUser(orgId, userId, {
|
|
96
|
+
role,
|
|
97
|
+
status: "active"
|
|
98
|
+
});
|
|
93
99
|
const orgUser = await this.uow.run(async ({ organizations, outbox }) => {
|
|
94
100
|
const created = await organizations.createOrgUser({
|
|
95
101
|
orgId,
|
|
@@ -113,9 +119,21 @@ var OrganizationServiceImpl = class {
|
|
|
113
119
|
async removeOrgUser(orgId, userId) {
|
|
114
120
|
const deleted = await this.uow.run(async ({ organizations, outbox }) => {
|
|
115
121
|
const orgUser = await organizations.findOrgUser(orgId, userId);
|
|
116
|
-
if (!orgUser) throw new NotFoundError("
|
|
122
|
+
if (!orgUser) throw new NotFoundError("Member", userId);
|
|
123
|
+
const write = this.memberWrite(orgUser.id);
|
|
124
|
+
if (write?.demote) {
|
|
125
|
+
const demoted = await organizations.updateOrgUser(orgId, orgUser.id, { role: STUDENT_ROLE });
|
|
126
|
+
if (!demoted) throw new NotFoundError("Member", orgUser.id);
|
|
127
|
+
if (orgUser.role !== "student") await outbox.append([organizationEvents.orgUserUpdated.make({
|
|
128
|
+
orgId,
|
|
129
|
+
data: demoted,
|
|
130
|
+
metadata: { actorOrgUserId: write.actorOrgUserId }
|
|
131
|
+
})]);
|
|
132
|
+
return demoted;
|
|
133
|
+
}
|
|
134
|
+
if (orgUser.role === "student") return orgUser;
|
|
117
135
|
const deletedUser = await organizations.deleteOrgUser(orgId, orgUser.id);
|
|
118
|
-
if (!deletedUser) throw new NotFoundError("
|
|
136
|
+
if (!deletedUser) throw new NotFoundError("Member", orgUser.id);
|
|
119
137
|
await outbox.append([organizationEvents.orgUserDeleted.make({
|
|
120
138
|
orgId,
|
|
121
139
|
data: deletedUser
|
|
@@ -130,10 +148,21 @@ var OrganizationServiceImpl = class {
|
|
|
130
148
|
return deleted;
|
|
131
149
|
}
|
|
132
150
|
async updateOrgUser(orgId, userId, patch) {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
const updated = await this.uow.run(async ({ organizations, outbox }) => {
|
|
152
|
+
const orgUser = await organizations.findOrgUser(orgId, userId);
|
|
153
|
+
if (!orgUser) throw new NotFoundError("Member", userId);
|
|
154
|
+
const next = await organizations.updateOrgUser(orgId, orgUser.id, patch);
|
|
155
|
+
if (!next) throw new NotFoundError("Member", orgUser.id);
|
|
156
|
+
if (patch.role !== void 0 && patch.role !== orgUser.role) {
|
|
157
|
+
const actorOrgUserId = this.memberWrite(orgUser.id)?.actorOrgUserId;
|
|
158
|
+
await outbox.append([organizationEvents.orgUserUpdated.make({
|
|
159
|
+
orgId,
|
|
160
|
+
data: next,
|
|
161
|
+
...actorOrgUserId !== void 0 && { metadata: { actorOrgUserId } }
|
|
162
|
+
})]);
|
|
163
|
+
}
|
|
164
|
+
return next;
|
|
165
|
+
});
|
|
137
166
|
this.logger.info("org user updated", {
|
|
138
167
|
orgId,
|
|
139
168
|
userId,
|
|
@@ -142,8 +171,10 @@ var OrganizationServiceImpl = class {
|
|
|
142
171
|
});
|
|
143
172
|
return updated;
|
|
144
173
|
}
|
|
145
|
-
async createInvite(input) {
|
|
174
|
+
async createInvite(actor, input) {
|
|
146
175
|
const { orgId, email, role, inviterUserId, firstName, lastName } = input;
|
|
176
|
+
this.assertManagesUsers(actor, orgId);
|
|
177
|
+
this.assertNotOwner(role, orgId);
|
|
147
178
|
const sendEmail = input.sendEmail ?? true;
|
|
148
179
|
const { token, tokenHash } = generateInviteToken();
|
|
149
180
|
const expiresAt = new Date(Date.now() + INVITE_TTL_MS);
|
|
@@ -184,7 +215,8 @@ var OrganizationServiceImpl = class {
|
|
|
184
215
|
await this.sendInviteEmail(invite, token);
|
|
185
216
|
} catch (err) {
|
|
186
217
|
this.logger.error("failed to send invite email", {
|
|
187
|
-
|
|
218
|
+
orgId,
|
|
219
|
+
inviteId: invite.id,
|
|
188
220
|
role: invite.role,
|
|
189
221
|
err: err instanceof Error ? err : new Error(String(err))
|
|
190
222
|
});
|
|
@@ -197,29 +229,9 @@ var OrganizationServiceImpl = class {
|
|
|
197
229
|
});
|
|
198
230
|
return invite;
|
|
199
231
|
}
|
|
200
|
-
async
|
|
201
|
-
const { orgId, orgUserId, firstName, lastName, email } = input;
|
|
202
|
-
const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);
|
|
203
|
-
if (!orgUser || orgUser.role !== "student") throw new NotFoundError("Student", orgUserId);
|
|
204
|
-
const person = await this.people.getUserById(orgUser.userId);
|
|
205
|
-
if (!person) throw new NotFoundError("User", orgUser.userId);
|
|
206
|
-
const previousEmail = person.email;
|
|
207
|
-
await this.people.updateUser(person.id, {
|
|
208
|
-
...firstName !== void 0 && { firstName },
|
|
209
|
-
...lastName !== void 0 && { lastName },
|
|
210
|
-
...email !== void 0 && { email }
|
|
211
|
-
});
|
|
212
|
-
const emailChanged = email !== void 0 && email.toLowerCase() !== previousEmail.toLowerCase();
|
|
213
|
-
if (emailChanged && orgUser.status !== "active") await this.repo.cancelPendingInvite(orgId, previousEmail);
|
|
214
|
-
this.logger.info("student updated", {
|
|
215
|
-
orgId,
|
|
216
|
-
orgUserId,
|
|
217
|
-
emailChanged
|
|
218
|
-
});
|
|
219
|
-
return orgUser;
|
|
220
|
-
}
|
|
221
|
-
async resendStudentInvite(input) {
|
|
232
|
+
async resendStudentInvite(actor, input) {
|
|
222
233
|
const { orgId, orgUserId, inviterUserId } = input;
|
|
234
|
+
this.assertManagesUsers(actor, orgId);
|
|
223
235
|
const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);
|
|
224
236
|
if (!orgUser || orgUser.role !== "student") throw new NotFoundError("Student", orgUserId);
|
|
225
237
|
if (orgUser.status === "active") throw new OrganizationRuleError("That student has already accepted their invitation");
|
|
@@ -270,9 +282,9 @@ var OrganizationServiceImpl = class {
|
|
|
270
282
|
async acceptStudentInvite(invite, userId) {
|
|
271
283
|
return this.uow.run(async ({ organizations, outbox }) => {
|
|
272
284
|
const existing = await organizations.findOrgUser(invite.orgId, userId);
|
|
273
|
-
if (!existing) throw new NotFoundError("
|
|
285
|
+
if (!existing) throw new NotFoundError("Member", userId);
|
|
274
286
|
const activated = await organizations.updateOrgUser(invite.orgId, existing.id, { status: "active" });
|
|
275
|
-
if (!activated) throw new NotFoundError("
|
|
287
|
+
if (!activated) throw new NotFoundError("Member", existing.id);
|
|
276
288
|
const acceptedInvite = await organizations.setInviteStatus(invite.orgId, invite.id, "accepted");
|
|
277
289
|
if (!acceptedInvite) throw new NotFoundError("Invite", invite.id);
|
|
278
290
|
await outbox.append([organizationEvents.inviteAccepted.make({
|
|
@@ -291,7 +303,7 @@ var OrganizationServiceImpl = class {
|
|
|
291
303
|
await this.orgAdmin().grantMembership(org.externalId ?? org.id, person.externalId, invite.role);
|
|
292
304
|
return this.uow.run(async ({ organizations, outbox }) => {
|
|
293
305
|
const orgUser = await organizations.findOrgUser(invite.orgId, userId);
|
|
294
|
-
if (!orgUser) throw new NotFoundError("
|
|
306
|
+
if (!orgUser) throw new NotFoundError("Member", userId);
|
|
295
307
|
const acceptedInvite = await organizations.setInviteStatus(invite.orgId, invite.id, "accepted");
|
|
296
308
|
if (!acceptedInvite) throw new NotFoundError("Invite", invite.id);
|
|
297
309
|
await outbox.append([organizationEvents.inviteAccepted.make({
|
|
@@ -301,21 +313,23 @@ var OrganizationServiceImpl = class {
|
|
|
301
313
|
return orgUser;
|
|
302
314
|
});
|
|
303
315
|
}
|
|
316
|
+
getOrgUserById(orgId, id) {
|
|
317
|
+
return this.repo.findOrgUserById(orgId, id);
|
|
318
|
+
}
|
|
304
319
|
async getOrgUser(orgId, userId) {
|
|
305
320
|
return this.repo.findOrgUser(orgId, userId);
|
|
306
321
|
}
|
|
307
|
-
async deleteOrgUser(orgId, id) {
|
|
322
|
+
async deleteOrgUser(orgId, actor, id) {
|
|
323
|
+
this.assertManagesUsers(actor, orgId);
|
|
308
324
|
const deleted = await this.uow.run(async ({ organizations, outbox }) => {
|
|
309
325
|
const orgUser = await organizations.findOrgUserById(orgId, id);
|
|
310
|
-
if (!orgUser) throw new NotFoundError("
|
|
326
|
+
if (!orgUser || orgUser.role !== "student") throw new NotFoundError("Student", id);
|
|
311
327
|
const deletedUser = await organizations.deleteOrgUser(orgId, id);
|
|
312
|
-
if (!deletedUser) throw new NotFoundError("
|
|
313
|
-
|
|
314
|
-
if (deletedUser.role === "student") events.push(organizationEvents.studentDeleted.make({
|
|
328
|
+
if (!deletedUser) throw new NotFoundError("Student", id);
|
|
329
|
+
await outbox.append([organizationEvents.studentDeleted.make({
|
|
315
330
|
orgId,
|
|
316
331
|
data: orgUser
|
|
317
|
-
}));
|
|
318
|
-
await outbox.append(events);
|
|
332
|
+
})]);
|
|
319
333
|
return deletedUser;
|
|
320
334
|
});
|
|
321
335
|
this.logger.info("org user deleted", {
|
|
@@ -354,12 +368,17 @@ var OrganizationServiceImpl = class {
|
|
|
354
368
|
async getBySlug(slug) {
|
|
355
369
|
return this.repo.findBySlug(slug);
|
|
356
370
|
}
|
|
371
|
+
staffOrgUserIds(orgId) {
|
|
372
|
+
return this.repo.findOrgUserIdsByRole(orgId, [...STAFF_ROLES]);
|
|
373
|
+
}
|
|
357
374
|
listMembers(orgId, query) {
|
|
358
375
|
return this.membersRepo.list(orgId, query);
|
|
359
376
|
}
|
|
360
|
-
async updateMemberRole(ctx, id, role) {
|
|
377
|
+
async updateMemberRole(ctx, actor, id, role) {
|
|
378
|
+
this.assertManagesUsers(actor, ctx.orgId);
|
|
379
|
+
this.assertNotOwner(role, ctx.orgId);
|
|
361
380
|
const member = await this.membersRepo.findById(ctx.orgId, id);
|
|
362
|
-
if (!member) return
|
|
381
|
+
if (!member) return this.promoteStudent(ctx, actor, id, role);
|
|
363
382
|
if (member.role === "owner") {
|
|
364
383
|
this.logger.warn("role change rejected: owner role immutable", {
|
|
365
384
|
orgId: ctx.orgId,
|
|
@@ -374,7 +393,9 @@ var OrganizationServiceImpl = class {
|
|
|
374
393
|
});
|
|
375
394
|
throw new OrganizationRuleError("Only active members can have their role changed");
|
|
376
395
|
}
|
|
377
|
-
|
|
396
|
+
if (role === "student") return this.demoteModerator(ctx, actor, member, member.userExternalId);
|
|
397
|
+
const userExternalId = member.userExternalId;
|
|
398
|
+
await this.asActor(actor, id, false, () => this.orgAdmin().updateRole(ctx, userExternalId, role));
|
|
378
399
|
const updated = await this.membersRepo.findById(ctx.orgId, id);
|
|
379
400
|
this.logger.info("member role updated", {
|
|
380
401
|
orgId: ctx.orgId,
|
|
@@ -383,7 +404,96 @@ var OrganizationServiceImpl = class {
|
|
|
383
404
|
});
|
|
384
405
|
return updated ? toMember(updated) : null;
|
|
385
406
|
}
|
|
386
|
-
async
|
|
407
|
+
async demoteModerator(ctx, actor, member, userExternalId) {
|
|
408
|
+
if (member.role !== "moderator") {
|
|
409
|
+
this.logger.warn("role change rejected: only a moderator becomes a student", {
|
|
410
|
+
orgId: ctx.orgId,
|
|
411
|
+
memberId: member.id
|
|
412
|
+
});
|
|
413
|
+
throw new OrganizationRuleError("Only a moderator can be made a student");
|
|
414
|
+
}
|
|
415
|
+
await this.asActor(actor, member.id, true, () => this.orgAdmin().removeMember(ctx, userExternalId));
|
|
416
|
+
const orgUser = await this.repo.findOrgUserById(ctx.orgId, member.id);
|
|
417
|
+
if (!orgUser) return null;
|
|
418
|
+
this.logger.info("moderator made a student", {
|
|
419
|
+
orgId: ctx.orgId,
|
|
420
|
+
memberId: member.id
|
|
421
|
+
});
|
|
422
|
+
return {
|
|
423
|
+
...toMember(member),
|
|
424
|
+
role: orgUser.role,
|
|
425
|
+
status: orgUser.status
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
async promoteStudent(ctx, actor, id, role) {
|
|
429
|
+
const orgUser = await this.repo.findOrgUserById(ctx.orgId, id);
|
|
430
|
+
if (!orgUser) return null;
|
|
431
|
+
if (orgUser.role !== "student" || role === "student") {
|
|
432
|
+
this.logger.warn("role change rejected: not an active member", {
|
|
433
|
+
orgId: ctx.orgId,
|
|
434
|
+
memberId: id
|
|
435
|
+
});
|
|
436
|
+
throw new OrganizationRuleError("Only active members can have their role changed");
|
|
437
|
+
}
|
|
438
|
+
const person = await this.people.getUserById(orgUser.userId);
|
|
439
|
+
if (!person?.externalId) {
|
|
440
|
+
this.logger.warn("role change rejected: student has no account", {
|
|
441
|
+
orgId: ctx.orgId,
|
|
442
|
+
memberId: id
|
|
443
|
+
});
|
|
444
|
+
throw new OrganizationRuleError("That student has not joined yet");
|
|
445
|
+
}
|
|
446
|
+
const org = await this.repo.findById(ctx.orgId);
|
|
447
|
+
if (!org) return null;
|
|
448
|
+
const orgExternalId = org.externalId ?? org.id;
|
|
449
|
+
const userExternalId = person.externalId;
|
|
450
|
+
await this.asActor(actor, orgUser.id, false, async () => {
|
|
451
|
+
await this.orgAdmin().grantMembership(orgExternalId, userExternalId, role);
|
|
452
|
+
if (!await this.membersRepo.findById(ctx.orgId, id)) await this.orgAdmin().updateRole(ctx, userExternalId, role);
|
|
453
|
+
});
|
|
454
|
+
const updated = await this.membersRepo.findById(ctx.orgId, id);
|
|
455
|
+
if (!updated) {
|
|
456
|
+
this.logger.warn("role change rejected: membership not mirrored", {
|
|
457
|
+
orgId: ctx.orgId,
|
|
458
|
+
memberId: id
|
|
459
|
+
});
|
|
460
|
+
throw new OrganizationRuleError("The role change did not take effect");
|
|
461
|
+
}
|
|
462
|
+
this.logger.info("student promoted", {
|
|
463
|
+
orgId: ctx.orgId,
|
|
464
|
+
memberId: id,
|
|
465
|
+
role
|
|
466
|
+
});
|
|
467
|
+
return toMember(updated);
|
|
468
|
+
}
|
|
469
|
+
asActor(actor, orgUserId, demote, write) {
|
|
470
|
+
return this.memberWrites.run({
|
|
471
|
+
orgUserId,
|
|
472
|
+
actorOrgUserId: actor.id,
|
|
473
|
+
demote
|
|
474
|
+
}, write);
|
|
475
|
+
}
|
|
476
|
+
memberWrite(orgUserId) {
|
|
477
|
+
const write = this.memberWrites.getStore();
|
|
478
|
+
return write?.orgUserId === orgUserId ? write : void 0;
|
|
479
|
+
}
|
|
480
|
+
assertManagesUsers(actor, orgId) {
|
|
481
|
+
if (capability(actor.role, "manage_users") !== true) {
|
|
482
|
+
this.logger.warn("member write rejected: actor cannot manage users", {
|
|
483
|
+
orgId,
|
|
484
|
+
actorOrgUserId: actor.id
|
|
485
|
+
});
|
|
486
|
+
throw new ForbiddenError("Only an owner or admin can manage members");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
assertNotOwner(role, orgId) {
|
|
490
|
+
if (role === "owner") {
|
|
491
|
+
this.logger.warn("member write rejected: owner cannot be granted", { orgId });
|
|
492
|
+
throw new OrganizationRuleError("Ownership cannot be granted");
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async removeMember(ctx, actor, id) {
|
|
496
|
+
this.assertManagesUsers(actor, ctx.orgId);
|
|
387
497
|
const member = await this.membersRepo.findById(ctx.orgId, id);
|
|
388
498
|
if (!member) return false;
|
|
389
499
|
if (member.role === "owner") {
|