@open-mercato/core 0.6.4-develop.4236.1.9fa6806b34 → 0.6.4-develop.4254.1.7a123d970c
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/helpers/integration/authFixtures.js +70 -1
- package/dist/helpers/integration/authFixtures.js.map +2 -2
- package/dist/helpers/integration/dbFixtures.js +98 -0
- package/dist/helpers/integration/dbFixtures.js.map +7 -0
- package/dist/modules/business_rules/api/execute/route.js +2 -1
- package/dist/modules/business_rules/api/execute/route.js.map +2 -2
- package/dist/modules/business_rules/api/rules/route.js +10 -0
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/backend/logs/[id]/page.js +24 -5
- package/dist/modules/business_rules/backend/logs/[id]/page.js.map +2 -2
- package/dist/modules/business_rules/cli.js +6 -0
- package/dist/modules/business_rules/cli.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-engine.js +116 -9
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/business_rules/subscribers/crud-rule-trigger.js +3 -2
- package/dist/modules/business_rules/subscribers/crud-rule-trigger.js.map +2 -2
- package/dist/modules/catalog/api/offers/route.js +15 -5
- package/dist/modules/catalog/api/offers/route.js.map +2 -2
- package/dist/modules/catalog/api/products/route.js +21 -4
- package/dist/modules/catalog/api/products/route.js.map +2 -2
- package/dist/modules/catalog/lib/pricing.js +6 -0
- package/dist/modules/catalog/lib/pricing.js.map +2 -2
- package/dist/modules/catalog/services/catalogPricingService.js +5 -1
- package/dist/modules/catalog/services/catalogPricingService.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/[id]/page.js +19 -2
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +27 -7
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +27 -7
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js.map +2 -2
- package/dist/modules/customers/api/activities/route.js +15 -2
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/comments/route.js +15 -3
- package/dist/modules/customers/api/comments/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/people/route.js +2 -4
- package/dist/modules/customers/api/companies/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +2 -4
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/companies/route.js +2 -4
- package/dist/modules/customers/api/deals/[id]/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +2 -4
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +2 -9
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/stats/route.js +2 -9
- package/dist/modules/customers/api/deals/[id]/stats/route.js.map +2 -2
- package/dist/modules/customers/api/entity-roles-factory.js +2 -8
- package/dist/modules/customers/api/entity-roles-factory.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/context.js +2 -4
- package/dist/modules/customers/api/people/[id]/companies/context.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +2 -4
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +2 -4
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/[id]/page.js +29 -8
- package/dist/modules/customers/backend/customers/people/[id]/page.js.map +2 -2
- package/dist/modules/directory/utils/organizationScopeGuard.js +22 -0
- package/dist/modules/directory/utils/organizationScopeGuard.js.map +7 -0
- package/dist/modules/progress/acl.js +8 -4
- package/dist/modules/progress/acl.js.map +2 -2
- package/dist/modules/workflows/backend/events/[id]/page.js +24 -6
- package/dist/modules/workflows/backend/events/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/instances/[id]/page.js +27 -5
- package/dist/modules/workflows/backend/instances/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.js +25 -6
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/cli.js +8 -0
- package/dist/modules/workflows/cli.js.map +2 -2
- package/dist/modules/workflows/lib/seeds.js +8 -4
- package/dist/modules/workflows/lib/seeds.js.map +2 -2
- package/dist/modules/workflows/setup.js +3 -1
- package/dist/modules/workflows/setup.js.map +2 -2
- package/package.json +7 -7
- package/src/helpers/integration/authFixtures.ts +98 -0
- package/src/helpers/integration/dbFixtures.ts +144 -0
- package/src/modules/business_rules/api/execute/route.ts +2 -1
- package/src/modules/business_rules/api/rules/route.ts +10 -0
- package/src/modules/business_rules/backend/logs/[id]/page.tsx +32 -7
- package/src/modules/business_rules/cli.ts +6 -0
- package/src/modules/business_rules/lib/rule-engine.ts +163 -9
- package/src/modules/business_rules/subscribers/crud-rule-trigger.ts +3 -2
- package/src/modules/catalog/api/offers/route.ts +20 -5
- package/src/modules/catalog/api/products/route.ts +23 -4
- package/src/modules/catalog/lib/pricing.ts +9 -0
- package/src/modules/catalog/services/catalogPricingService.ts +6 -0
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +21 -2
- package/src/modules/currencies/i18n/de.json +1 -0
- package/src/modules/currencies/i18n/en.json +1 -0
- package/src/modules/currencies/i18n/es.json +1 -0
- package/src/modules/currencies/i18n/pl.json +1 -0
- package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +34 -11
- package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +34 -11
- package/src/modules/customers/api/activities/route.ts +16 -5
- package/src/modules/customers/api/comments/route.ts +15 -5
- package/src/modules/customers/api/companies/[id]/people/route.ts +2 -4
- package/src/modules/customers/api/companies/[id]/route.ts +2 -5
- package/src/modules/customers/api/deals/[id]/companies/route.ts +2 -4
- package/src/modules/customers/api/deals/[id]/people/route.ts +2 -4
- package/src/modules/customers/api/deals/[id]/route.ts +2 -9
- package/src/modules/customers/api/deals/[id]/stats/route.ts +2 -9
- package/src/modules/customers/api/entity-roles-factory.ts +2 -12
- package/src/modules/customers/api/people/[id]/companies/context.ts +2 -5
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +2 -5
- package/src/modules/customers/api/people/[id]/route.ts +2 -5
- package/src/modules/customers/backend/customers/people/[id]/page.tsx +35 -11
- package/src/modules/directory/utils/organizationScopeGuard.ts +39 -0
- package/src/modules/progress/acl.ts +4 -0
- package/src/modules/workflows/backend/events/[id]/page.tsx +32 -10
- package/src/modules/workflows/backend/instances/[id]/page.tsx +33 -9
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +33 -10
- package/src/modules/workflows/cli.ts +8 -0
- package/src/modules/workflows/i18n/de.json +1 -0
- package/src/modules/workflows/i18n/en.json +1 -0
- package/src/modules/workflows/i18n/es.json +1 -0
- package/src/modules/workflows/i18n/pl.json +1 -0
- package/src/modules/workflows/lib/seeds.ts +13 -3
- package/src/modules/workflows/setup.ts +3 -1
|
@@ -16,6 +16,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
|
16
16
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
17
17
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
18
18
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
19
|
+
import { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
19
20
|
|
|
20
21
|
type UserDetail = {
|
|
21
22
|
id: string
|
|
@@ -147,6 +148,7 @@ export default function CustomerUserDetailPage({ params }: { params?: { id?: str
|
|
|
147
148
|
const [data, setData] = React.useState<UserDetail | null>(null)
|
|
148
149
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
149
150
|
const [error, setError] = React.useState<string | null>(null)
|
|
151
|
+
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
150
152
|
const [isSaving, setIsSaving] = React.useState(false)
|
|
151
153
|
const [editActive, setEditActive] = React.useState<boolean | null>(null)
|
|
152
154
|
const [editDisplayName, setEditDisplayName] = React.useState('')
|
|
@@ -186,7 +188,7 @@ export default function CustomerUserDetailPage({ params }: { params?: { id?: str
|
|
|
186
188
|
|
|
187
189
|
React.useEffect(() => {
|
|
188
190
|
if (!id) {
|
|
189
|
-
|
|
191
|
+
setIsNotFound(true)
|
|
190
192
|
setIsLoading(false)
|
|
191
193
|
return
|
|
192
194
|
}
|
|
@@ -194,6 +196,7 @@ export default function CustomerUserDetailPage({ params }: { params?: { id?: str
|
|
|
194
196
|
async function load() {
|
|
195
197
|
setIsLoading(true)
|
|
196
198
|
setError(null)
|
|
199
|
+
setIsNotFound(false)
|
|
197
200
|
try {
|
|
198
201
|
const payload = await readApiResultOrThrow<UserDetail>(
|
|
199
202
|
`/api/customer_accounts/admin/users/${encodeURIComponent(id!)}`,
|
|
@@ -209,8 +212,12 @@ export default function CustomerUserDetailPage({ params }: { params?: { id?: str
|
|
|
209
212
|
setEditCustomerEntityId(payload.customerEntityId)
|
|
210
213
|
} catch (err) {
|
|
211
214
|
if (cancelled) return
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
if ((err as { status?: number }).status === 404) {
|
|
216
|
+
setIsNotFound(true)
|
|
217
|
+
} else {
|
|
218
|
+
const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.error.load', 'Failed to load user')
|
|
219
|
+
setError(message)
|
|
220
|
+
}
|
|
214
221
|
} finally {
|
|
215
222
|
if (!cancelled) setIsLoading(false)
|
|
216
223
|
}
|
|
@@ -463,18 +470,34 @@ export default function CustomerUserDetailPage({ params }: { params?: { id?: str
|
|
|
463
470
|
)
|
|
464
471
|
}
|
|
465
472
|
|
|
473
|
+
if (isNotFound) {
|
|
474
|
+
return (
|
|
475
|
+
<Page>
|
|
476
|
+
<PageBody>
|
|
477
|
+
<RecordNotFoundState
|
|
478
|
+
label={t('customer_accounts.admin.detail.error.notFound', 'User not found')}
|
|
479
|
+
backHref="/backend/customer_accounts/users"
|
|
480
|
+
backLabel={t('customer_accounts.admin.detail.actions.backToList', 'Back to list')}
|
|
481
|
+
/>
|
|
482
|
+
</PageBody>
|
|
483
|
+
</Page>
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
|
|
466
487
|
if (error || !data) {
|
|
467
488
|
return (
|
|
468
489
|
<Page>
|
|
469
490
|
<PageBody>
|
|
470
|
-
<
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
<
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
491
|
+
<ErrorMessage
|
|
492
|
+
label={error ?? t('customer_accounts.admin.detail.error.notFound', 'User not found')}
|
|
493
|
+
action={
|
|
494
|
+
<Button asChild variant="outline" size="sm">
|
|
495
|
+
<Link href="/backend/customer_accounts/users">
|
|
496
|
+
{t('customer_accounts.admin.detail.actions.backToList', 'Back to list')}
|
|
497
|
+
</Link>
|
|
498
|
+
</Button>
|
|
499
|
+
}
|
|
500
|
+
/>
|
|
478
501
|
</PageBody>
|
|
479
502
|
</Page>
|
|
480
503
|
)
|
|
@@ -185,7 +185,7 @@ function paginateActivityItems(
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
async function decorateActivityItems(
|
|
188
|
+
export async function decorateActivityItems(
|
|
189
189
|
em: EntityManager,
|
|
190
190
|
items: ActivityItem[],
|
|
191
191
|
decryptionScope?: { tenantId: string; organizationId: string },
|
|
@@ -207,12 +207,23 @@ async function decorateActivityItems(
|
|
|
207
207
|
),
|
|
208
208
|
)
|
|
209
209
|
|
|
210
|
+
if (dealIds.length > 0 && (!decryptionScope?.tenantId || !decryptionScope?.organizationId)) {
|
|
211
|
+
const { translate } = await resolveTranslations()
|
|
212
|
+
throw new CrudHttpError(400, {
|
|
213
|
+
error: translate('customers.errors.tenant_required', 'Tenant context is required'),
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
210
217
|
const [users, deals] = await Promise.all([
|
|
211
218
|
authorIds.length > 0 ? em.find(User, { id: { $in: authorIds } }) : Promise.resolve([]),
|
|
212
|
-
dealIds.length > 0
|
|
213
|
-
?
|
|
214
|
-
|
|
215
|
-
|
|
219
|
+
dealIds.length > 0 && decryptionScope
|
|
220
|
+
? findWithDecryption(
|
|
221
|
+
em,
|
|
222
|
+
CustomerDeal,
|
|
223
|
+
{ id: { $in: dealIds }, tenantId: decryptionScope.tenantId, organizationId: decryptionScope.organizationId },
|
|
224
|
+
undefined,
|
|
225
|
+
decryptionScope,
|
|
226
|
+
)
|
|
216
227
|
: Promise.resolve([]),
|
|
217
228
|
])
|
|
218
229
|
|
|
@@ -139,13 +139,23 @@ const crud = makeCrudRoute({
|
|
|
139
139
|
),
|
|
140
140
|
)
|
|
141
141
|
if (!dealIds.length) return
|
|
142
|
+
const tenantId = ctx.auth?.tenantId ?? null
|
|
143
|
+
const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
|
|
144
|
+
if (!tenantId || !organizationId) {
|
|
145
|
+
const { translate } = await resolveTranslations()
|
|
146
|
+
throw new CrudHttpError(400, {
|
|
147
|
+
error: translate('customers.errors.tenant_required', 'Tenant context is required'),
|
|
148
|
+
})
|
|
149
|
+
}
|
|
142
150
|
try {
|
|
143
151
|
const em = (ctx.container.resolve('em') as EntityManager)
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
const deals = await findWithDecryption(
|
|
153
|
+
em,
|
|
154
|
+
CustomerDeal,
|
|
155
|
+
{ id: { $in: dealIds }, tenantId, organizationId },
|
|
156
|
+
undefined,
|
|
157
|
+
{ tenantId, organizationId },
|
|
158
|
+
)
|
|
149
159
|
const map = new Map<string, string>()
|
|
150
160
|
deals.forEach((deal: CustomerDeal) => {
|
|
151
161
|
if (deal.id) map.set(deal.id, deal.title ?? '')
|
|
@@ -8,6 +8,7 @@ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/d
|
|
|
8
8
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
9
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
10
10
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
11
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
11
12
|
import {
|
|
12
13
|
CustomerEntity,
|
|
13
14
|
CustomerPersonCompanyLink,
|
|
@@ -115,10 +116,7 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
|
|
|
115
116
|
throw new CrudHttpError(404, { error: translate('customers.errors.company_not_found', 'Company not found') })
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((entry) => allowedOrgIds.add(entry))
|
|
120
|
-
else if (auth.orgId) allowedOrgIds.add(auth.orgId)
|
|
121
|
-
if (allowedOrgIds.size > 0 && company.organizationId && !allowedOrgIds.has(company.organizationId)) {
|
|
119
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: company.organizationId })) {
|
|
122
120
|
throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })
|
|
123
121
|
}
|
|
124
122
|
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
withActiveCustomerPersonCompanyLinkFilter,
|
|
48
48
|
} from '../../../lib/personCompanyLinkTable'
|
|
49
49
|
import { normalizeCustomerDetailCustomFields } from '../../detailCustomFields'
|
|
50
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
50
51
|
|
|
51
52
|
export const metadata = {
|
|
52
53
|
GET: { requireAuth: true, requireFeatures: ['customers.companies.view'] },
|
|
@@ -386,11 +387,7 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
|
|
|
386
387
|
if (!company) return notFound('Company not found')
|
|
387
388
|
|
|
388
389
|
if (auth.tenantId && company.tenantId !== auth.tenantId) return notFound('Company not found')
|
|
389
|
-
|
|
390
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((id) => allowedOrgIds.add(id))
|
|
391
|
-
else if (auth.orgId) allowedOrgIds.add(auth.orgId)
|
|
392
|
-
|
|
393
|
-
if (allowedOrgIds.size && company.organizationId && !allowedOrgIds.has(company.organizationId)) {
|
|
390
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: company.organizationId })) {
|
|
394
391
|
return forbidden('Access denied')
|
|
395
392
|
}
|
|
396
393
|
|
|
@@ -8,6 +8,7 @@ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/d
|
|
|
8
8
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
9
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
10
10
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
11
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
11
12
|
import { CustomerCompanyProfile, CustomerDeal, CustomerDealCompanyLink, CustomerEntity } from '../../../../data/entities'
|
|
12
13
|
|
|
13
14
|
const paramsSchema = z.object({
|
|
@@ -97,10 +98,7 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
|
|
|
97
98
|
throw new CrudHttpError(404, { error: translate('customers.errors.deal_not_found', 'Deal not found') })
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((entry) => allowedOrgIds.add(entry))
|
|
102
|
-
else if (auth.orgId) allowedOrgIds.add(auth.orgId)
|
|
103
|
-
if (allowedOrgIds.size > 0 && deal.organizationId && !allowedOrgIds.has(deal.organizationId)) {
|
|
101
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {
|
|
104
102
|
throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })
|
|
105
103
|
}
|
|
106
104
|
|
|
@@ -8,6 +8,7 @@ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/d
|
|
|
8
8
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
9
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
10
10
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
11
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
11
12
|
import { CustomerDeal, CustomerDealPersonLink, CustomerEntity } from '../../../../data/entities'
|
|
12
13
|
|
|
13
14
|
const paramsSchema = z.object({
|
|
@@ -97,10 +98,7 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
|
|
|
97
98
|
throw new CrudHttpError(404, { error: translate('customers.errors.deal_not_found', 'Deal not found') })
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((entry) => allowedOrgIds.add(entry))
|
|
102
|
-
else if (auth.orgId) allowedOrgIds.add(auth.orgId)
|
|
103
|
-
if (allowedOrgIds.size > 0 && deal.organizationId && !allowedOrgIds.has(deal.organizationId)) {
|
|
101
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {
|
|
104
102
|
throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })
|
|
105
103
|
}
|
|
106
104
|
|
|
@@ -22,6 +22,7 @@ import { E } from '#generated/entities.ids.generated'
|
|
|
22
22
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
23
23
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
24
24
|
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
25
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
25
26
|
import { decryptEntitiesWithFallbackScope } from '@open-mercato/shared/lib/encryption/subscriber'
|
|
26
27
|
import { isMissingDealStageTransitionTable, warnMissingDealStageTransitionTable } from '../../../lib/dealStageTransitionTable'
|
|
27
28
|
|
|
@@ -377,15 +378,7 @@ export async function GET(request: Request, context: { params?: Record<string, u
|
|
|
377
378
|
return notFound('Deal not found')
|
|
378
379
|
}
|
|
379
380
|
|
|
380
|
-
|
|
381
|
-
if (Array.isArray(scope?.filterIds)) {
|
|
382
|
-
scope.filterIds.forEach((id) => {
|
|
383
|
-
if (typeof id === 'string' && id.trim().length) allowedOrgIds.add(id)
|
|
384
|
-
})
|
|
385
|
-
} else if (auth.orgId) {
|
|
386
|
-
allowedOrgIds.add(auth.orgId)
|
|
387
|
-
}
|
|
388
|
-
if (allowedOrgIds.size && deal.organizationId && !allowedOrgIds.has(deal.organizationId)) {
|
|
381
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {
|
|
389
382
|
return forbidden('Access denied')
|
|
390
383
|
}
|
|
391
384
|
|
|
@@ -10,6 +10,7 @@ import { DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/en
|
|
|
10
10
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
11
11
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
12
12
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
13
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
13
14
|
|
|
14
15
|
export const metadata = {
|
|
15
16
|
GET: { requireAuth: true, requireFeatures: ['customers.deals.view'] },
|
|
@@ -97,15 +98,7 @@ export async function GET(request: Request, context: { params?: Record<string, u
|
|
|
97
98
|
return notFound(translate('customers.errors.deal_not_found', 'Deal not found'))
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
if (Array.isArray(scope?.filterIds)) {
|
|
102
|
-
scope.filterIds.forEach((id) => {
|
|
103
|
-
if (typeof id === 'string' && id.trim().length) allowedOrgIds.add(id)
|
|
104
|
-
})
|
|
105
|
-
} else if (auth.orgId) {
|
|
106
|
-
allowedOrgIds.add(auth.orgId)
|
|
107
|
-
}
|
|
108
|
-
if (allowedOrgIds.size && deal.organizationId && !allowedOrgIds.has(deal.organizationId)) {
|
|
101
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {
|
|
109
102
|
return forbidden(translate('customers.errors.access_denied', 'Access denied'))
|
|
110
103
|
}
|
|
111
104
|
|
|
@@ -7,6 +7,7 @@ import { validateCrudMutationGuard, runCrudMutationGuardAfterSuccess } from '@op
|
|
|
7
7
|
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
8
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
9
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
10
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
10
11
|
import { User } from '@open-mercato/core/modules/auth/data/entities'
|
|
11
12
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
12
13
|
import { CustomerEntity, CustomerEntityRole } from '../data/entities'
|
|
@@ -69,24 +70,13 @@ async function buildContext(request: Request) {
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
function collectAllowedOrganizationIds(
|
|
73
|
-
scope: Awaited<ReturnType<typeof resolveCustomersRequestContext>>['scope'],
|
|
74
|
-
auth: Awaited<ReturnType<typeof resolveCustomersRequestContext>>['auth'],
|
|
75
|
-
) {
|
|
76
|
-
const allowedOrgIds = new Set<string>()
|
|
77
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((id) => allowedOrgIds.add(id))
|
|
78
|
-
else if (auth.orgId) allowedOrgIds.add(auth.orgId)
|
|
79
|
-
return allowedOrgIds
|
|
80
|
-
}
|
|
81
|
-
|
|
82
73
|
function ensureRouteOrganizationAccess(
|
|
83
74
|
organizationId: string,
|
|
84
75
|
scope: Awaited<ReturnType<typeof resolveCustomersRequestContext>>['scope'],
|
|
85
76
|
auth: Awaited<ReturnType<typeof resolveCustomersRequestContext>>['auth'],
|
|
86
77
|
translate: Translator,
|
|
87
78
|
) {
|
|
88
|
-
|
|
89
|
-
if (allowedOrgIds.size > 0 && !allowedOrgIds.has(organizationId)) {
|
|
79
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId })) {
|
|
90
80
|
throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })
|
|
91
81
|
}
|
|
92
82
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CustomerPersonProfile,
|
|
8
8
|
} from '@open-mercato/core/modules/customers/data/entities'
|
|
9
9
|
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
10
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
10
11
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
11
12
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
12
13
|
|
|
@@ -37,11 +38,7 @@ export async function loadPersonContext(req: Request, personId: string) {
|
|
|
37
38
|
throw new CrudHttpError(404, { error: translate('customers.errors.person_not_found', 'Person not found') })
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((entry) => allowedOrgIds.add(entry))
|
|
42
|
-
else if (authenticatedAuth.orgId) allowedOrgIds.add(authenticatedAuth.orgId)
|
|
43
|
-
|
|
44
|
-
if (allowedOrgIds.size > 0 && !allowedOrgIds.has(person.organizationId)) {
|
|
41
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth: authenticatedAuth, organizationId: person.organizationId })) {
|
|
45
42
|
throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })
|
|
46
43
|
}
|
|
47
44
|
|
|
@@ -8,6 +8,7 @@ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/d
|
|
|
8
8
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
9
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
10
10
|
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
11
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
11
12
|
import {
|
|
12
13
|
CustomerEntity,
|
|
13
14
|
CustomerCompanyProfile,
|
|
@@ -173,11 +174,7 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
|
|
|
173
174
|
throw new CrudHttpError(404, { error: translate('customers.errors.person_not_found', 'Person not found') })
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
|
|
177
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((entry) => allowedOrgIds.add(entry))
|
|
178
|
-
else if (auth.orgId) allowedOrgIds.add(auth.orgId)
|
|
179
|
-
|
|
180
|
-
if (allowedOrgIds.size > 0 && !allowedOrgIds.has(person.organizationId)) {
|
|
177
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: person.organizationId })) {
|
|
181
178
|
throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })
|
|
182
179
|
}
|
|
183
180
|
|
|
@@ -37,6 +37,7 @@ import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
|
37
37
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
38
38
|
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
39
39
|
import { parseBooleanFromUnknown, parseBooleanToken } from '@open-mercato/shared/lib/boolean'
|
|
40
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
40
41
|
import { loadPersonCompanyLinks, summarizePersonCompanies } from '../../../lib/personCompanies'
|
|
41
42
|
import { normalizeCustomerDetailCustomFields } from '../../detailCustomFields'
|
|
42
43
|
|
|
@@ -474,11 +475,7 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
|
|
|
474
475
|
profileMeta = { reason: 'person_tenant_mismatch' }
|
|
475
476
|
return notFound('Person not found')
|
|
476
477
|
}
|
|
477
|
-
|
|
478
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((id) => allowedOrgIds.add(id))
|
|
479
|
-
else if (auth.orgId) allowedOrgIds.add(auth.orgId)
|
|
480
|
-
|
|
481
|
-
if (allowedOrgIds.size && person.organizationId && !allowedOrgIds.has(person.organizationId)) {
|
|
478
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: person.organizationId })) {
|
|
482
479
|
statusCode = 403
|
|
483
480
|
profileMeta = { reason: 'organization_forbidden' }
|
|
484
481
|
return forbidden('Access denied')
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
NotesSection,
|
|
21
21
|
type CommentSummary,
|
|
22
22
|
type SectionAction,
|
|
23
|
+
RecordNotFoundState,
|
|
24
|
+
ErrorMessage,
|
|
23
25
|
} from '@open-mercato/ui/backend/detail'
|
|
24
26
|
import {
|
|
25
27
|
TagsSection,
|
|
@@ -116,6 +118,7 @@ export default function CustomerPersonDetailPage({ params }: { params?: { id?: s
|
|
|
116
118
|
const [data, setData] = React.useState<PersonOverview | null>(null)
|
|
117
119
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
118
120
|
const [error, setError] = React.useState<string | null>(null)
|
|
121
|
+
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
119
122
|
const [activeTab, setActiveTab] = React.useState<SectionKey>(initialTab)
|
|
120
123
|
const [sectionAction, setSectionAction] = React.useState<SectionAction | null>(null)
|
|
121
124
|
const [isDeleting, setIsDeleting] = React.useState(false)
|
|
@@ -276,7 +279,7 @@ export default function CustomerPersonDetailPage({ params }: { params?: { id?: s
|
|
|
276
279
|
const initialLoadDoneRef = React.useRef(false)
|
|
277
280
|
const loadData = React.useCallback(async () => {
|
|
278
281
|
if (!id) {
|
|
279
|
-
|
|
282
|
+
setIsNotFound(true)
|
|
280
283
|
setIsLoading(false)
|
|
281
284
|
return
|
|
282
285
|
}
|
|
@@ -284,6 +287,7 @@ export default function CustomerPersonDetailPage({ params }: { params?: { id?: s
|
|
|
284
287
|
setIsLoading(true)
|
|
285
288
|
}
|
|
286
289
|
setError(null)
|
|
290
|
+
setIsNotFound(false)
|
|
287
291
|
try {
|
|
288
292
|
const payload = await readApiResultOrThrow<PersonOverview>(
|
|
289
293
|
`/api/customers/people/${encodeURIComponent(id)}?include=todos`,
|
|
@@ -292,8 +296,12 @@ export default function CustomerPersonDetailPage({ params }: { params?: { id?: s
|
|
|
292
296
|
)
|
|
293
297
|
setData(payload as PersonOverview)
|
|
294
298
|
} catch (err) {
|
|
295
|
-
|
|
296
|
-
|
|
299
|
+
if ((err as { status?: number }).status === 404) {
|
|
300
|
+
setIsNotFound(true)
|
|
301
|
+
} else {
|
|
302
|
+
const message = err instanceof Error ? err.message : t('customers.people.detail.error.load')
|
|
303
|
+
setError(message)
|
|
304
|
+
}
|
|
297
305
|
if (!initialLoadDoneRef.current) setData(null)
|
|
298
306
|
} finally {
|
|
299
307
|
setIsLoading(false)
|
|
@@ -469,18 +477,34 @@ export default function CustomerPersonDetailPage({ params }: { params?: { id?: s
|
|
|
469
477
|
)
|
|
470
478
|
}
|
|
471
479
|
|
|
480
|
+
if (isNotFound) {
|
|
481
|
+
return (
|
|
482
|
+
<Page>
|
|
483
|
+
<PageBody>
|
|
484
|
+
<RecordNotFoundState
|
|
485
|
+
label={t('customers.people.detail.error.notFound', 'Person not found.')}
|
|
486
|
+
backHref="/backend/customers/people"
|
|
487
|
+
backLabel={t('customers.people.detail.actions.backToList', 'Back to people')}
|
|
488
|
+
/>
|
|
489
|
+
</PageBody>
|
|
490
|
+
</Page>
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
472
494
|
if (error || !data || !personId) {
|
|
473
495
|
return (
|
|
474
496
|
<Page>
|
|
475
497
|
<PageBody>
|
|
476
|
-
<
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
<
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
498
|
+
<ErrorMessage
|
|
499
|
+
label={error ?? t('customers.people.detail.error.notFound', 'Person not found.')}
|
|
500
|
+
action={
|
|
501
|
+
<Button asChild variant="outline" size="sm">
|
|
502
|
+
<Link href="/backend/customers/people">
|
|
503
|
+
{t('customers.people.detail.actions.backToList', 'Back to people')}
|
|
504
|
+
</Link>
|
|
505
|
+
</Button>
|
|
506
|
+
}
|
|
507
|
+
/>
|
|
484
508
|
</PageBody>
|
|
485
509
|
</Page>
|
|
486
510
|
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
2
|
+
import { isOrganizationAccessAllowed } from '@open-mercato/shared/lib/auth/organizationAccess'
|
|
3
|
+
import type { OrganizationScope } from './organizationScope'
|
|
4
|
+
|
|
5
|
+
export type OrganizationReadAccessInput = {
|
|
6
|
+
scope: OrganizationScope | null | undefined
|
|
7
|
+
auth: AuthContext
|
|
8
|
+
organizationId: string | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fail-closed read guard for single-record detail routes. Centralizes the
|
|
13
|
+
* decision so callers keep their own deny mechanism (throw / return response)
|
|
14
|
+
* and their own i18n key.
|
|
15
|
+
*
|
|
16
|
+
* Unrestricted access (super admin or `scope.allowedIds === null`) is the only
|
|
17
|
+
* bypass. For a restricted principal the allowed set is derived the same way
|
|
18
|
+
* the detail routes always have (`filterIds` narrows the active view, else the
|
|
19
|
+
* principal's home org); an empty derived set denies instead of skipping.
|
|
20
|
+
*/
|
|
21
|
+
export function isOrganizationReadAccessAllowed(input: OrganizationReadAccessInput): boolean {
|
|
22
|
+
const isSuperAdmin = input.auth?.isSuperAdmin === true
|
|
23
|
+
if (isSuperAdmin || input.scope?.allowedIds === null) return true
|
|
24
|
+
|
|
25
|
+
const allowedOrganizationIds = new Set<string>()
|
|
26
|
+
if (input.scope?.filterIds?.length) {
|
|
27
|
+
for (const id of input.scope.filterIds) {
|
|
28
|
+
if (typeof id === 'string' && id.trim().length) allowedOrganizationIds.add(id)
|
|
29
|
+
}
|
|
30
|
+
} else if (input.auth?.orgId) {
|
|
31
|
+
allowedOrganizationIds.add(input.auth.orgId)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return isOrganizationAccessAllowed({
|
|
35
|
+
isSuperAdmin,
|
|
36
|
+
allowedOrganizationIds: Array.from(allowedOrganizationIds),
|
|
37
|
+
targetOrganizationId: input.organizationId,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -8,21 +8,25 @@ export const features = [
|
|
|
8
8
|
id: 'progress.create',
|
|
9
9
|
title: 'Create progress jobs',
|
|
10
10
|
module: 'progress',
|
|
11
|
+
dependsOn: ['progress.view'],
|
|
11
12
|
},
|
|
12
13
|
{
|
|
13
14
|
id: 'progress.update',
|
|
14
15
|
title: 'Update progress jobs',
|
|
15
16
|
module: 'progress',
|
|
17
|
+
dependsOn: ['progress.view'],
|
|
16
18
|
},
|
|
17
19
|
{
|
|
18
20
|
id: 'progress.cancel',
|
|
19
21
|
title: 'Cancel progress jobs',
|
|
20
22
|
module: 'progress',
|
|
23
|
+
dependsOn: ['progress.view'],
|
|
21
24
|
},
|
|
22
25
|
{
|
|
23
26
|
id: 'progress.manage',
|
|
24
27
|
title: 'Manage all progress jobs',
|
|
25
28
|
module: 'progress',
|
|
29
|
+
dependsOn: ['progress.view'],
|
|
26
30
|
},
|
|
27
31
|
]
|
|
28
32
|
|
|
@@ -11,6 +11,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
11
11
|
import { FormHeader } from '@open-mercato/ui/backend/forms'
|
|
12
12
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
13
13
|
import { JsonDisplay } from '@open-mercato/ui/backend/JsonDisplay'
|
|
14
|
+
import { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
14
15
|
|
|
15
16
|
type WorkflowEvent = {
|
|
16
17
|
id: string
|
|
@@ -55,8 +56,13 @@ export default function WorkflowEventDetailPage() {
|
|
|
55
56
|
const response = await apiFetch(`/api/workflows/events/${eventId}`)
|
|
56
57
|
|
|
57
58
|
if (!response.ok) {
|
|
58
|
-
const
|
|
59
|
-
|
|
59
|
+
const httpErr = new Error(
|
|
60
|
+
response.status === 404
|
|
61
|
+
? t('workflows.events.notFound', 'Event not found.')
|
|
62
|
+
: t('workflows.events.messages.loadFailed')
|
|
63
|
+
) as Error & { status: number }
|
|
64
|
+
httpErr.status = response.status
|
|
65
|
+
throw httpErr
|
|
60
66
|
}
|
|
61
67
|
const result = await response.json()
|
|
62
68
|
return result as WorkflowEvent
|
|
@@ -77,18 +83,34 @@ export default function WorkflowEventDetailPage() {
|
|
|
77
83
|
)
|
|
78
84
|
}
|
|
79
85
|
|
|
86
|
+
const isNotFound = !isLoading && (error as (Error & { status?: number }) | null)?.status === 404
|
|
87
|
+
|
|
88
|
+
if (isNotFound) {
|
|
89
|
+
return (
|
|
90
|
+
<Page>
|
|
91
|
+
<PageBody>
|
|
92
|
+
<RecordNotFoundState
|
|
93
|
+
label={t('workflows.events.notFound', 'Event not found.')}
|
|
94
|
+
backHref="/backend/events"
|
|
95
|
+
backLabel={t('workflows.events.backToList', 'Back to Events')}
|
|
96
|
+
/>
|
|
97
|
+
</PageBody>
|
|
98
|
+
</Page>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
80
102
|
if (error || !event) {
|
|
81
103
|
return (
|
|
82
104
|
<Page>
|
|
83
105
|
<PageBody>
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
{t('workflows.events.backToList')}
|
|
89
|
-
</
|
|
90
|
-
|
|
91
|
-
|
|
106
|
+
<ErrorMessage
|
|
107
|
+
label={(error as Error | null)?.message ?? t('workflows.events.messages.loadFailed')}
|
|
108
|
+
action={
|
|
109
|
+
<Button asChild variant="outline" size="sm">
|
|
110
|
+
<Link href="/backend/events">{t('workflows.events.backToList', 'Back to Events')}</Link>
|
|
111
|
+
</Button>
|
|
112
|
+
}
|
|
113
|
+
/>
|
|
92
114
|
</PageBody>
|
|
93
115
|
</Page>
|
|
94
116
|
)
|