@open-mercato/core 0.6.6-develop.6429.1.2fba7258d1 → 0.6.6-develop.6450.1.b493fb430c
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/api/people/[id]/route.js +73 -2
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/planner/commands/shared.js +1 -8
- package/dist/modules/planner/commands/shared.js.map +2 -2
- package/dist/modules/resources/commands/shared.js +1 -7
- package/dist/modules/resources/commands/shared.js.map +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/points/route.js +2 -2
- package/dist/modules/shipping_carriers/api/points/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/rates/route.js +2 -2
- package/dist/modules/shipping_carriers/api/rates/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/tracking/refresh/route.js +2 -2
- package/dist/modules/shipping_carriers/api/tracking/refresh/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/tracking/route.js +2 -2
- package/dist/modules/shipping_carriers/api/tracking/route.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/upstream-error-response.js +13 -0
- package/dist/modules/shipping_carriers/lib/upstream-error-response.js.map +7 -0
- package/dist/modules/staff/commands/activities.js +46 -8
- package/dist/modules/staff/commands/activities.js.map +2 -2
- package/dist/modules/staff/commands/addresses.js +40 -7
- package/dist/modules/staff/commands/addresses.js.map +2 -2
- package/dist/modules/staff/commands/comments.js +46 -8
- package/dist/modules/staff/commands/comments.js.map +2 -2
- package/dist/modules/staff/commands/job-histories.js +40 -7
- package/dist/modules/staff/commands/job-histories.js.map +2 -2
- package/dist/modules/staff/commands/leave-requests.js +31 -9
- package/dist/modules/staff/commands/leave-requests.js.map +2 -2
- package/dist/modules/staff/commands/shared.js +56 -7
- package/dist/modules/staff/commands/shared.js.map +2 -2
- package/dist/modules/staff/commands/tag-assignments.js +18 -7
- package/dist/modules/staff/commands/tag-assignments.js.map +2 -2
- package/dist/modules/staff/commands/team-members.js +16 -5
- package/dist/modules/staff/commands/team-members.js.map +2 -2
- package/dist/modules/staff/commands/team-roles.js +16 -5
- package/dist/modules/staff/commands/team-roles.js.map +2 -2
- package/dist/modules/staff/commands/teams.js +16 -5
- package/dist/modules/staff/commands/teams.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-entries.js +17 -5
- package/dist/modules/staff/commands/timesheets-entries.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-projects.js +22 -9
- package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
- package/dist/modules/sync_excel/lib/upload-storage.js +9 -6
- package/dist/modules/sync_excel/lib/upload-storage.js.map +2 -2
- package/dist/modules/translations/api/get/locales.js +2 -1
- package/dist/modules/translations/api/get/locales.js.map +2 -2
- package/dist/modules/translations/api/put/locales.js +1 -1
- package/dist/modules/translations/api/put/locales.js.map +2 -2
- package/dist/modules/workflows/examples/sales-pipeline-definition.json +8 -8
- package/dist/modules/workflows/lib/activity-executor.js +39 -3
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/api/people/[id]/route.ts +107 -2
- package/src/modules/planner/commands/shared.ts +2 -11
- package/src/modules/resources/commands/shared.ts +2 -10
- package/src/modules/shipping_carriers/api/cancel/route.ts +2 -2
- package/src/modules/shipping_carriers/api/points/route.ts +2 -2
- package/src/modules/shipping_carriers/api/rates/route.ts +2 -2
- package/src/modules/shipping_carriers/api/shipments/route.ts +2 -2
- package/src/modules/shipping_carriers/api/tracking/refresh/route.ts +2 -2
- package/src/modules/shipping_carriers/api/tracking/route.ts +2 -2
- package/src/modules/shipping_carriers/i18n/de.json +1 -0
- package/src/modules/shipping_carriers/i18n/en.json +1 -0
- package/src/modules/shipping_carriers/i18n/es.json +1 -0
- package/src/modules/shipping_carriers/i18n/pl.json +1 -0
- package/src/modules/shipping_carriers/lib/upstream-error-response.ts +11 -0
- package/src/modules/staff/commands/activities.ts +46 -8
- package/src/modules/staff/commands/addresses.ts +40 -7
- package/src/modules/staff/commands/comments.ts +46 -8
- package/src/modules/staff/commands/job-histories.ts +40 -7
- package/src/modules/staff/commands/leave-requests.ts +36 -9
- package/src/modules/staff/commands/shared.ts +71 -8
- package/src/modules/staff/commands/tag-assignments.ts +18 -7
- package/src/modules/staff/commands/team-members.ts +16 -5
- package/src/modules/staff/commands/team-roles.ts +16 -5
- package/src/modules/staff/commands/teams.ts +16 -5
- package/src/modules/staff/commands/timesheets-entries.ts +17 -5
- package/src/modules/staff/commands/timesheets-projects.ts +22 -9
- package/src/modules/sync_excel/lib/upload-storage.ts +8 -6
- package/src/modules/translations/api/get/locales.ts +1 -0
- package/src/modules/translations/api/put/locales.ts +1 -1
- package/src/modules/workflows/AGENTS.md +2 -1
- package/src/modules/workflows/examples/sales-pipeline-definition.json +8 -8
- package/src/modules/workflows/lib/activity-executor.ts +49 -5
|
@@ -14,7 +14,16 @@ import {
|
|
|
14
14
|
type StaffTeamMemberCommentUpdateInput,
|
|
15
15
|
} from '../data/validators'
|
|
16
16
|
import { staffTeamMemberCommentCrudEvents } from '../lib/crud'
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
applyScopeToWhere,
|
|
19
|
+
commandActorScope,
|
|
20
|
+
commandInputScope,
|
|
21
|
+
ensureOrganizationScope,
|
|
22
|
+
ensureTenantScope,
|
|
23
|
+
explicitStaffCommandScope,
|
|
24
|
+
extractUndoPayload,
|
|
25
|
+
requireTeamMember,
|
|
26
|
+
} from './shared'
|
|
18
27
|
import { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'
|
|
19
28
|
import { E } from '#generated/entities.ids.generated'
|
|
20
29
|
|
|
@@ -62,10 +71,16 @@ const createCommentCommand: CommandHandler<
|
|
|
62
71
|
const parsed = staffTeamMemberCommentCreateSchema.parse(rawInput)
|
|
63
72
|
ensureTenantScope(ctx, parsed.tenantId)
|
|
64
73
|
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
74
|
+
const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
|
|
65
75
|
const normalizedAuthor = normalizeAuthorUserId(parsed.authorUserId, ctx.auth)
|
|
66
76
|
|
|
67
77
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
68
|
-
const member = await requireTeamMember(
|
|
78
|
+
const member = await requireTeamMember(
|
|
79
|
+
em,
|
|
80
|
+
parsed.entityId,
|
|
81
|
+
scope,
|
|
82
|
+
'Team member not found',
|
|
83
|
+
)
|
|
69
84
|
ensureTenantScope(ctx, member.tenantId)
|
|
70
85
|
ensureOrganizationScope(ctx, member.organizationId)
|
|
71
86
|
|
|
@@ -151,7 +166,12 @@ const createCommentCommand: CommandHandler<
|
|
|
151
166
|
appearanceColor: snapshot.appearanceColor,
|
|
152
167
|
}),
|
|
153
168
|
beforeRestore: async ({ em, snapshot }) => {
|
|
154
|
-
const member = await requireTeamMember(
|
|
169
|
+
const member = await requireTeamMember(
|
|
170
|
+
em,
|
|
171
|
+
snapshot.memberId,
|
|
172
|
+
explicitStaffCommandScope(snapshot.tenantId, snapshot.organizationId),
|
|
173
|
+
'Team member not found',
|
|
174
|
+
)
|
|
155
175
|
return { member }
|
|
156
176
|
},
|
|
157
177
|
buildResult: (entity) => ({ commentId: entity.id, authorUserId: entity.authorUserId ?? null }),
|
|
@@ -169,13 +189,17 @@ const updateCommentCommand: CommandHandler<StaffTeamMemberCommentUpdateInput, {
|
|
|
169
189
|
async execute(rawInput, ctx) {
|
|
170
190
|
const parsed = staffTeamMemberCommentUpdateSchema.parse(rawInput)
|
|
171
191
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
172
|
-
const
|
|
192
|
+
const scope = commandActorScope(ctx)
|
|
193
|
+
const comment = await em.findOne(
|
|
194
|
+
StaffTeamMemberComment,
|
|
195
|
+
applyScopeToWhere<StaffTeamMemberComment>({ id: parsed.id }, scope),
|
|
196
|
+
)
|
|
173
197
|
if (!comment) throw new CrudHttpError(404, { error: 'Comment not found' })
|
|
174
198
|
ensureTenantScope(ctx, comment.tenantId)
|
|
175
199
|
ensureOrganizationScope(ctx, comment.organizationId)
|
|
176
200
|
|
|
177
201
|
if (parsed.entityId !== undefined) {
|
|
178
|
-
const member = await requireTeamMember(em, parsed.entityId, 'Team member not found')
|
|
202
|
+
const member = await requireTeamMember(em, parsed.entityId, scope, 'Team member not found')
|
|
179
203
|
ensureTenantScope(ctx, member.tenantId)
|
|
180
204
|
ensureOrganizationScope(ctx, member.organizationId)
|
|
181
205
|
comment.member = member
|
|
@@ -245,7 +269,12 @@ const updateCommentCommand: CommandHandler<StaffTeamMemberCommentUpdateInput, {
|
|
|
245
269
|
if (!before) return
|
|
246
270
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
247
271
|
let comment = await em.findOne(StaffTeamMemberComment, { id: before.id })
|
|
248
|
-
const member = await requireTeamMember(
|
|
272
|
+
const member = await requireTeamMember(
|
|
273
|
+
em,
|
|
274
|
+
before.memberId,
|
|
275
|
+
explicitStaffCommandScope(before.tenantId, before.organizationId),
|
|
276
|
+
'Team member not found',
|
|
277
|
+
)
|
|
249
278
|
|
|
250
279
|
if (!comment) {
|
|
251
280
|
comment = em.create(StaffTeamMemberComment, {
|
|
@@ -298,7 +327,11 @@ const deleteCommentCommand: CommandHandler<{ body?: Record<string, unknown>; que
|
|
|
298
327
|
async execute(input, ctx) {
|
|
299
328
|
const id = requireId(input, 'Comment id required')
|
|
300
329
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
301
|
-
const
|
|
330
|
+
const scope = commandActorScope(ctx)
|
|
331
|
+
const comment = await em.findOne(
|
|
332
|
+
StaffTeamMemberComment,
|
|
333
|
+
applyScopeToWhere<StaffTeamMemberComment>({ id }, scope),
|
|
334
|
+
)
|
|
302
335
|
if (!comment) throw new CrudHttpError(404, { error: 'Comment not found' })
|
|
303
336
|
ensureTenantScope(ctx, comment.tenantId)
|
|
304
337
|
ensureOrganizationScope(ctx, comment.organizationId)
|
|
@@ -345,7 +378,12 @@ const deleteCommentCommand: CommandHandler<{ body?: Record<string, unknown>; que
|
|
|
345
378
|
const before = payload?.before
|
|
346
379
|
if (!before) return
|
|
347
380
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
348
|
-
const member = await requireTeamMember(
|
|
381
|
+
const member = await requireTeamMember(
|
|
382
|
+
em,
|
|
383
|
+
before.memberId,
|
|
384
|
+
explicitStaffCommandScope(before.tenantId, before.organizationId),
|
|
385
|
+
'Team member not found',
|
|
386
|
+
)
|
|
349
387
|
let comment = await em.findOne(StaffTeamMemberComment, { id: before.id })
|
|
350
388
|
if (!comment) {
|
|
351
389
|
comment = em.create(StaffTeamMemberComment, {
|
|
@@ -12,8 +12,12 @@ import {
|
|
|
12
12
|
} from '../data/validators'
|
|
13
13
|
import { staffTeamMemberJobHistoryCrudEvents } from '../lib/crud'
|
|
14
14
|
import {
|
|
15
|
+
applyScopeToWhere,
|
|
16
|
+
commandActorScope,
|
|
17
|
+
commandInputScope,
|
|
15
18
|
ensureOrganizationScope,
|
|
16
19
|
ensureTenantScope,
|
|
20
|
+
explicitStaffCommandScope,
|
|
17
21
|
extractUndoPayload,
|
|
18
22
|
requireTeamMember,
|
|
19
23
|
} from './shared'
|
|
@@ -74,9 +78,15 @@ const createJobHistoryCommand: CommandHandler<StaffTeamMemberJobHistoryCreateInp
|
|
|
74
78
|
const parsed = staffTeamMemberJobHistoryCreateSchema.parse(rawInput)
|
|
75
79
|
ensureTenantScope(ctx, parsed.tenantId)
|
|
76
80
|
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
81
|
+
const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
|
|
77
82
|
|
|
78
83
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
79
|
-
const member = await requireTeamMember(
|
|
84
|
+
const member = await requireTeamMember(
|
|
85
|
+
em,
|
|
86
|
+
parsed.entityId,
|
|
87
|
+
scope,
|
|
88
|
+
'Team member not found',
|
|
89
|
+
)
|
|
80
90
|
ensureTenantScope(ctx, member.tenantId)
|
|
81
91
|
ensureOrganizationScope(ctx, member.organizationId)
|
|
82
92
|
|
|
@@ -150,7 +160,12 @@ const createJobHistoryCommand: CommandHandler<StaffTeamMemberJobHistoryCreateInp
|
|
|
150
160
|
throw new CrudHttpError(400, { error: '[internal] redo snapshot unavailable for job history create' })
|
|
151
161
|
}
|
|
152
162
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
153
|
-
const member = await requireTeamMember(
|
|
163
|
+
const member = await requireTeamMember(
|
|
164
|
+
em,
|
|
165
|
+
after.memberId,
|
|
166
|
+
explicitStaffCommandScope(after.tenantId, after.organizationId),
|
|
167
|
+
'Team member not found',
|
|
168
|
+
)
|
|
154
169
|
let record = await em.findOne(StaffTeamMemberJobHistory, { id: after.id })
|
|
155
170
|
if (!record) {
|
|
156
171
|
record = em.create(StaffTeamMemberJobHistory, {
|
|
@@ -206,7 +221,11 @@ const updateJobHistoryCommand: CommandHandler<StaffTeamMemberJobHistoryUpdateInp
|
|
|
206
221
|
async execute(rawInput, ctx) {
|
|
207
222
|
const parsed = staffTeamMemberJobHistoryUpdateSchema.parse(rawInput)
|
|
208
223
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
209
|
-
const
|
|
224
|
+
const scope = commandActorScope(ctx)
|
|
225
|
+
const record = await em.findOne(
|
|
226
|
+
StaffTeamMemberJobHistory,
|
|
227
|
+
applyScopeToWhere<StaffTeamMemberJobHistory>({ id: parsed.id }, scope),
|
|
228
|
+
)
|
|
210
229
|
if (!record) {
|
|
211
230
|
enforceRecordGoneIsConflict({
|
|
212
231
|
resourceKind: JOB_HISTORY_LOCK_RESOURCE_KIND,
|
|
@@ -227,7 +246,7 @@ const updateJobHistoryCommand: CommandHandler<StaffTeamMemberJobHistoryUpdateInp
|
|
|
227
246
|
})
|
|
228
247
|
|
|
229
248
|
if (parsed.entityId !== undefined) {
|
|
230
|
-
const member = await requireTeamMember(em, parsed.entityId, 'Team member not found')
|
|
249
|
+
const member = await requireTeamMember(em, parsed.entityId, scope, 'Team member not found')
|
|
231
250
|
ensureTenantScope(ctx, member.tenantId)
|
|
232
251
|
ensureOrganizationScope(ctx, member.organizationId)
|
|
233
252
|
record.member = member
|
|
@@ -298,7 +317,12 @@ const updateJobHistoryCommand: CommandHandler<StaffTeamMemberJobHistoryUpdateInp
|
|
|
298
317
|
if (!before) return
|
|
299
318
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
300
319
|
let record = await em.findOne(StaffTeamMemberJobHistory, { id: before.id })
|
|
301
|
-
const member = await requireTeamMember(
|
|
320
|
+
const member = await requireTeamMember(
|
|
321
|
+
em,
|
|
322
|
+
before.memberId,
|
|
323
|
+
explicitStaffCommandScope(before.tenantId, before.organizationId),
|
|
324
|
+
'Team member not found',
|
|
325
|
+
)
|
|
302
326
|
|
|
303
327
|
if (!record) {
|
|
304
328
|
record = em.create(StaffTeamMemberJobHistory, {
|
|
@@ -354,7 +378,11 @@ const deleteJobHistoryCommand: CommandHandler<{ id?: string; updatedAt?: string;
|
|
|
354
378
|
const id = requireId(input, 'Job history id required')
|
|
355
379
|
const expectedUpdatedAt = typeof input.updatedAt === 'string' ? input.updatedAt : undefined
|
|
356
380
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
357
|
-
const
|
|
381
|
+
const scope = commandActorScope(ctx)
|
|
382
|
+
const record = await em.findOne(
|
|
383
|
+
StaffTeamMemberJobHistory,
|
|
384
|
+
applyScopeToWhere<StaffTeamMemberJobHistory>({ id }, scope),
|
|
385
|
+
)
|
|
358
386
|
if (!record) {
|
|
359
387
|
enforceRecordGoneIsConflict({
|
|
360
388
|
resourceKind: JOB_HISTORY_LOCK_RESOURCE_KIND,
|
|
@@ -416,7 +444,12 @@ const deleteJobHistoryCommand: CommandHandler<{ id?: string; updatedAt?: string;
|
|
|
416
444
|
const before = payload?.before
|
|
417
445
|
if (!before) return
|
|
418
446
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
419
|
-
const member = await requireTeamMember(
|
|
447
|
+
const member = await requireTeamMember(
|
|
448
|
+
em,
|
|
449
|
+
before.memberId,
|
|
450
|
+
explicitStaffCommandScope(before.tenantId, before.organizationId),
|
|
451
|
+
'Team member not found',
|
|
452
|
+
)
|
|
420
453
|
let record = await em.findOne(StaffTeamMemberJobHistory, { id: before.id })
|
|
421
454
|
if (!record) {
|
|
422
455
|
record = em.create(StaffTeamMemberJobHistory, {
|
|
@@ -21,7 +21,17 @@ import {
|
|
|
21
21
|
type StaffLeaveRequestDecisionInput,
|
|
22
22
|
type StaffLeaveRequestUpdateInput,
|
|
23
23
|
} from '../data/validators'
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
commandActorScope,
|
|
26
|
+
commandInputScope,
|
|
27
|
+
ensureOrganizationScope,
|
|
28
|
+
ensureTenantScope,
|
|
29
|
+
explicitStaffCommandScope,
|
|
30
|
+
extractUndoPayload,
|
|
31
|
+
requireTeamMember,
|
|
32
|
+
scopeForDecryption,
|
|
33
|
+
applyScopeToWhere,
|
|
34
|
+
} from './shared'
|
|
25
35
|
import { E } from '#generated/entities.ids.generated'
|
|
26
36
|
import { resolveNotificationService } from '../../notifications/lib/notificationService'
|
|
27
37
|
import { buildFeatureNotificationFromType, buildNotificationFromType } from '../../notifications/lib/notificationBuilder'
|
|
@@ -167,8 +177,18 @@ function leaveRequestSeedFromSnapshot(snapshot: LeaveRequestSnapshot): Record<st
|
|
|
167
177
|
}
|
|
168
178
|
}
|
|
169
179
|
|
|
170
|
-
async function requireLeaveRequest(
|
|
171
|
-
|
|
180
|
+
async function requireLeaveRequest(
|
|
181
|
+
em: EntityManager,
|
|
182
|
+
id: string,
|
|
183
|
+
scope: ReturnType<typeof explicitStaffCommandScope>,
|
|
184
|
+
): Promise<StaffLeaveRequest> {
|
|
185
|
+
const request = await findOneWithDecryption(
|
|
186
|
+
em,
|
|
187
|
+
StaffLeaveRequest,
|
|
188
|
+
applyScopeToWhere<StaffLeaveRequest>({ id, deletedAt: null }, scope),
|
|
189
|
+
undefined,
|
|
190
|
+
scopeForDecryption(scope),
|
|
191
|
+
)
|
|
172
192
|
if (!request) throw new CrudHttpError(404, { error: 'Leave request not found.' })
|
|
173
193
|
return request
|
|
174
194
|
}
|
|
@@ -245,9 +265,15 @@ const createLeaveRequestCommand: CommandHandler<StaffLeaveRequestCreateInput, {
|
|
|
245
265
|
const parsed = staffLeaveRequestCreateSchema.parse(rawInput)
|
|
246
266
|
ensureTenantScope(ctx, parsed.tenantId)
|
|
247
267
|
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
268
|
+
const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
|
|
248
269
|
|
|
249
270
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
250
|
-
const member = await requireTeamMember(
|
|
271
|
+
const member = await requireTeamMember(
|
|
272
|
+
em,
|
|
273
|
+
parsed.memberId,
|
|
274
|
+
scope,
|
|
275
|
+
'Team member not found',
|
|
276
|
+
)
|
|
251
277
|
ensureTenantScope(ctx, member.tenantId)
|
|
252
278
|
ensureOrganizationScope(ctx, member.organizationId)
|
|
253
279
|
|
|
@@ -400,13 +426,14 @@ const updateLeaveRequestCommand: CommandHandler<StaffLeaveRequestUpdateInput, {
|
|
|
400
426
|
async execute(rawInput, ctx) {
|
|
401
427
|
const parsed = staffLeaveRequestUpdateSchema.parse(rawInput)
|
|
402
428
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
403
|
-
const
|
|
429
|
+
const scope = commandActorScope(ctx)
|
|
430
|
+
const request = await requireLeaveRequest(em, parsed.id, scope)
|
|
404
431
|
ensureTenantScope(ctx, request.tenantId)
|
|
405
432
|
ensureOrganizationScope(ctx, request.organizationId)
|
|
406
433
|
ensurePendingStatus(request)
|
|
407
434
|
|
|
408
435
|
if (parsed.memberId !== undefined) {
|
|
409
|
-
const member = await requireTeamMember(em, parsed.memberId, 'Team member not found')
|
|
436
|
+
const member = await requireTeamMember(em, parsed.memberId, scope, 'Team member not found')
|
|
410
437
|
ensureTenantScope(ctx, member.tenantId)
|
|
411
438
|
ensureOrganizationScope(ctx, member.organizationId)
|
|
412
439
|
request.member = member
|
|
@@ -514,7 +541,7 @@ const deleteLeaveRequestCommand: CommandHandler<{ id: string }, { requestId: str
|
|
|
514
541
|
async execute(rawInput, ctx) {
|
|
515
542
|
const parsed = staffLeaveRequestDecisionSchema.pick({ id: true }).parse(rawInput)
|
|
516
543
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
517
|
-
const request = await requireLeaveRequest(em, parsed.id)
|
|
544
|
+
const request = await requireLeaveRequest(em, parsed.id, commandActorScope(ctx))
|
|
518
545
|
ensureTenantScope(ctx, request.tenantId)
|
|
519
546
|
ensureOrganizationScope(ctx, request.organizationId)
|
|
520
547
|
ensurePendingStatus(request)
|
|
@@ -595,7 +622,7 @@ const acceptLeaveRequestCommand: CommandHandler<StaffLeaveRequestDecisionInput,
|
|
|
595
622
|
async execute(rawInput, ctx) {
|
|
596
623
|
const parsed = staffLeaveRequestDecisionSchema.parse(rawInput)
|
|
597
624
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
598
|
-
const request = await requireLeaveRequest(em, parsed.id)
|
|
625
|
+
const request = await requireLeaveRequest(em, parsed.id, commandActorScope(ctx))
|
|
599
626
|
ensureTenantScope(ctx, request.tenantId)
|
|
600
627
|
ensureOrganizationScope(ctx, request.organizationId)
|
|
601
628
|
ensurePendingStatus(request)
|
|
@@ -802,7 +829,7 @@ const rejectLeaveRequestCommand: CommandHandler<StaffLeaveRequestDecisionInput,
|
|
|
802
829
|
async execute(rawInput, ctx) {
|
|
803
830
|
const parsed = staffLeaveRequestDecisionSchema.parse(rawInput)
|
|
804
831
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
805
|
-
const request = await requireLeaveRequest(em, parsed.id)
|
|
832
|
+
const request = await requireLeaveRequest(em, parsed.id, commandActorScope(ctx))
|
|
806
833
|
ensureTenantScope(ctx, request.tenantId)
|
|
807
834
|
ensureOrganizationScope(ctx, request.organizationId)
|
|
808
835
|
ensurePendingStatus(request)
|
|
@@ -1,25 +1,88 @@
|
|
|
1
|
-
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
2
1
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
3
|
-
import { ensureOrganizationScope } from '@open-mercato/shared/lib/commands/scope'
|
|
2
|
+
import { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
|
|
4
3
|
import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
|
|
5
|
-
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
|
+
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
6
5
|
import { StaffTeamMember } from '../data/entities'
|
|
7
6
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export { ensureOrganizationScope, ensureTenantScope, extractUndoPayload }
|
|
8
|
+
|
|
9
|
+
export type StaffCommandScope = {
|
|
10
|
+
tenantId: string | null
|
|
11
|
+
organizationId: string | null
|
|
12
|
+
requireTenant: boolean
|
|
13
|
+
requireOrganization: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function commandActorScope(ctx: CommandRuntimeContext): StaffCommandScope {
|
|
17
|
+
const isPrivilegedActor = ctx.auth?.isSuperAdmin === true || ctx.systemActor === true
|
|
18
|
+
const tenantId = isPrivilegedActor ? null : (ctx.auth?.tenantId ?? ctx.organizationScope?.tenantId ?? null)
|
|
19
|
+
const organizationId = isPrivilegedActor ? null : (ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null)
|
|
20
|
+
const organizationUnrestricted =
|
|
21
|
+
isPrivilegedActor || (organizationId === null && tenantId !== null && ctx.organizationScope?.allowedIds === null)
|
|
22
|
+
return {
|
|
23
|
+
tenantId,
|
|
24
|
+
organizationId: organizationUnrestricted ? null : organizationId,
|
|
25
|
+
requireTenant: !isPrivilegedActor,
|
|
26
|
+
requireOrganization: !organizationUnrestricted,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function explicitStaffCommandScope(tenantId: string | null, organizationId: string | null): StaffCommandScope {
|
|
31
|
+
return {
|
|
32
|
+
tenantId,
|
|
33
|
+
organizationId,
|
|
34
|
+
requireTenant: true,
|
|
35
|
+
requireOrganization: true,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function commandInputScope(ctx: CommandRuntimeContext, tenantId: string, organizationId: string): StaffCommandScope {
|
|
40
|
+
if (ctx.auth?.isSuperAdmin === true || ctx.systemActor === true) {
|
|
41
|
+
return explicitStaffCommandScope(tenantId, organizationId)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ensureTenantScope(ctx, tenantId)
|
|
45
|
+
ensureOrganizationScope(ctx, organizationId)
|
|
46
|
+
|
|
47
|
+
const actorTenantId = ctx.auth?.tenantId ?? ctx.organizationScope?.tenantId ?? null
|
|
48
|
+
if (!actorTenantId || actorTenantId !== tenantId) {
|
|
11
49
|
throw new CrudHttpError(403, { error: 'Forbidden' })
|
|
12
50
|
}
|
|
51
|
+
|
|
52
|
+
if (!ctx.organizationScope) {
|
|
53
|
+
const currentOrganizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
|
|
54
|
+
if (!currentOrganizationId || currentOrganizationId !== organizationId) {
|
|
55
|
+
throw new CrudHttpError(403, { error: 'Forbidden' })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return explicitStaffCommandScope(tenantId, organizationId)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function applyScopeToWhere<TEntity extends object>(
|
|
63
|
+
where: FilterQuery<TEntity>,
|
|
64
|
+
scope: StaffCommandScope,
|
|
65
|
+
): FilterQuery<TEntity> {
|
|
66
|
+
const scoped = { ...(where as Record<string, unknown>) }
|
|
67
|
+
if (scope.requireTenant || scope.tenantId !== null) scoped.tenantId = scope.tenantId
|
|
68
|
+
if (scope.requireOrganization || scope.organizationId !== null) scoped.organizationId = scope.organizationId
|
|
69
|
+
return scoped as FilterQuery<TEntity>
|
|
13
70
|
}
|
|
14
71
|
|
|
15
|
-
export {
|
|
72
|
+
export function scopeForDecryption(scope: StaffCommandScope): { tenantId: string | null; organizationId: string | null } {
|
|
73
|
+
return { tenantId: scope.tenantId, organizationId: scope.organizationId }
|
|
74
|
+
}
|
|
16
75
|
|
|
17
76
|
export async function requireTeamMember(
|
|
18
77
|
em: EntityManager,
|
|
19
78
|
memberId: string,
|
|
79
|
+
scope: StaffCommandScope,
|
|
20
80
|
message = 'Team member not found',
|
|
21
81
|
): Promise<StaffTeamMember> {
|
|
22
|
-
const member = await em.findOne(
|
|
82
|
+
const member = await em.findOne(
|
|
83
|
+
StaffTeamMember,
|
|
84
|
+
applyScopeToWhere<StaffTeamMember>({ id: memberId, deletedAt: null }, scope),
|
|
85
|
+
)
|
|
23
86
|
if (!member) throw new CrudHttpError(404, { error: message })
|
|
24
87
|
return member
|
|
25
88
|
}
|
|
@@ -12,7 +12,15 @@ import {
|
|
|
12
12
|
type StaffTeamMemberTagAssignmentInput,
|
|
13
13
|
} from '../data/validators'
|
|
14
14
|
import { staffTeamMemberCrudEvents } from '../lib/crud'
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
applyScopeToWhere,
|
|
17
|
+
commandInputScope,
|
|
18
|
+
ensureOrganizationScope,
|
|
19
|
+
ensureTenantScope,
|
|
20
|
+
explicitStaffCommandScope,
|
|
21
|
+
extractUndoPayload,
|
|
22
|
+
scopeForDecryption,
|
|
23
|
+
} from './shared'
|
|
16
24
|
|
|
17
25
|
type TeamMemberTagAssignmentSnapshot = {
|
|
18
26
|
tag: string
|
|
@@ -43,12 +51,13 @@ const assignTeamMemberTagCommand: CommandHandler<StaffTeamMemberTagAssignmentInp
|
|
|
43
51
|
ensureTenantScope(ctx, parsed.tenantId)
|
|
44
52
|
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
45
53
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
54
|
+
const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
|
|
46
55
|
const member = await findOneWithDecryption(
|
|
47
56
|
em,
|
|
48
57
|
StaffTeamMember,
|
|
49
|
-
{ id: parsed.memberId, deletedAt: null },
|
|
58
|
+
applyScopeToWhere<StaffTeamMember>({ id: parsed.memberId, deletedAt: null }, scope),
|
|
50
59
|
undefined,
|
|
51
|
-
|
|
60
|
+
scopeForDecryption(scope),
|
|
52
61
|
)
|
|
53
62
|
if (!member) throw new CrudHttpError(404, { error: 'Team member not found.' })
|
|
54
63
|
ensureTenantScope(ctx, member.tenantId)
|
|
@@ -131,12 +140,13 @@ const assignTeamMemberTagCommand: CommandHandler<StaffTeamMemberTagAssignmentInp
|
|
|
131
140
|
const before = payload?.before
|
|
132
141
|
if (!before) return { memberId: logEntry.resourceId ?? '' }
|
|
133
142
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
143
|
+
const scope = explicitStaffCommandScope(before.tenantId, before.organizationId)
|
|
134
144
|
const member = await findOneWithDecryption(
|
|
135
145
|
em,
|
|
136
146
|
StaffTeamMember,
|
|
137
|
-
{ id: before.memberId, deletedAt: null },
|
|
147
|
+
applyScopeToWhere<StaffTeamMember>({ id: before.memberId, deletedAt: null }, scope),
|
|
138
148
|
undefined,
|
|
139
|
-
|
|
149
|
+
scopeForDecryption(scope),
|
|
140
150
|
)
|
|
141
151
|
if (!member) throw new CrudHttpError(404, { error: 'Team member not found.' })
|
|
142
152
|
const currentTags = normalizeTagList(Array.isArray(member.tags) ? member.tags : [])
|
|
@@ -171,12 +181,13 @@ const unassignTeamMemberTagCommand: CommandHandler<StaffTeamMemberTagAssignmentI
|
|
|
171
181
|
ensureTenantScope(ctx, parsed.tenantId)
|
|
172
182
|
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
173
183
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
184
|
+
const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
|
|
174
185
|
const member = await findOneWithDecryption(
|
|
175
186
|
em,
|
|
176
187
|
StaffTeamMember,
|
|
177
|
-
{ id: parsed.memberId, deletedAt: null },
|
|
188
|
+
applyScopeToWhere<StaffTeamMember>({ id: parsed.memberId, deletedAt: null }, scope),
|
|
178
189
|
undefined,
|
|
179
|
-
|
|
190
|
+
scopeForDecryption(scope),
|
|
180
191
|
)
|
|
181
192
|
if (!member) throw new CrudHttpError(404, { error: 'Team member not found.' })
|
|
182
193
|
ensureTenantScope(ctx, member.tenantId)
|
|
@@ -19,7 +19,15 @@ import {
|
|
|
19
19
|
type StaffTeamMemberUpdateInput,
|
|
20
20
|
} from '../data/validators'
|
|
21
21
|
import { staffTeamMemberCrudEvents } from '../lib/crud'
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
applyScopeToWhere,
|
|
24
|
+
commandActorScope,
|
|
25
|
+
commandInputScope,
|
|
26
|
+
ensureOrganizationScope,
|
|
27
|
+
ensureTenantScope,
|
|
28
|
+
extractUndoPayload,
|
|
29
|
+
scopeForDecryption,
|
|
30
|
+
} from './shared'
|
|
23
31
|
import { E } from '#generated/entities.ids.generated'
|
|
24
32
|
|
|
25
33
|
const teamMemberCrudIndexer: CrudIndexerConfig<StaffTeamMember> = {
|
|
@@ -165,6 +173,7 @@ const createTeamMemberCommand: CommandHandler<StaffTeamMemberCreateInput, { memb
|
|
|
165
173
|
const { parsed, custom } = parseWithCustomFields(staffTeamMemberCreateSchema, rawInput)
|
|
166
174
|
ensureTenantScope(ctx, parsed.tenantId)
|
|
167
175
|
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
176
|
+
commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
|
|
168
177
|
|
|
169
178
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
170
179
|
const roleIds = normalizeStringList(parsed.roleIds)
|
|
@@ -305,12 +314,13 @@ const updateTeamMemberCommand: CommandHandler<StaffTeamMemberUpdateInput, { memb
|
|
|
305
314
|
async execute(rawInput, ctx) {
|
|
306
315
|
const { parsed, custom } = parseWithCustomFields(staffTeamMemberUpdateSchema, rawInput)
|
|
307
316
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
317
|
+
const scope = commandActorScope(ctx)
|
|
308
318
|
const member = await findOneWithDecryption(
|
|
309
319
|
em,
|
|
310
320
|
StaffTeamMember,
|
|
311
|
-
{ id: parsed.id, deletedAt: null },
|
|
321
|
+
applyScopeToWhere<StaffTeamMember>({ id: parsed.id, deletedAt: null }, scope),
|
|
312
322
|
undefined,
|
|
313
|
-
|
|
323
|
+
scopeForDecryption(scope),
|
|
314
324
|
)
|
|
315
325
|
if (!member) throw new CrudHttpError(404, { error: 'Team member not found.' })
|
|
316
326
|
ensureTenantScope(ctx, member.tenantId)
|
|
@@ -476,12 +486,13 @@ const deleteTeamMemberCommand: CommandHandler<{ id?: string }, { memberId: strin
|
|
|
476
486
|
const id = input?.id
|
|
477
487
|
if (!id) throw new CrudHttpError(400, { error: 'Member id is required.' })
|
|
478
488
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
489
|
+
const scope = commandActorScope(ctx)
|
|
479
490
|
const member = await findOneWithDecryption(
|
|
480
491
|
em,
|
|
481
492
|
StaffTeamMember,
|
|
482
|
-
{ id, deletedAt: null },
|
|
493
|
+
applyScopeToWhere<StaffTeamMember>({ id, deletedAt: null }, scope),
|
|
483
494
|
undefined,
|
|
484
|
-
|
|
495
|
+
scopeForDecryption(scope),
|
|
485
496
|
)
|
|
486
497
|
if (!member) throw new CrudHttpError(404, { error: 'Team member not found.' })
|
|
487
498
|
ensureTenantScope(ctx, member.tenantId)
|
|
@@ -17,7 +17,15 @@ import {
|
|
|
17
17
|
type StaffTeamRoleUpdateInput,
|
|
18
18
|
} from '../data/validators'
|
|
19
19
|
import { staffTeamRoleCrudEvents } from '../lib/crud'
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
applyScopeToWhere,
|
|
22
|
+
commandActorScope,
|
|
23
|
+
commandInputScope,
|
|
24
|
+
ensureOrganizationScope,
|
|
25
|
+
ensureTenantScope,
|
|
26
|
+
extractUndoPayload,
|
|
27
|
+
scopeForDecryption,
|
|
28
|
+
} from './shared'
|
|
21
29
|
import { E } from '#generated/entities.ids.generated'
|
|
22
30
|
|
|
23
31
|
const teamRoleCrudIndexer: CrudIndexerConfig<StaffTeamRole> = {
|
|
@@ -100,6 +108,7 @@ const createTeamRoleCommand: CommandHandler<StaffTeamRoleCreateInput, { roleId:
|
|
|
100
108
|
const { parsed, custom } = parseWithCustomFields(staffTeamRoleCreateSchema, rawInput)
|
|
101
109
|
ensureTenantScope(ctx, parsed.tenantId)
|
|
102
110
|
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
111
|
+
commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
|
|
103
112
|
|
|
104
113
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
105
114
|
if (parsed.teamId) {
|
|
@@ -230,12 +239,13 @@ const updateTeamRoleCommand: CommandHandler<StaffTeamRoleUpdateInput, { roleId:
|
|
|
230
239
|
async execute(rawInput, ctx) {
|
|
231
240
|
const { parsed, custom } = parseWithCustomFields(staffTeamRoleUpdateSchema, rawInput)
|
|
232
241
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
242
|
+
const scope = commandActorScope(ctx)
|
|
233
243
|
const role = await findOneWithDecryption(
|
|
234
244
|
em,
|
|
235
245
|
StaffTeamRole,
|
|
236
|
-
{ id: parsed.id, deletedAt: null },
|
|
246
|
+
applyScopeToWhere<StaffTeamRole>({ id: parsed.id, deletedAt: null }, scope),
|
|
237
247
|
undefined,
|
|
238
|
-
|
|
248
|
+
scopeForDecryption(scope),
|
|
239
249
|
)
|
|
240
250
|
if (!role) throw new CrudHttpError(404, { error: 'Team role not found.' })
|
|
241
251
|
ensureTenantScope(ctx, role.tenantId)
|
|
@@ -378,12 +388,13 @@ const deleteTeamRoleCommand: CommandHandler<{ id?: string }, { roleId: string }>
|
|
|
378
388
|
const id = input?.id
|
|
379
389
|
if (!id) throw new CrudHttpError(400, { error: 'Role id is required.' })
|
|
380
390
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
391
|
+
const scope = commandActorScope(ctx)
|
|
381
392
|
const role = await findOneWithDecryption(
|
|
382
393
|
em,
|
|
383
394
|
StaffTeamRole,
|
|
384
|
-
{ id, deletedAt: null },
|
|
395
|
+
applyScopeToWhere<StaffTeamRole>({ id, deletedAt: null }, scope),
|
|
385
396
|
undefined,
|
|
386
|
-
|
|
397
|
+
scopeForDecryption(scope),
|
|
387
398
|
)
|
|
388
399
|
if (!role) throw new CrudHttpError(404, { error: 'Team role not found.' })
|
|
389
400
|
ensureTenantScope(ctx, role.tenantId)
|
|
@@ -17,7 +17,15 @@ import {
|
|
|
17
17
|
type StaffTeamUpdateInput,
|
|
18
18
|
} from '../data/validators'
|
|
19
19
|
import { staffTeamCrudEvents } from '../lib/crud'
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
applyScopeToWhere,
|
|
22
|
+
commandActorScope,
|
|
23
|
+
commandInputScope,
|
|
24
|
+
ensureOrganizationScope,
|
|
25
|
+
ensureTenantScope,
|
|
26
|
+
extractUndoPayload,
|
|
27
|
+
scopeForDecryption,
|
|
28
|
+
} from './shared'
|
|
21
29
|
import { E } from '#generated/entities.ids.generated'
|
|
22
30
|
|
|
23
31
|
const teamCrudIndexer: CrudIndexerConfig<StaffTeam> = {
|
|
@@ -77,6 +85,7 @@ const createTeamCommand: CommandHandler<StaffTeamCreateInput, { teamId: string }
|
|
|
77
85
|
const { parsed, custom } = parseWithCustomFields(staffTeamCreateSchema, rawInput)
|
|
78
86
|
ensureTenantScope(ctx, parsed.tenantId)
|
|
79
87
|
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
88
|
+
commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
|
|
80
89
|
|
|
81
90
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
82
91
|
const now = new Date()
|
|
@@ -202,12 +211,13 @@ const updateTeamCommand: CommandHandler<StaffTeamUpdateInput, { teamId: string }
|
|
|
202
211
|
async execute(rawInput, ctx) {
|
|
203
212
|
const { parsed, custom } = parseWithCustomFields(staffTeamUpdateSchema, rawInput)
|
|
204
213
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
214
|
+
const scope = commandActorScope(ctx)
|
|
205
215
|
const team = await findOneWithDecryption(
|
|
206
216
|
em,
|
|
207
217
|
StaffTeam,
|
|
208
|
-
{ id: parsed.id, deletedAt: null },
|
|
218
|
+
applyScopeToWhere<StaffTeam>({ id: parsed.id, deletedAt: null }, scope),
|
|
209
219
|
undefined,
|
|
210
|
-
|
|
220
|
+
scopeForDecryption(scope),
|
|
211
221
|
)
|
|
212
222
|
if (!team) throw new CrudHttpError(404, { error: 'Team not found.' })
|
|
213
223
|
ensureTenantScope(ctx, team.tenantId)
|
|
@@ -339,12 +349,13 @@ const deleteTeamCommand: CommandHandler<{ id?: string }, { teamId: string }> = {
|
|
|
339
349
|
const id = input?.id
|
|
340
350
|
if (!id) throw new CrudHttpError(400, { error: 'Team id is required.' })
|
|
341
351
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
352
|
+
const scope = commandActorScope(ctx)
|
|
342
353
|
const team = await findOneWithDecryption(
|
|
343
354
|
em,
|
|
344
355
|
StaffTeam,
|
|
345
|
-
{ id, deletedAt: null },
|
|
356
|
+
applyScopeToWhere<StaffTeam>({ id, deletedAt: null }, scope),
|
|
346
357
|
undefined,
|
|
347
|
-
|
|
358
|
+
scopeForDecryption(scope),
|
|
348
359
|
)
|
|
349
360
|
if (!team) throw new CrudHttpError(404, { error: 'Team not found.' })
|
|
350
361
|
ensureTenantScope(ctx, team.tenantId)
|