@headless-lms/core 0.1.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 +90 -9
- package/dist/assets/events.js +19 -3
- 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 +292 -37
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.d.ts +12 -15
- package/dist/automations/actions.js +188 -30
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +5 -1
- package/dist/automations/catalog.js +514 -197
- 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 +74 -24
- package/dist/automations/events.js +5 -5
- package/dist/automations/events.js.map +1 -1
- package/dist/automations/index.d.ts +7 -5
- package/dist/automations/index.js +3 -2
- package/dist/automations/model.d.ts +20 -3
- package/dist/automations/model.js +36 -3
- package/dist/automations/model.js.map +1 -1
- package/dist/automations/ports.d.ts +27 -8
- 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 +41 -6
- package/dist/automations/service.js +542 -59
- package/dist/automations/service.js.map +1 -1
- package/dist/automations/types.d.ts +2 -2
- package/dist/automations/webhook-signing.d.ts +7 -0
- package/dist/automations/webhook-signing.js +22 -0
- package/dist/automations/webhook-signing.js.map +1 -0
- package/dist/content/events.d.ts +1013 -25
- package/dist/content/events.js +168 -19
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +5 -5
- package/dist/content/index.js +2 -2
- package/dist/content/model.d.ts +2 -2
- package/dist/content/ports.d.ts +109 -25
- package/dist/content/service.d.ts +68 -23
- package/dist/content/service.js +647 -60
- 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 +633 -14
- package/dist/discussion/events.js +134 -5
- 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/model.d.ts +1 -2
- 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 +33 -11
- package/dist/discussion/service.js +297 -81
- package/dist/discussion/service.js.map +1 -1
- package/dist/entitlements/events.d.ts +264 -13
- package/dist/entitlements/events.js +49 -5
- 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 +473 -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/events.d.ts +17 -3
- package/dist/identity/events.js +12 -2
- package/dist/identity/events.js.map +1 -1
- 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 +76 -3
- package/dist/identity/service.d.ts +17 -3
- package/dist/identity/service.js +73 -5
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +27 -3
- package/dist/integrations/events.js +15 -3
- package/dist/integrations/events.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 +234 -25
- package/dist/organizations/events.js +58 -9
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +8 -7
- 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 +20 -21
- 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 +28 -14
- package/dist/organizations/service.js +274 -105
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +2 -11
- package/dist/progress/events.d.ts +34 -2
- package/dist/progress/events.js +15 -2
- package/dist/progress/events.js.map +1 -1
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +6 -1
- package/dist/progress/service.d.ts +5 -3
- package/dist/progress/service.js +9 -3
- 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/events.d.ts +5 -3
- package/dist/shared/events.js +1 -0
- package/dist/shared/events.js.map +1 -1
- package/dist/shared/id.d.ts +12 -0
- package/dist/shared/id.js +13 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +6 -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/shared/settings.js.map +1 -1
- package/dist/types/assets.d.ts +2 -2
- package/dist/types/automations.d.ts +21 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +12 -3
- package/dist/types/email-templates.d.ts +124 -1
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/events.d.ts +3 -3
- package/dist/types/identity.d.ts +48 -2
- package/dist/types/index.d.ts +10 -10
- package/dist/types/organizations.d.ts +2 -2
- package/dist/types/ports.d.ts +7 -1
- package/dist/types/schemas/assets.d.ts +67 -36
- package/dist/types/schemas/assets.js +19 -13
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +88 -4
- package/dist/types/schemas/automations.js +46 -6
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +385 -20
- package/dist/types/schemas/content.js +178 -15
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/credentials.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +379 -4
- package/dist/types/schemas/discussion.js +209 -7
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +44 -4
- package/dist/types/schemas/entitlements.js +32 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +115 -5
- package/dist/types/schemas/identity.js +59 -6
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +9 -9
- package/dist/types/schemas/index.js +9 -9
- package/dist/types/schemas/integrations.js.map +1 -1
- package/dist/types/schemas/organizations.d.ts +27 -5
- package/dist/types/schemas/organizations.js +14 -7
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/outbox.d.ts +1 -0
- package/dist/types/schemas/outbox.js +1 -0
- package/dist/types/schemas/outbox.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +1 -0
- package/dist/types/schemas/progress.js +1 -0
- package/dist/types/schemas/progress.js.map +1 -1
- package/dist/types/schemas/settings.js.map +1 -1
- package/dist/types/schemas/shared.d.ts +4 -1
- package/dist/types/schemas/shared.js +7 -1
- package/dist/types/schemas/shared.js.map +1 -1
- package/dist/types/shared.d.ts +1 -1
- package/package.json +2 -2
- 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 -42
- package/dist/reporting/learn/service.d.ts +0 -48
- package/dist/reporting/learn/service.js +0 -111
- 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 -31
- package/dist/reporting/students/ports.d.ts +0 -13
- package/dist/reporting/students/ports.js +0 -1
- package/dist/reporting/students/service.d.ts +0 -19
- package/dist/reporting/students/service.js +0 -20
- package/dist/reporting/students/service.js.map +0 -1
- /package/dist/reporting/{dashboard → admin}/model.js +0 -0
- /package/dist/reporting/{dashboard → admin}/ports.js +0 -0
- /package/dist/reporting/{learn → member}/model.js +0 -0
- /package/dist/reporting/{learn → member}/ports.js +0 -0
- /package/dist/{reporting/students/model.js → shared/actor.js} +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { genId } from "../shared/id.js";
|
|
2
|
+
import { ForbiddenError, NotFoundError } from "../shared/errors.js";
|
|
2
3
|
import { noopLogger } from "../shared/logger.js";
|
|
3
|
-
import {
|
|
4
|
-
import { STUDENT_ROLE } from "./roles.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) {
|
|
@@ -25,14 +26,17 @@ var OrganizationServiceImpl = class {
|
|
|
25
26
|
membersRepo;
|
|
26
27
|
people;
|
|
27
28
|
uow;
|
|
29
|
+
orgAdmin;
|
|
28
30
|
logger;
|
|
29
31
|
mailer;
|
|
30
32
|
inviteUrls;
|
|
33
|
+
memberWrites = new AsyncLocalStorage();
|
|
31
34
|
constructor(params) {
|
|
32
35
|
this.repo = params.repo;
|
|
33
36
|
this.membersRepo = params.membersRepo;
|
|
34
37
|
this.people = params.people;
|
|
35
38
|
this.uow = params.uow;
|
|
39
|
+
this.orgAdmin = params.orgAdmin;
|
|
36
40
|
this.logger = params.logger ?? noopLogger;
|
|
37
41
|
this.mailer = params.mailer;
|
|
38
42
|
this.inviteUrls = params.inviteUrls;
|
|
@@ -88,6 +92,10 @@ var OrganizationServiceImpl = class {
|
|
|
88
92
|
return deleted;
|
|
89
93
|
}
|
|
90
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
|
+
});
|
|
91
99
|
const orgUser = await this.uow.run(async ({ organizations, outbox }) => {
|
|
92
100
|
const created = await organizations.createOrgUser({
|
|
93
101
|
orgId,
|
|
@@ -108,57 +116,111 @@ var OrganizationServiceImpl = class {
|
|
|
108
116
|
});
|
|
109
117
|
return orgUser;
|
|
110
118
|
}
|
|
111
|
-
async removeOrgUser(orgId,
|
|
119
|
+
async removeOrgUser(orgId, userId) {
|
|
112
120
|
const deleted = await this.uow.run(async ({ organizations, outbox }) => {
|
|
113
|
-
const
|
|
114
|
-
if (!
|
|
121
|
+
const orgUser = await organizations.findOrgUser(orgId, userId);
|
|
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;
|
|
135
|
+
const deletedUser = await organizations.deleteOrgUser(orgId, orgUser.id);
|
|
136
|
+
if (!deletedUser) throw new NotFoundError("Member", orgUser.id);
|
|
115
137
|
await outbox.append([organizationEvents.orgUserDeleted.make({
|
|
116
138
|
orgId,
|
|
117
139
|
data: deletedUser
|
|
118
140
|
})]);
|
|
119
141
|
return deletedUser;
|
|
120
142
|
});
|
|
121
|
-
this.logger.info("
|
|
143
|
+
this.logger.info("org user removed", {
|
|
144
|
+
orgId,
|
|
145
|
+
userId,
|
|
146
|
+
orgUserId: deleted.id
|
|
147
|
+
});
|
|
122
148
|
return deleted;
|
|
123
149
|
}
|
|
124
|
-
async
|
|
150
|
+
async updateOrgUser(orgId, userId, patch) {
|
|
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
|
+
});
|
|
166
|
+
this.logger.info("org user updated", {
|
|
167
|
+
orgId,
|
|
168
|
+
userId,
|
|
169
|
+
orgUserId: updated.id,
|
|
170
|
+
patch
|
|
171
|
+
});
|
|
172
|
+
return updated;
|
|
173
|
+
}
|
|
174
|
+
async createInvite(actor, input) {
|
|
125
175
|
const { orgId, email, role, inviterUserId, firstName, lastName } = input;
|
|
176
|
+
this.assertManagesUsers(actor, orgId);
|
|
177
|
+
this.assertNotOwner(role, orgId);
|
|
126
178
|
const sendEmail = input.sendEmail ?? true;
|
|
127
179
|
const { token, tokenHash } = generateInviteToken();
|
|
128
180
|
const expiresAt = new Date(Date.now() + INVITE_TTL_MS);
|
|
129
|
-
const person = role === "student" ? await this.people.createUser({
|
|
181
|
+
const person = role === "student" ? await this.people.getUserByEmail(email) ?? await this.people.createUser({
|
|
130
182
|
email,
|
|
131
183
|
...firstName !== void 0 && { firstName },
|
|
132
184
|
...lastName !== void 0 && { lastName }
|
|
133
185
|
}) : null;
|
|
134
186
|
const invite = await this.uow.run(async ({ organizations, outbox }) => {
|
|
135
|
-
const
|
|
136
|
-
email,
|
|
137
|
-
role,
|
|
138
|
-
invitedBy: inviterUserId,
|
|
139
|
-
tokenHash,
|
|
140
|
-
expiresAt
|
|
141
|
-
});
|
|
142
|
-
const events = [organizationEvents.inviteCreated.make({
|
|
143
|
-
orgId,
|
|
144
|
-
data: row
|
|
145
|
-
})];
|
|
187
|
+
const events = [];
|
|
146
188
|
if (person) {
|
|
147
|
-
const
|
|
189
|
+
const orgUser = await organizations.createOrgUser({
|
|
148
190
|
orgId,
|
|
149
191
|
userId: person.id,
|
|
150
192
|
role: STUDENT_ROLE,
|
|
151
193
|
status: "invited"
|
|
152
194
|
});
|
|
153
|
-
|
|
195
|
+
events.push(organizationEvents.studentCreated.make({
|
|
154
196
|
orgId,
|
|
155
197
|
data: orgUser
|
|
156
198
|
}));
|
|
157
199
|
}
|
|
200
|
+
const row = await organizations.upsertPendingInvite(orgId, {
|
|
201
|
+
email,
|
|
202
|
+
role,
|
|
203
|
+
invitedBy: inviterUserId,
|
|
204
|
+
tokenHash,
|
|
205
|
+
expiresAt
|
|
206
|
+
});
|
|
207
|
+
events.push(organizationEvents.inviteCreated.make({
|
|
208
|
+
orgId,
|
|
209
|
+
data: row
|
|
210
|
+
}));
|
|
158
211
|
await outbox.append(events);
|
|
159
212
|
return row;
|
|
160
213
|
});
|
|
161
|
-
if (sendEmail)
|
|
214
|
+
if (sendEmail) try {
|
|
215
|
+
await this.sendInviteEmail(invite, token);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
this.logger.error("failed to send invite email", {
|
|
218
|
+
orgId,
|
|
219
|
+
inviteId: invite.id,
|
|
220
|
+
role: invite.role,
|
|
221
|
+
err: err instanceof Error ? err : new Error(String(err))
|
|
222
|
+
});
|
|
223
|
+
}
|
|
162
224
|
this.logger.info("invite created", {
|
|
163
225
|
orgId,
|
|
164
226
|
inviteId: invite.id,
|
|
@@ -167,44 +229,26 @@ var OrganizationServiceImpl = class {
|
|
|
167
229
|
});
|
|
168
230
|
return invite;
|
|
169
231
|
}
|
|
170
|
-
async
|
|
171
|
-
const { orgId, orgUserId, firstName, lastName, email } = input;
|
|
172
|
-
const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);
|
|
173
|
-
if (!orgUser || orgUser.role !== "student") throw new NotFoundError("Student", orgUserId);
|
|
174
|
-
const person = await this.people.getUserById(orgUser.userId);
|
|
175
|
-
if (!person) throw new NotFoundError("User", orgUser.userId);
|
|
176
|
-
const previousEmail = person.email;
|
|
177
|
-
await this.people.updateUser(person.id, {
|
|
178
|
-
...firstName !== void 0 && { firstName },
|
|
179
|
-
...lastName !== void 0 && { lastName },
|
|
180
|
-
...email !== void 0 && { email }
|
|
181
|
-
});
|
|
182
|
-
const emailChanged = email !== void 0 && email.toLowerCase() !== previousEmail.toLowerCase();
|
|
183
|
-
if (emailChanged && orgUser.status !== "active") await this.repo.cancelPendingInvite(orgId, previousEmail);
|
|
184
|
-
this.logger.info("student updated", {
|
|
185
|
-
orgId,
|
|
186
|
-
orgUserId,
|
|
187
|
-
emailChanged
|
|
188
|
-
});
|
|
189
|
-
return orgUser;
|
|
190
|
-
}
|
|
191
|
-
async resendStudentInvite(input) {
|
|
232
|
+
async resendStudentInvite(actor, input) {
|
|
192
233
|
const { orgId, orgUserId, inviterUserId } = input;
|
|
234
|
+
this.assertManagesUsers(actor, orgId);
|
|
193
235
|
const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);
|
|
194
236
|
if (!orgUser || orgUser.role !== "student") throw new NotFoundError("Student", orgUserId);
|
|
195
237
|
if (orgUser.status === "active") throw new OrganizationRuleError("That student has already accepted their invitation");
|
|
196
238
|
const person = await this.people.getUserById(orgUser.userId);
|
|
197
239
|
if (!person) throw new NotFoundError("User", orgUser.userId);
|
|
198
|
-
await this.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
240
|
+
const pending = await this.repo.findPendingInvite(orgId, person.email);
|
|
241
|
+
if (!pending) throw new NotFoundError("Invite", person.email);
|
|
242
|
+
const { token, tokenHash } = generateInviteToken();
|
|
243
|
+
const expiresAt = new Date(Date.now() + INVITE_TTL_MS);
|
|
244
|
+
const invite = await this.repo.setInviteToken(orgId, pending.id, tokenHash, expiresAt);
|
|
245
|
+
if (!invite) throw new NotFoundError("Invite", pending.id);
|
|
246
|
+
await this.sendInviteEmail(invite, token);
|
|
205
247
|
this.logger.info("student invite resent", {
|
|
206
248
|
orgId,
|
|
207
|
-
orgUserId
|
|
249
|
+
orgUserId,
|
|
250
|
+
inviteId: invite.id,
|
|
251
|
+
inviterUserId
|
|
208
252
|
});
|
|
209
253
|
}
|
|
210
254
|
async peekInvite(token) {
|
|
@@ -226,55 +270,66 @@ var OrganizationServiceImpl = class {
|
|
|
226
270
|
});
|
|
227
271
|
throw new InviteError("emails must match");
|
|
228
272
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
273
|
+
const org = await this.repo.findById(invite.orgId);
|
|
274
|
+
if (!org) throw new InviteError("Org not found");
|
|
275
|
+
const orgUser = invite.role === "student" ? await this.acceptStudentInvite(invite, input.userId) : await this.acceptStaffInvite(invite, org, input.userId);
|
|
276
|
+
this.logger.info("invite accepted", {
|
|
277
|
+
orgId: invite.orgId,
|
|
278
|
+
userId: input.userId
|
|
279
|
+
});
|
|
280
|
+
return orgUser;
|
|
281
|
+
}
|
|
282
|
+
async acceptStudentInvite(invite, userId) {
|
|
283
|
+
return this.uow.run(async ({ organizations, outbox }) => {
|
|
284
|
+
const existing = await organizations.findOrgUser(invite.orgId, userId);
|
|
285
|
+
if (!existing) throw new NotFoundError("Member", userId);
|
|
286
|
+
const activated = await organizations.updateOrgUser(invite.orgId, existing.id, { status: "active" });
|
|
287
|
+
if (!activated) throw new NotFoundError("Member", existing.id);
|
|
237
288
|
const acceptedInvite = await organizations.setInviteStatus(invite.orgId, invite.id, "accepted");
|
|
238
289
|
if (!acceptedInvite) throw new NotFoundError("Invite", invite.id);
|
|
239
|
-
|
|
290
|
+
await outbox.append([organizationEvents.inviteAccepted.make({
|
|
240
291
|
orgId: invite.orgId,
|
|
241
292
|
data: acceptedInvite
|
|
242
|
-
})
|
|
243
|
-
if (row.role === "student") events.push(created ? organizationEvents.studentCreated.make({
|
|
293
|
+
}), organizationEvents.studentLinked.make({
|
|
244
294
|
orgId: invite.orgId,
|
|
245
|
-
data:
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
data: row
|
|
249
|
-
}));
|
|
250
|
-
else if (created) events.push(organizationEvents.orgUserLinked.make({
|
|
251
|
-
orgId: invite.orgId,
|
|
252
|
-
data: row
|
|
253
|
-
}));
|
|
254
|
-
await outbox.append(events);
|
|
255
|
-
return row;
|
|
295
|
+
data: activated
|
|
296
|
+
})]);
|
|
297
|
+
return activated;
|
|
256
298
|
});
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
299
|
+
}
|
|
300
|
+
async acceptStaffInvite(invite, org, userId) {
|
|
301
|
+
const person = await this.people.getUserById(userId);
|
|
302
|
+
if (!person?.externalId) throw new NotFoundError("User", userId);
|
|
303
|
+
await this.orgAdmin().grantMembership(org.externalId ?? org.id, person.externalId, invite.role);
|
|
304
|
+
return this.uow.run(async ({ organizations, outbox }) => {
|
|
305
|
+
const orgUser = await organizations.findOrgUser(invite.orgId, userId);
|
|
306
|
+
if (!orgUser) throw new NotFoundError("Member", userId);
|
|
307
|
+
const acceptedInvite = await organizations.setInviteStatus(invite.orgId, invite.id, "accepted");
|
|
308
|
+
if (!acceptedInvite) throw new NotFoundError("Invite", invite.id);
|
|
309
|
+
await outbox.append([organizationEvents.inviteAccepted.make({
|
|
310
|
+
orgId: invite.orgId,
|
|
311
|
+
data: acceptedInvite
|
|
312
|
+
})]);
|
|
313
|
+
return orgUser;
|
|
260
314
|
});
|
|
261
|
-
|
|
315
|
+
}
|
|
316
|
+
getOrgUserById(orgId, id) {
|
|
317
|
+
return this.repo.findOrgUserById(orgId, id);
|
|
262
318
|
}
|
|
263
319
|
async getOrgUser(orgId, userId) {
|
|
264
320
|
return this.repo.findOrgUser(orgId, userId);
|
|
265
321
|
}
|
|
266
|
-
async deleteOrgUser(orgId, id) {
|
|
322
|
+
async deleteOrgUser(orgId, actor, id) {
|
|
323
|
+
this.assertManagesUsers(actor, orgId);
|
|
267
324
|
const deleted = await this.uow.run(async ({ organizations, outbox }) => {
|
|
268
325
|
const orgUser = await organizations.findOrgUserById(orgId, id);
|
|
269
|
-
if (!orgUser) throw new NotFoundError("
|
|
326
|
+
if (!orgUser || orgUser.role !== "student") throw new NotFoundError("Student", id);
|
|
270
327
|
const deletedUser = await organizations.deleteOrgUser(orgId, id);
|
|
271
|
-
if (!deletedUser) throw new NotFoundError("
|
|
272
|
-
|
|
273
|
-
if (deletedUser.role === "student") events.push(organizationEvents.studentDeleted.make({
|
|
328
|
+
if (!deletedUser) throw new NotFoundError("Student", id);
|
|
329
|
+
await outbox.append([organizationEvents.studentDeleted.make({
|
|
274
330
|
orgId,
|
|
275
331
|
data: orgUser
|
|
276
|
-
}));
|
|
277
|
-
await outbox.append(events);
|
|
332
|
+
})]);
|
|
278
333
|
return deletedUser;
|
|
279
334
|
});
|
|
280
335
|
this.logger.info("org user deleted", {
|
|
@@ -297,23 +352,15 @@ var OrganizationServiceImpl = class {
|
|
|
297
352
|
token,
|
|
298
353
|
email
|
|
299
354
|
})).toString()}`;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
});
|
|
310
|
-
} catch (err) {
|
|
311
|
-
this.logger.error("failed to send invite email", {
|
|
312
|
-
email,
|
|
313
|
-
role,
|
|
314
|
-
err: err instanceof Error ? err : new Error(String(err))
|
|
315
|
-
});
|
|
316
|
-
}
|
|
355
|
+
if (role === "student") await this.mailer.send(email, "studentInvite", {
|
|
356
|
+
inviteUrl,
|
|
357
|
+
studentName: email
|
|
358
|
+
});
|
|
359
|
+
else await this.mailer.send(email, "memberInvite", {
|
|
360
|
+
inviteUrl,
|
|
361
|
+
inviterName: "Your team",
|
|
362
|
+
role
|
|
363
|
+
});
|
|
317
364
|
}
|
|
318
365
|
async getByExternalId(externalId) {
|
|
319
366
|
return this.repo.findByExternalId(externalId);
|
|
@@ -321,12 +368,17 @@ var OrganizationServiceImpl = class {
|
|
|
321
368
|
async getBySlug(slug) {
|
|
322
369
|
return this.repo.findBySlug(slug);
|
|
323
370
|
}
|
|
371
|
+
staffOrgUserIds(orgId) {
|
|
372
|
+
return this.repo.findOrgUserIdsByRole(orgId, [...STAFF_ROLES]);
|
|
373
|
+
}
|
|
324
374
|
listMembers(orgId, query) {
|
|
325
375
|
return this.membersRepo.list(orgId, query);
|
|
326
376
|
}
|
|
327
|
-
async updateMemberRole(ctx, id, role) {
|
|
377
|
+
async updateMemberRole(ctx, actor, id, role) {
|
|
378
|
+
this.assertManagesUsers(actor, ctx.orgId);
|
|
379
|
+
this.assertNotOwner(role, ctx.orgId);
|
|
328
380
|
const member = await this.membersRepo.findById(ctx.orgId, id);
|
|
329
|
-
if (!member) return
|
|
381
|
+
if (!member) return this.promoteStudent(ctx, actor, id, role);
|
|
330
382
|
if (member.role === "owner") {
|
|
331
383
|
this.logger.warn("role change rejected: owner role immutable", {
|
|
332
384
|
orgId: ctx.orgId,
|
|
@@ -341,6 +393,9 @@ var OrganizationServiceImpl = class {
|
|
|
341
393
|
});
|
|
342
394
|
throw new OrganizationRuleError("Only active members can have their role changed");
|
|
343
395
|
}
|
|
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));
|
|
344
399
|
const updated = await this.membersRepo.findById(ctx.orgId, id);
|
|
345
400
|
this.logger.info("member role updated", {
|
|
346
401
|
orgId: ctx.orgId,
|
|
@@ -349,7 +404,121 @@ var OrganizationServiceImpl = class {
|
|
|
349
404
|
});
|
|
350
405
|
return updated ? toMember(updated) : null;
|
|
351
406
|
}
|
|
352
|
-
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);
|
|
497
|
+
const member = await this.membersRepo.findById(ctx.orgId, id);
|
|
498
|
+
if (!member) return false;
|
|
499
|
+
if (member.role === "owner") {
|
|
500
|
+
this.logger.warn("member removal rejected: owner cannot be removed", {
|
|
501
|
+
orgId: ctx.orgId,
|
|
502
|
+
memberId: id
|
|
503
|
+
});
|
|
504
|
+
throw new OrganizationRuleError("The owner cannot be removed");
|
|
505
|
+
}
|
|
506
|
+
if (member.kind === "member" && member.userExternalId) await this.orgAdmin().removeMember(ctx, member.userExternalId);
|
|
507
|
+
else if (member.kind === "invite" && member.inviteId) {
|
|
508
|
+
const inviteId = member.inviteId;
|
|
509
|
+
await this.uow.run(async ({ organizations, outbox }) => {
|
|
510
|
+
const canceled = await organizations.setInviteStatus(ctx.orgId, inviteId, "canceled");
|
|
511
|
+
if (!canceled) throw new NotFoundError("Invite", inviteId);
|
|
512
|
+
await outbox.append([organizationEvents.inviteCanceled.make({
|
|
513
|
+
orgId: ctx.orgId,
|
|
514
|
+
data: canceled
|
|
515
|
+
})]);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
this.logger.info("member removed", {
|
|
519
|
+
orgId: ctx.orgId,
|
|
520
|
+
memberId: id
|
|
521
|
+
});
|
|
353
522
|
return true;
|
|
354
523
|
}
|
|
355
524
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/organizations/service.ts"],"sourcesContent":["// organizations context — service implementation (inbound port).\nimport type {\n MemberRecord,\n MembersRepository,\n MemberWriteContext,\n OrganizationService,\n OrganizationsRepository,\n OrganizationsUnitOfWork,\n} from './ports.js';\nimport type { Invite, Organization, OrgUser } from './model.js';\nimport { type Role, STUDENT_ROLE } from './roles.js';\nimport {\n InviteError,\n type Member,\n type MembersQuery,\n OrganizationRuleError,\n type Page,\n} from './members.js';\nimport type {\n AcceptInviteInput,\n AddOrgUserInput,\n CreateInviteInput,\n CreateOrganizationInput,\n ResendStudentInviteInput,\n UpdateOrganizationInput,\n UpdateStudentInput,\n} from './types.js';\nimport { organizationEvents, type OrganizationEvent } from './events.js';\nimport type { Logger, NewDomainEvent } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Mailer } from '../shared/mailer.js';\nimport { generateInviteToken, hashInviteToken } from '../shared/invite-token.js';\nimport { NotFoundError } from '../shared/errors.js';\nimport type { IdentityService } from '../identity/index.js';\nimport { genId } from '../shared/id.js';\n\nconst INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n\ntype NewOrganizationEvent = NewDomainEvent<OrganizationEvent>;\n\nexport interface InviteUrls {\n studentPortalUrl: string;\n adminAppUrl: string;\n}\n\nfunction toMember(r: MemberRecord): Member {\n return {\n id: r.id,\n firstName: r.firstName,\n lastName: r.lastName,\n email: r.email,\n image: r.image,\n role: r.role,\n status: r.status,\n joinedAt: r.joinedAt,\n invitedAt: r.invitedAt,\n };\n}\n\nexport type OrganizationServiceParams = {\n repo: OrganizationsRepository;\n membersRepo: MembersRepository;\n people: IdentityService;\n uow: OrganizationsUnitOfWork;\n logger?: Logger;\n mailer?: Pick<Mailer, 'send'>;\n inviteUrls?: InviteUrls;\n};\n\nexport class OrganizationServiceImpl implements OrganizationService {\n private readonly repo: OrganizationsRepository;\n private readonly membersRepo: MembersRepository;\n private readonly people: IdentityService;\n private readonly uow: OrganizationsUnitOfWork;\n private readonly logger: Logger;\n private readonly mailer?: Pick<Mailer, 'send'>;\n private readonly inviteUrls?: InviteUrls;\n\n constructor(params: OrganizationServiceParams) {\n this.repo = params.repo;\n this.membersRepo = params.membersRepo;\n this.people = params.people;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n this.mailer = params.mailer;\n this.inviteUrls = params.inviteUrls;\n }\n\n async createOrganization(input: CreateOrganizationInput): Promise<Organization> {\n const created = await this.uow.run(async ({ organizations, outbox }) => {\n const id = genId('organization');\n\n const created = await organizations.create({\n id,\n // The auth provider adopts this id for its own org row, so the org is\n // its own external id — a lookup by either id lands on the same row.\n externalId: input.externalId ?? id,\n name: input.name ?? `My Org`,\n slug: input.slug ?? '',\n ownerId: input.ownerId,\n });\n await outbox.append([\n organizationEvents.organizationCreated.make({\n orgId: created.id,\n data: created,\n }),\n ]);\n return created;\n });\n this.logger.info('organization created', { orgId: created.id, slug: created.slug });\n return created;\n }\n\n async updateOrganization(id: string, input: UpdateOrganizationInput): Promise<Organization> {\n const updatedOrg = await this.uow.run(async ({ organizations, outbox }) => {\n const updated = await organizations.update(id, {\n name: input.name,\n slug: input.slug,\n });\n if (!updated) {\n throw new NotFoundError('Organization', id);\n }\n await outbox.append([\n organizationEvents.organizationUpdated.make({ orgId: id, data: updated }),\n ]);\n return updated;\n });\n\n this.logger.info('organization updated', { updatedOrg });\n return updatedOrg!;\n }\n\n async deleteOrganization(id: string): Promise<Organization> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const deleted = await organizations.delete(id);\n\n await outbox.append([\n organizationEvents.organizationDeleted.make({\n orgId: deleted.id,\n data: deleted,\n }),\n ]);\n return deleted;\n });\n this.logger.info('organization deleted', { orgId: deleted.id });\n return deleted;\n }\n\n async addOrgUser({ orgId, userId, role }: AddOrgUserInput): Promise<OrgUser> {\n const orgUser = await this.uow.run(async ({ organizations, outbox }) => {\n const created = await organizations.createOrgUser({ orgId, role, userId, status: 'active' });\n await outbox.append([\n organizationEvents.orgUserLinked.make({ orgId, data: created }),\n ]);\n return created;\n });\n this.logger.info('user added to organization', { orgId, userId, role });\n return orgUser;\n }\n\n async removeOrgUser(orgId: string, id: string): Promise<OrgUser> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const deletedUser = await organizations.deleteOrgUser(orgId, id);\n if (!deletedUser) {\n throw new NotFoundError('OrgUser', id);\n }\n await outbox.append([\n organizationEvents.orgUserDeleted.make({\n orgId,\n data: deletedUser,\n }),\n ]);\n return deletedUser;\n });\n this.logger.info('organization deleted', { orgId: deleted.id });\n return deleted;\n }\n\n async createInvite(input: CreateInviteInput): Promise<Invite> {\n const { orgId, email, role, inviterUserId, firstName, lastName } = input;\n const sendEmail = input.sendEmail ?? true;\n const { token, tokenHash } = generateInviteToken();\n const expiresAt = new Date(Date.now() + INVITE_TTL_MS);\n\n // A student belongs to the org from the moment an admin adds them, not from\n // the moment they click a link — so the person exists before the invite\n // does. Staff are excluded: their org_users row is mirrored from the auth\n // provider's member record, which does not exist until they join.\n const person =\n role === STUDENT_ROLE\n ? await this.people.createUser({\n email,\n ...(firstName !== undefined && { firstName }),\n ...(lastName !== undefined && { lastName }),\n })\n : null;\n\n const invite = await this.uow.run(async ({ organizations, outbox }) => {\n const row = await organizations.upsertPendingInvite(orgId, {\n email,\n role,\n invitedBy: inviterUserId,\n tokenHash,\n expiresAt,\n });\n const events: NewOrganizationEvent[] = [\n organizationEvents.inviteCreated.make({ orgId, data: row }),\n ];\n if (person) {\n // Idempotent: re-inviting an address rotates the token without\n // producing a second org user or a second student.created.\n const { orgUser, created } = await organizations.ensureOrgUser({\n orgId,\n userId: person.id,\n role: STUDENT_ROLE,\n status: 'invited',\n });\n if (created) {\n events.push(\n organizationEvents.studentCreated.make({ orgId, data: orgUser }),\n );\n }\n }\n await outbox.append(events);\n return row;\n });\n\n if (sendEmail) {\n // TODO make this workflow durable\n await this.sendInviteEmail(invite, token);\n }\n\n this.logger.info('invite created', { orgId, inviteId: invite.id, role, sendEmail });\n return invite;\n }\n\n async updateStudent(input: UpdateStudentInput): Promise<OrgUser> {\n const { orgId, orgUserId, firstName, lastName, email } = input;\n const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);\n if (!orgUser || orgUser.role !== STUDENT_ROLE) {\n throw new NotFoundError('Student', orgUserId);\n }\n const person = await this.people.getUserById(orgUser.userId);\n if (!person) {\n throw new NotFoundError('User', orgUser.userId);\n }\n\n const previousEmail = person.email;\n await this.people.updateUser(person.id, {\n ...(firstName !== undefined && { firstName }),\n ...(lastName !== undefined && { lastName }),\n ...(email !== undefined && { email }),\n });\n\n // The pending invite carries its own copy of the address and a token minted\n // against it. Once the address is corrected that invite points at the wrong\n // person, so it is cancelled rather than left live — the admin re-sends,\n // which mints a fresh one for the new address. A student who has already\n // accepted has no pending invite to worry about.\n const emailChanged = email !== undefined && email.toLowerCase() !== previousEmail.toLowerCase();\n if (emailChanged && orgUser.status !== 'active') {\n await this.repo.cancelPendingInvite(orgId, previousEmail);\n }\n\n this.logger.info('student updated', { orgId, orgUserId, emailChanged });\n return orgUser;\n }\n\n async resendStudentInvite(input: ResendStudentInviteInput): Promise<void> {\n const { orgId, orgUserId, inviterUserId } = input;\n const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);\n if (!orgUser || orgUser.role !== STUDENT_ROLE) {\n throw new NotFoundError('Student', orgUserId);\n }\n if (orgUser.status === 'active') {\n throw new OrganizationRuleError('That student has already accepted their invitation');\n }\n const person = await this.people.getUserById(orgUser.userId);\n if (!person) {\n throw new NotFoundError('User', orgUser.userId);\n }\n // Straight back through createInvite: it rotates the token on the pending\n // row and finds the existing org user, so a resend is an invite that\n // happens to be the second one.\n await this.createInvite({\n orgId,\n email: person.email,\n role: STUDENT_ROLE,\n inviterUserId,\n sendEmail: true,\n });\n this.logger.info('student invite resent', { orgId, orgUserId });\n }\n\n async peekInvite(token: string): Promise<Invite | null> {\n const invite = await this.repo.findInviteByTokenHash(hashInviteToken(token));\n if (!invite || invite.status !== 'pending') {\n return null;\n }\n if (invite.expiresAt && invite.expiresAt < new Date()) {\n return null;\n }\n return invite;\n }\n\n async acceptInvite(input: AcceptInviteInput): Promise<OrgUser> {\n const invite = await this.peekInvite(input.token);\n if (!invite) {\n this.logger.warn('invite accept refused: token invalid or expired');\n throw new InviteError('invalid invite');\n }\n if (invite.email.toLowerCase() !== input.email.toLowerCase()) {\n this.logger.warn('invite accept refused: email mismatch', {\n orgId: invite.orgId,\n inviteId: invite.id,\n });\n throw new InviteError('emails must match');\n }\n const org = await this.repo.findById(invite.orgId);\n if (!org) {\n throw new InviteError('Org not found');\n }\n\n const orgUser = await this.uow.run(async ({ organizations, outbox }) => {\n // A student provisioned at invite time already has a row — acceptance\n // activates it. Staff have none yet, so this inserts one.\n const { orgUser: row, created } = await organizations.ensureOrgUser({\n orgId: invite.orgId,\n userId: input.userId,\n role: invite.role,\n status: 'active',\n });\n const acceptedInvite = await organizations.setInviteStatus(invite.orgId, invite.id, 'accepted');\n if (!acceptedInvite) {\n throw new NotFoundError('Invite', invite.id);\n }\n const events: NewOrganizationEvent[] = [\n organizationEvents.inviteAccepted.make({\n orgId: invite.orgId,\n data: acceptedInvite,\n }),\n ];\n if (row.role === STUDENT_ROLE) {\n events.push(\n created\n ? organizationEvents.studentCreated.make({\n orgId: invite.orgId,\n data: row,\n })\n : organizationEvents.studentLinked.make({\n orgId: invite.orgId,\n data: row,\n }),\n );\n } else if (created) {\n events.push(\n organizationEvents.orgUserLinked.make({\n orgId: invite.orgId,\n data: row,\n }),\n );\n }\n await outbox.append(events);\n return row;\n });\n this.logger.info('invite accepted', { orgId: invite.orgId, userId: input.userId });\n return orgUser;\n }\n\n // By identity USER id — `(org_id, user_id)` is unique. Every caller (scope\n // resolution, the MCP principal, the learn routes) asks \"what is this person\n // in this org\", never \"what is this org_users row\".\n async getOrgUser(orgId: string, userId: string): Promise<OrgUser | null> {\n return this.repo.findOrgUser(orgId, userId);\n }\n\n async deleteOrgUser(orgId: string, id: string): Promise<OrgUser> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const orgUser = await organizations.findOrgUserById(orgId, id);\n if (!orgUser) {\n throw new NotFoundError('OrgUser', id);\n }\n const deletedUser = await organizations.deleteOrgUser(orgId, id);\n if (!deletedUser) {\n throw new NotFoundError('OrgUser', id);\n }\n const events: NewOrganizationEvent[] = [];\n if (deletedUser.role === STUDENT_ROLE) {\n events.push(\n organizationEvents.studentDeleted.make({ orgId, data: orgUser }),\n );\n }\n\n await outbox.append(events);\n return deletedUser;\n });\n this.logger.info('org user deleted', { orgId, orgUserId: id });\n return deleted;\n }\n\n async getOrgUsersForUser(userId: string): Promise<OrgUser[]> {\n return this.repo.findOrgUsersByUser(userId);\n }\n\n async getById(id: string): Promise<Organization | null> {\n return this.repo.findById(id);\n }\n\n private async sendInviteEmail(invite: Invite, token: string): Promise<void> {\n if (!this.mailer || !this.inviteUrls) {\n throw new Error('invite delivery is not configured (mailer / invite urls missing)');\n }\n const { email, role } = invite;\n const isStudent = role === STUDENT_ROLE;\n const base = isStudent\n ? `${this.inviteUrls.studentPortalUrl}/welcome`\n : `${this.inviteUrls.adminAppUrl}/invite`;\n // The portal resolves the token to the invited email, so the link carries\n // only the token. The admin app still reads it from the query.\n const query = isStudent\n ? new URLSearchParams({ token })\n : new URLSearchParams({ token, email });\n const inviteUrl = `${base}?${query.toString()}`;\n try {\n if (role === STUDENT_ROLE) {\n await this.mailer.send(email, 'studentInvite', { inviteUrl, studentName: email });\n } else {\n await this.mailer.send(email, 'memberInvite', {\n inviteUrl,\n inviterName: 'Your team',\n role,\n });\n }\n } catch (err) {\n // A failed email must not abort invite creation: the token is already\n // minted and recorded, so the admin can fix transport and resend.\n this.logger.error('failed to send invite email', {\n email,\n role,\n err: err instanceof Error ? err : new Error(String(err)),\n });\n }\n }\n\n async getByExternalId(externalId: string): Promise<Organization | null> {\n return this.repo.findByExternalId(externalId);\n }\n\n async getBySlug(slug: string): Promise<Organization | null> {\n return this.repo.findBySlug(slug);\n }\n\n listMembers(orgId: string, query: MembersQuery): Promise<Page<Member>> {\n return this.membersRepo.list(orgId, query);\n }\n\n async updateMemberRole(ctx: MemberWriteContext, id: string, role: Role): Promise<Member | null> {\n const member = await this.membersRepo.findById(ctx.orgId, id);\n if (!member) {\n return null;\n }\n if (member.role === 'owner') {\n this.logger.warn('role change rejected: owner role immutable', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('The owner role cannot be reassigned');\n }\n if (member.kind !== 'member' || !member.userExternalId) {\n this.logger.warn('role change rejected: not an active member', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('Only active members can have their role changed');\n }\n\n const updated = await this.membersRepo.findById(ctx.orgId, id);\n this.logger.info('member role updated', { orgId: ctx.orgId, memberId: id, role });\n return updated ? toMember(updated) : null;\n }\n\n async removeMember(_ctx: MemberWriteContext, _id: string): Promise<boolean> {\n return true;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../src/organizations/service.ts"],"sourcesContent":["// organizations context — service implementation (inbound port).\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type {\n MemberRecord,\n MembersRepository,\n MemberWriteContext,\n OrgAdmin,\n OrganizationService,\n OrganizationsRepository,\n OrganizationsUnitOfWork,\n} from './ports.js';\nimport type { Invite, Organization, OrgUser } from './model.js';\nimport { capability, type Role, STAFF_ROLES, STUDENT_ROLE } from './roles.js';\nimport {\n InviteError,\n type Member,\n type MemberRoleChange,\n type MembersQuery,\n OrganizationRuleError,\n type Page,\n} from './members.js';\nimport type {\n AcceptInviteInput,\n AddOrgUserInput,\n CreateInviteInput,\n CreateOrganizationInput,\n ResendStudentInviteInput,\n UpdateOrganizationInput,\n UpdateOrgUserInput,\n} from './types.js';\nimport { organizationEvents, type OrganizationEvent } from './events.js';\nimport type { Logger, NewDomainEvent } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Mailer } from '../shared/mailer.js';\nimport { generateInviteToken, hashInviteToken } from '../shared/invite-token.js';\nimport { ForbiddenError, NotFoundError } from '../shared/errors.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { IdentityService } from '../identity/index.js';\nimport { genId } from '../shared/id.js';\n\nconst INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n\ntype NewOrganizationEvent = NewDomainEvent<OrganizationEvent>;\n\ninterface MemberWrite {\n orgUserId: string;\n actorOrgUserId: string;\n demote: boolean;\n}\n\nexport interface InviteUrls {\n studentPortalUrl: string;\n adminAppUrl: string;\n}\n\nfunction toMember(r: MemberRecord): Member {\n return {\n id: r.id,\n firstName: r.firstName,\n lastName: r.lastName,\n email: r.email,\n image: r.image,\n role: r.role,\n status: r.status,\n joinedAt: r.joinedAt,\n invitedAt: r.invitedAt,\n };\n}\n\nexport type OrganizationServiceParams = {\n repo: OrganizationsRepository;\n membersRepo: MembersRepository;\n people: IdentityService;\n uow: OrganizationsUnitOfWork;\n orgAdmin: () => OrgAdmin;\n logger?: Logger;\n mailer?: Pick<Mailer, 'send'>;\n inviteUrls?: InviteUrls;\n};\n\nexport class OrganizationServiceImpl implements OrganizationService {\n private readonly repo: OrganizationsRepository;\n private readonly membersRepo: MembersRepository;\n private readonly people: IdentityService;\n private readonly uow: OrganizationsUnitOfWork;\n private readonly orgAdmin: () => OrgAdmin;\n private readonly logger: Logger;\n private readonly mailer?: Pick<Mailer, 'send'>;\n private readonly inviteUrls?: InviteUrls;\n private readonly memberWrites = new AsyncLocalStorage<MemberWrite>();\n\n constructor(params: OrganizationServiceParams) {\n this.repo = params.repo;\n this.membersRepo = params.membersRepo;\n this.people = params.people;\n this.uow = params.uow;\n this.orgAdmin = params.orgAdmin;\n this.logger = params.logger ?? noopLogger;\n this.mailer = params.mailer;\n this.inviteUrls = params.inviteUrls;\n }\n\n async createOrganization(input: CreateOrganizationInput): Promise<Organization> {\n const created = await this.uow.run(async ({ organizations, outbox }) => {\n const id = genId('organization');\n\n const created = await organizations.create({\n id,\n // The auth provider adopts this id for its own org row, so the org is\n // its own external id — a lookup by either id lands on the same row.\n externalId: input.externalId ?? id,\n name: input.name ?? `My Org`,\n slug: input.slug ?? '',\n ownerId: input.ownerId,\n });\n await outbox.append([\n organizationEvents.organizationCreated.make({\n orgId: created.id,\n data: created,\n }),\n ]);\n return created;\n });\n this.logger.info('organization created', { orgId: created.id, slug: created.slug });\n return created;\n }\n\n async updateOrganization(id: string, input: UpdateOrganizationInput): Promise<Organization> {\n const updatedOrg = await this.uow.run(async ({ organizations, outbox }) => {\n const updated = await organizations.update(id, {\n name: input.name,\n slug: input.slug,\n });\n if (!updated) {\n throw new NotFoundError('Organization', id);\n }\n await outbox.append([\n organizationEvents.organizationUpdated.make({ orgId: id, data: updated }),\n ]);\n return updated;\n });\n\n this.logger.info('organization updated', { updatedOrg });\n return updatedOrg!;\n }\n\n async deleteOrganization(id: string): Promise<Organization> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const deleted = await organizations.delete(id);\n\n await outbox.append([\n organizationEvents.organizationDeleted.make({\n orgId: deleted.id,\n data: deleted,\n }),\n ]);\n return deleted;\n });\n this.logger.info('organization deleted', { orgId: deleted.id });\n return deleted;\n }\n\n async addOrgUser({ orgId, userId, role }: AddOrgUserInput): Promise<OrgUser> {\n const existing = await this.repo.findOrgUser(orgId, userId);\n if (existing) {\n return this.updateOrgUser(orgId, userId, { role, status: 'active' });\n }\n const orgUser = await this.uow.run(async ({ organizations, outbox }) => {\n const created = await organizations.createOrgUser({ orgId, role, userId, status: 'active' });\n await outbox.append([organizationEvents.orgUserLinked.make({ orgId, data: created })]);\n return created;\n });\n this.logger.info('user added to organization', { orgId, userId, role });\n return orgUser;\n }\n\n async removeOrgUser(orgId: string, userId: string): Promise<OrgUser> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const orgUser = await organizations.findOrgUser(orgId, userId);\n if (!orgUser) {\n throw new NotFoundError('Member', userId);\n }\n const write = this.memberWrite(orgUser.id);\n if (write?.demote) {\n const demoted = await organizations.updateOrgUser(orgId, orgUser.id, {\n role: STUDENT_ROLE,\n });\n if (!demoted) {\n throw new NotFoundError('Member', orgUser.id);\n }\n if (orgUser.role !== STUDENT_ROLE) {\n await outbox.append([\n organizationEvents.orgUserUpdated.make({\n orgId,\n data: demoted,\n metadata: { actorOrgUserId: write.actorOrgUserId },\n }),\n ]);\n }\n return demoted;\n }\n if (orgUser.role === STUDENT_ROLE) {\n return orgUser;\n }\n const deletedUser = await organizations.deleteOrgUser(orgId, orgUser.id);\n if (!deletedUser) {\n throw new NotFoundError('Member', orgUser.id);\n }\n await outbox.append([\n organizationEvents.orgUserDeleted.make({\n orgId,\n data: deletedUser,\n }),\n ]);\n return deletedUser;\n });\n this.logger.info('org user removed', { orgId, userId, orgUserId: deleted.id });\n return deleted;\n }\n\n async updateOrgUser(orgId: string, userId: string, patch: UpdateOrgUserInput): Promise<OrgUser> {\n const updated = await this.uow.run(async ({ organizations, outbox }) => {\n const orgUser = await organizations.findOrgUser(orgId, userId);\n if (!orgUser) {\n throw new NotFoundError('Member', userId);\n }\n const next = await organizations.updateOrgUser(orgId, orgUser.id, patch);\n if (!next) {\n throw new NotFoundError('Member', orgUser.id);\n }\n if (patch.role !== undefined && patch.role !== orgUser.role) {\n const actorOrgUserId = this.memberWrite(orgUser.id)?.actorOrgUserId;\n await outbox.append([\n organizationEvents.orgUserUpdated.make({\n orgId,\n data: next,\n ...(actorOrgUserId !== undefined && { metadata: { actorOrgUserId } }),\n }),\n ]);\n }\n return next;\n });\n this.logger.info('org user updated', { orgId, userId, orgUserId: updated.id, patch });\n return updated;\n }\n\n async createInvite(actor: Actor, input: CreateInviteInput): Promise<Invite> {\n const { orgId, email, role, inviterUserId, firstName, lastName } = input;\n this.assertManagesUsers(actor, orgId);\n this.assertNotOwner(role, orgId);\n const sendEmail = input.sendEmail ?? true;\n const { token, tokenHash } = generateInviteToken();\n const expiresAt = new Date(Date.now() + INVITE_TTL_MS);\n\n // A student belongs to the org from the moment an admin adds them, not from\n // the moment they click a link — so the person exists before the invite\n // does. Staff are excluded: their org_users row is mirrored from the auth\n // provider's member record, which does not exist until they join.\n const person =\n role === STUDENT_ROLE\n ? ((await this.people.getUserByEmail(email)) ??\n (await this.people.createUser({\n email,\n ...(firstName !== undefined && { firstName }),\n ...(lastName !== undefined && { lastName }),\n })))\n : null;\n\n const invite = await this.uow.run(async ({ organizations, outbox }) => {\n const events: NewOrganizationEvent[] = [];\n if (person) {\n const orgUser = await organizations.createOrgUser({\n orgId,\n userId: person.id,\n role: STUDENT_ROLE,\n status: 'invited',\n });\n events.push(organizationEvents.studentCreated.make({ orgId, data: orgUser }));\n }\n const row = await organizations.upsertPendingInvite(orgId, {\n email,\n role,\n invitedBy: inviterUserId,\n tokenHash,\n expiresAt,\n });\n events.push(organizationEvents.inviteCreated.make({ orgId, data: row }));\n await outbox.append(events);\n return row;\n });\n\n if (sendEmail) {\n // TODO make this workflow durable\n try {\n await this.sendInviteEmail(invite, token);\n } catch (err) {\n // A failed email must not abort invite creation: the token is already\n // minted and recorded, so the admin can fix transport and resend.\n this.logger.error('failed to send invite email', {\n orgId,\n inviteId: invite.id,\n role: invite.role,\n err: err instanceof Error ? err : new Error(String(err)),\n });\n }\n }\n\n this.logger.info('invite created', { orgId, inviteId: invite.id, role, sendEmail });\n return invite;\n }\n\n async resendStudentInvite(actor: Actor, input: ResendStudentInviteInput): Promise<void> {\n const { orgId, orgUserId, inviterUserId } = input;\n this.assertManagesUsers(actor, orgId);\n const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);\n if (!orgUser || orgUser.role !== STUDENT_ROLE) {\n throw new NotFoundError('Student', orgUserId);\n }\n if (orgUser.status === 'active') {\n throw new OrganizationRuleError('That student has already accepted their invitation');\n }\n const person = await this.people.getUserById(orgUser.userId);\n if (!person) {\n throw new NotFoundError('User', orgUser.userId);\n }\n const pending = await this.repo.findPendingInvite(orgId, person.email);\n if (!pending) {\n throw new NotFoundError('Invite', person.email);\n }\n const { token, tokenHash } = generateInviteToken();\n const expiresAt = new Date(Date.now() + INVITE_TTL_MS);\n const invite = await this.repo.setInviteToken(orgId, pending.id, tokenHash, expiresAt);\n if (!invite) {\n throw new NotFoundError('Invite', pending.id);\n }\n await this.sendInviteEmail(invite, token);\n this.logger.info('student invite resent', {\n orgId,\n orgUserId,\n inviteId: invite.id,\n inviterUserId,\n });\n }\n\n async peekInvite(token: string): Promise<Invite | null> {\n const invite = await this.repo.findInviteByTokenHash(hashInviteToken(token));\n if (!invite || invite.status !== 'pending') {\n return null;\n }\n if (invite.expiresAt && invite.expiresAt < new Date()) {\n return null;\n }\n return invite;\n }\n\n async acceptInvite(input: AcceptInviteInput): Promise<OrgUser> {\n const invite = await this.peekInvite(input.token);\n if (!invite) {\n this.logger.warn('invite accept refused: token invalid or expired');\n throw new InviteError('invalid invite');\n }\n if (invite.email.toLowerCase() !== input.email.toLowerCase()) {\n this.logger.warn('invite accept refused: email mismatch', {\n orgId: invite.orgId,\n inviteId: invite.id,\n });\n throw new InviteError('emails must match');\n }\n const org = await this.repo.findById(invite.orgId);\n if (!org) {\n throw new InviteError('Org not found');\n }\n\n const orgUser =\n invite.role === STUDENT_ROLE\n ? await this.acceptStudentInvite(invite, input.userId)\n : await this.acceptStaffInvite(invite, org, input.userId);\n this.logger.info('invite accepted', { orgId: invite.orgId, userId: input.userId });\n return orgUser;\n }\n\n private async acceptStudentInvite(invite: Invite, userId: string): Promise<OrgUser> {\n return this.uow.run(async ({ organizations, outbox }) => {\n const existing = await organizations.findOrgUser(invite.orgId, userId);\n if (!existing) {\n throw new NotFoundError('Member', userId);\n }\n const activated = await organizations.updateOrgUser(invite.orgId, existing.id, {\n status: 'active',\n });\n if (!activated) {\n throw new NotFoundError('Member', existing.id);\n }\n const acceptedInvite = await organizations.setInviteStatus(\n invite.orgId,\n invite.id,\n 'accepted',\n );\n if (!acceptedInvite) {\n throw new NotFoundError('Invite', invite.id);\n }\n await outbox.append([\n organizationEvents.inviteAccepted.make({ orgId: invite.orgId, data: acceptedInvite }),\n organizationEvents.studentLinked.make({ orgId: invite.orgId, data: activated }),\n ]);\n return activated;\n });\n }\n\n private async acceptStaffInvite(\n invite: Invite,\n org: Organization,\n userId: string,\n ): Promise<OrgUser> {\n const person = await this.people.getUserById(userId);\n if (!person?.externalId) {\n throw new NotFoundError('User', userId);\n }\n await this.orgAdmin().grantMembership(org.externalId ?? org.id, person.externalId, invite.role);\n return this.uow.run(async ({ organizations, outbox }) => {\n const orgUser = await organizations.findOrgUser(invite.orgId, userId);\n if (!orgUser) {\n throw new NotFoundError('Member', userId);\n }\n const acceptedInvite = await organizations.setInviteStatus(\n invite.orgId,\n invite.id,\n 'accepted',\n );\n if (!acceptedInvite) {\n throw new NotFoundError('Invite', invite.id);\n }\n await outbox.append([\n organizationEvents.inviteAccepted.make({ orgId: invite.orgId, data: acceptedInvite }),\n ]);\n return orgUser;\n });\n }\n\n // By identity USER id — `(org_id, user_id)` is unique. Every caller (scope\n // resolution, the MCP principal, the member routes) asks \"what is this person\n // in this org\", never \"what is this org_users row\".\n getOrgUserById(orgId: string, id: string): Promise<OrgUser | null> {\n return this.repo.findOrgUserById(orgId, id);\n }\n\n async getOrgUser(orgId: string, userId: string): Promise<OrgUser | null> {\n return this.repo.findOrgUser(orgId, userId);\n }\n\n async deleteOrgUser(orgId: string, actor: Actor, id: string): Promise<OrgUser> {\n this.assertManagesUsers(actor, orgId);\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const orgUser = await organizations.findOrgUserById(orgId, id);\n if (!orgUser || orgUser.role !== STUDENT_ROLE) {\n throw new NotFoundError('Student', id);\n }\n const deletedUser = await organizations.deleteOrgUser(orgId, id);\n if (!deletedUser) {\n throw new NotFoundError('Student', id);\n }\n await outbox.append([organizationEvents.studentDeleted.make({ orgId, data: orgUser })]);\n return deletedUser;\n });\n this.logger.info('org user deleted', { orgId, orgUserId: id });\n return deleted;\n }\n\n async getOrgUsersForUser(userId: string): Promise<OrgUser[]> {\n return this.repo.findOrgUsersByUser(userId);\n }\n\n async getById(id: string): Promise<Organization | null> {\n return this.repo.findById(id);\n }\n\n private async sendInviteEmail(invite: Invite, token: string): Promise<void> {\n if (!this.mailer || !this.inviteUrls) {\n throw new Error('invite delivery is not configured (mailer / invite urls missing)');\n }\n const { email, role } = invite;\n const isStudent = role === STUDENT_ROLE;\n const base = isStudent\n ? `${this.inviteUrls.studentPortalUrl}/welcome`\n : `${this.inviteUrls.adminAppUrl}/invite`;\n // The portal resolves the token to the invited email, so the link carries\n // only the token. The admin app still reads it from the query.\n const query = isStudent\n ? new URLSearchParams({ token })\n : new URLSearchParams({ token, email });\n const inviteUrl = `${base}?${query.toString()}`;\n if (role === STUDENT_ROLE) {\n await this.mailer.send(email, 'studentInvite', { inviteUrl, studentName: email });\n } else {\n await this.mailer.send(email, 'memberInvite', {\n inviteUrl,\n inviterName: 'Your team',\n role,\n });\n }\n }\n\n async getByExternalId(externalId: string): Promise<Organization | null> {\n return this.repo.findByExternalId(externalId);\n }\n\n async getBySlug(slug: string): Promise<Organization | null> {\n return this.repo.findBySlug(slug);\n }\n\n staffOrgUserIds(orgId: string): Promise<string[]> {\n return this.repo.findOrgUserIdsByRole(orgId, [...STAFF_ROLES]);\n }\n\n listMembers(orgId: string, query: MembersQuery): Promise<Page<Member>> {\n return this.membersRepo.list(orgId, query);\n }\n\n async updateMemberRole(\n ctx: MemberWriteContext,\n actor: Actor,\n id: string,\n role: Role,\n ): Promise<MemberRoleChange | null> {\n this.assertManagesUsers(actor, ctx.orgId);\n this.assertNotOwner(role, ctx.orgId);\n const member = await this.membersRepo.findById(ctx.orgId, id);\n if (!member) {\n return this.promoteStudent(ctx, actor, id, role);\n }\n if (member.role === 'owner') {\n this.logger.warn('role change rejected: owner role immutable', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('The owner role cannot be reassigned');\n }\n if (member.kind !== 'member' || !member.userExternalId) {\n this.logger.warn('role change rejected: not an active member', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('Only active members can have their role changed');\n }\n if (role === STUDENT_ROLE) {\n return this.demoteModerator(ctx, actor, member, member.userExternalId);\n }\n const userExternalId = member.userExternalId;\n await this.asActor(actor, id, false, () =>\n this.orgAdmin().updateRole(ctx, userExternalId, role),\n );\n const updated = await this.membersRepo.findById(ctx.orgId, id);\n this.logger.info('member role updated', { orgId: ctx.orgId, memberId: id, role });\n return updated ? toMember(updated) : null;\n }\n\n private async demoteModerator(\n ctx: MemberWriteContext,\n actor: Actor,\n member: MemberRecord,\n userExternalId: string,\n ): Promise<MemberRoleChange | null> {\n if (member.role !== 'moderator') {\n this.logger.warn('role change rejected: only a moderator becomes a student', {\n orgId: ctx.orgId,\n memberId: member.id,\n });\n throw new OrganizationRuleError('Only a moderator can be made a student');\n }\n await this.asActor(actor, member.id, true, () =>\n this.orgAdmin().removeMember(ctx, userExternalId),\n );\n const orgUser = await this.repo.findOrgUserById(ctx.orgId, member.id);\n if (!orgUser) {\n return null;\n }\n this.logger.info('moderator made a student', { orgId: ctx.orgId, memberId: member.id });\n return { ...toMember(member), role: orgUser.role, status: orgUser.status };\n }\n\n private async promoteStudent(\n ctx: MemberWriteContext,\n actor: Actor,\n id: string,\n role: Role,\n ): Promise<MemberRoleChange | null> {\n const orgUser = await this.repo.findOrgUserById(ctx.orgId, id);\n if (!orgUser) {\n return null;\n }\n if (orgUser.role !== STUDENT_ROLE || role === STUDENT_ROLE) {\n this.logger.warn('role change rejected: not an active member', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('Only active members can have their role changed');\n }\n const person = await this.people.getUserById(orgUser.userId);\n if (!person?.externalId) {\n this.logger.warn('role change rejected: student has no account', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('That student has not joined yet');\n }\n const org = await this.repo.findById(ctx.orgId);\n if (!org) {\n return null;\n }\n const orgExternalId = org.externalId ?? org.id;\n const userExternalId = person.externalId;\n await this.asActor(actor, orgUser.id, false, async () => {\n await this.orgAdmin().grantMembership(orgExternalId, userExternalId, role);\n if (!(await this.membersRepo.findById(ctx.orgId, id))) {\n await this.orgAdmin().updateRole(ctx, userExternalId, role);\n }\n });\n const updated = await this.membersRepo.findById(ctx.orgId, id);\n if (!updated) {\n this.logger.warn('role change rejected: membership not mirrored', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('The role change did not take effect');\n }\n this.logger.info('student promoted', { orgId: ctx.orgId, memberId: id, role });\n return toMember(updated);\n }\n\n private asActor<T>(\n actor: Actor,\n orgUserId: string,\n demote: boolean,\n write: () => Promise<T>,\n ): Promise<T> {\n return this.memberWrites.run({ orgUserId, actorOrgUserId: actor.id, demote }, write);\n }\n\n private memberWrite(orgUserId: string): MemberWrite | undefined {\n const write = this.memberWrites.getStore();\n return write?.orgUserId === orgUserId ? write : undefined;\n }\n\n private assertManagesUsers(actor: Actor, orgId: string): void {\n if (capability(actor.role, 'manage_users') !== true) {\n this.logger.warn('member write rejected: actor cannot manage users', {\n orgId,\n actorOrgUserId: actor.id,\n });\n throw new ForbiddenError('Only an owner or admin can manage members');\n }\n }\n\n private assertNotOwner(role: Role, orgId: string): void {\n if (role === 'owner') {\n this.logger.warn('member write rejected: owner cannot be granted', { orgId });\n throw new OrganizationRuleError('Ownership cannot be granted');\n }\n }\n\n async removeMember(ctx: MemberWriteContext, actor: Actor, id: string): Promise<boolean> {\n this.assertManagesUsers(actor, ctx.orgId);\n const member = await this.membersRepo.findById(ctx.orgId, id);\n if (!member) {\n return false;\n }\n if (member.role === 'owner') {\n this.logger.warn('member removal rejected: owner cannot be removed', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('The owner cannot be removed');\n }\n if (member.kind === 'member' && member.userExternalId) {\n await this.orgAdmin().removeMember(ctx, member.userExternalId);\n } else if (member.kind === 'invite' && member.inviteId) {\n const inviteId = member.inviteId;\n await this.uow.run(async ({ organizations, outbox }) => {\n const canceled = await organizations.setInviteStatus(ctx.orgId, inviteId, 'canceled');\n if (!canceled) {\n throw new NotFoundError('Invite', inviteId);\n }\n await outbox.append([\n organizationEvents.inviteCanceled.make({ orgId: ctx.orgId, data: canceled }),\n ]);\n });\n }\n this.logger.info('member removed', { orgId: ctx.orgId, memberId: id });\n return true;\n }\n}\n"],"mappings":"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"}
|