@open-mercato/core 0.6.7-develop.6775.1.c2313bb8a3 → 0.6.7-develop.6784.1.f80b9afce5
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/AGENTS.md +3 -3
- package/agentic/standalone-guide.md +13 -0
- package/dist/generated/entities/organization/index.js +2 -0
- package/dist/generated/entities/organization/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/auth/cli.js +1 -1
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +4 -6
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +1 -0
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/auth/services/rbacService.js +29 -44
- package/dist/modules/auth/services/rbacService.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/product-seo/widget.js +1 -1
- package/dist/modules/catalog/widgets/injection/product-seo/widget.js.map +2 -2
- package/dist/modules/communication_channels/lib/access-control.js +2 -2
- package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
- package/dist/modules/customer_accounts/api/invitations/accept.js +1 -2
- package/dist/modules/customer_accounts/api/invitations/accept.js.map +2 -2
- package/dist/modules/customer_accounts/api/login.js +4 -2
- package/dist/modules/customer_accounts/api/login.js.map +2 -2
- package/dist/modules/customer_accounts/api/magic-link/verify.js +4 -2
- package/dist/modules/customer_accounts/api/magic-link/verify.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/feature-check.js +5 -4
- package/dist/modules/customer_accounts/api/portal/feature-check.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/nav.js +6 -4
- package/dist/modules/customer_accounts/api/portal/nav.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/profile.js +5 -1
- package/dist/modules/customer_accounts/api/portal/profile.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/sessions-refresh.js +3 -3
- package/dist/modules/customer_accounts/api/portal/sessions-refresh.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuth.js +7 -2
- package/dist/modules/customer_accounts/lib/customerAuth.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuthServer.js +2 -1
- package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerRbacService.js +12 -3
- package/dist/modules/customer_accounts/services/customerRbacService.js.map +2 -2
- package/dist/modules/customer_accounts/setup.js +6 -0
- package/dist/modules/customer_accounts/setup.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +1 -1
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +2 -2
- package/dist/modules/customers/lib/visibilityFilter.js +6 -4
- package/dist/modules/customers/lib/visibilityFilter.js.map +2 -2
- package/dist/modules/dashboards/api/layout/[itemId]/route.js +6 -3
- package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
- package/dist/modules/dashboards/api/layout/route.js +22 -9
- package/dist/modules/dashboards/api/layout/route.js.map +2 -2
- package/dist/modules/dashboards/api/roles/widgets/route.js +9 -3
- package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/users/widgets/route.js +9 -3
- package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/catalog.js +6 -3
- package/dist/modules/dashboards/api/widgets/catalog.js.map +2 -2
- package/dist/modules/dashboards/lib/access.js +5 -3
- package/dist/modules/dashboards/lib/access.js.map +2 -2
- package/dist/modules/directory/api/organization-branding/route.js +10 -3
- package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
- package/dist/modules/directory/api/organizations/route.js +7 -0
- package/dist/modules/directory/api/organizations/route.js.map +3 -3
- package/dist/modules/directory/backend/directory/branding/page.js +31 -8
- package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
- package/dist/modules/directory/commands/organizations.js +8 -1
- package/dist/modules/directory/commands/organizations.js.map +2 -2
- package/dist/modules/directory/data/entities.js +4 -0
- package/dist/modules/directory/data/entities.js.map +2 -2
- package/dist/modules/directory/data/validators.js +2 -0
- package/dist/modules/directory/data/validators.js.map +2 -2
- package/dist/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.js +13 -0
- package/dist/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.js.map +7 -0
- package/dist/modules/entities/lib/entityAcl.js +47 -15
- package/dist/modules/entities/lib/entityAcl.js.map +3 -3
- package/dist/modules/inbox_ops/ai-tools.js +5 -2
- package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
- package/dist/modules/messages/lib/routeHelpers.js +1 -3
- package/dist/modules/messages/lib/routeHelpers.js.map +2 -2
- package/dist/modules/notifications/lib/notificationRecipients.js +6 -7
- package/dist/modules/notifications/lib/notificationRecipients.js.map +2 -2
- package/dist/modules/query_index/di.js +3 -0
- package/dist/modules/query_index/di.js.map +2 -2
- package/dist/modules/query_index/lib/coverage.js +2 -2
- package/dist/modules/query_index/lib/coverage.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +83 -17
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +13 -7
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/query_index/subscribers/coverage_refresh.js +4 -0
- package/dist/modules/query_index/subscribers/coverage_refresh.js.map +2 -2
- package/dist/modules/query_index/subscribers/delete_one.js +53 -1
- package/dist/modules/query_index/subscribers/delete_one.js.map +3 -3
- package/dist/modules/query_index/subscribers/upsert_one.js +60 -0
- package/dist/modules/query_index/subscribers/upsert_one.js.map +3 -3
- package/dist/modules/sales/commands/documents.js +17 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/staff/api/interceptors.js +5 -3
- package/dist/modules/staff/api/interceptors.js.map +2 -2
- package/dist/modules/sync_excel/api/upload/route.js +15 -2
- package/dist/modules/sync_excel/api/upload/route.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +7 -7
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/generated/entities/organization/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/auth/AGENTS.md +5 -3
- package/src/modules/auth/cli.ts +1 -1
- package/src/modules/auth/lib/backendChrome.tsx +5 -11
- package/src/modules/auth/lib/setup-app.ts +1 -0
- package/src/modules/auth/services/rbacService.ts +41 -46
- package/src/modules/catalog/widgets/injection/product-seo/widget.ts +1 -1
- package/src/modules/communication_channels/lib/access-control.ts +3 -3
- package/src/modules/customer_accounts/AGENTS.md +3 -3
- package/src/modules/customer_accounts/api/invitations/accept.ts +1 -2
- package/src/modules/customer_accounts/api/login.ts +4 -2
- package/src/modules/customer_accounts/api/magic-link/verify.ts +4 -2
- package/src/modules/customer_accounts/api/portal/feature-check.ts +5 -7
- package/src/modules/customer_accounts/api/portal/nav.ts +5 -3
- package/src/modules/customer_accounts/api/portal/profile.ts +5 -1
- package/src/modules/customer_accounts/api/portal/sessions-refresh.ts +3 -3
- package/src/modules/customer_accounts/lib/customerAuth.ts +8 -2
- package/src/modules/customer_accounts/lib/customerAuthServer.ts +1 -0
- package/src/modules/customer_accounts/services/customerRbacService.ts +16 -3
- package/src/modules/customer_accounts/setup.ts +7 -0
- package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +1 -3
- package/src/modules/customers/lib/visibilityFilter.ts +7 -5
- package/src/modules/dashboards/api/layout/[itemId]/route.ts +6 -3
- package/src/modules/dashboards/api/layout/route.ts +22 -9
- package/src/modules/dashboards/api/roles/widgets/route.ts +9 -3
- package/src/modules/dashboards/api/users/widgets/route.ts +9 -3
- package/src/modules/dashboards/api/widgets/catalog.ts +6 -3
- package/src/modules/dashboards/lib/access.ts +5 -3
- package/src/modules/directory/api/organization-branding/route.ts +12 -1
- package/src/modules/directory/api/organizations/route.ts +7 -0
- package/src/modules/directory/backend/directory/branding/page.tsx +34 -9
- package/src/modules/directory/commands/organizations.ts +9 -1
- package/src/modules/directory/data/entities.ts +3 -0
- package/src/modules/directory/data/validators.ts +2 -0
- package/src/modules/directory/i18n/de.json +3 -1
- package/src/modules/directory/i18n/en.json +3 -1
- package/src/modules/directory/i18n/es.json +3 -1
- package/src/modules/directory/i18n/pl.json +3 -1
- package/src/modules/directory/migrations/.snapshot-open-mercato.json +18 -1
- package/src/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.ts +13 -0
- package/src/modules/entities/lib/entityAcl.ts +47 -18
- package/src/modules/inbox_ops/ai-tools.ts +5 -3
- package/src/modules/messages/lib/routeHelpers.ts +4 -6
- package/src/modules/notifications/lib/notificationRecipients.ts +6 -8
- package/src/modules/query_index/di.ts +6 -0
- package/src/modules/query_index/lib/coverage.ts +9 -6
- package/src/modules/query_index/lib/indexer.ts +105 -19
- package/src/modules/query_index/lib/search-tokens.ts +19 -8
- package/src/modules/query_index/subscribers/coverage_refresh.ts +4 -0
- package/src/modules/query_index/subscribers/delete_one.ts +65 -1
- package/src/modules/query_index/subscribers/upsert_one.ts +68 -1
- package/src/modules/sales/commands/documents.ts +27 -0
- package/src/modules/sales/i18n/de.json +1 -0
- package/src/modules/sales/i18n/en.json +1 -0
- package/src/modules/sales/i18n/es.json +1 -0
- package/src/modules/sales/i18n/pl.json +1 -0
- package/src/modules/staff/api/interceptors.ts +6 -4
- package/src/modules/sync_excel/api/upload/route.ts +15 -2
- package/src/modules/workflows/lib/activity-executor.ts +12 -10
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
import { resolveRegisteredLucideIconNode } from '@open-mercato/ui/backend/icons/lucideRegistry'
|
|
22
22
|
import { profilePathPrefixes, profileSections } from './profile-sections'
|
|
23
23
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
24
|
-
import { filterGrantsByEnabledModules } from '@open-mercato/shared/security/enabledModulesRegistry'
|
|
25
24
|
import { getNavGroupOrderOverride } from '@open-mercato/shared/modules/overrides'
|
|
26
25
|
import {
|
|
27
26
|
getSelectedOrganizationFromRequest,
|
|
@@ -283,10 +282,7 @@ export async function resolveBackendChromePayload({
|
|
|
283
282
|
const container = await loadScopedContainer()
|
|
284
283
|
const em = container.resolve('em') as EntityManager
|
|
285
284
|
const rbac = container.resolve('rbacService') as {
|
|
286
|
-
|
|
287
|
-
isSuperAdmin: boolean
|
|
288
|
-
features: string[]
|
|
289
|
-
}>
|
|
285
|
+
getEffectiveFeatures: (userId: string, scope: { tenantId: string | null; organizationId: string | null }) => Promise<string[]>
|
|
290
286
|
userHasAllFeatures: (userId: string, required: string[], scope: { tenantId: string | null; organizationId: string | null }) => Promise<boolean>
|
|
291
287
|
}
|
|
292
288
|
|
|
@@ -319,15 +315,12 @@ export async function resolveBackendChromePayload({
|
|
|
319
315
|
concretelySelectedOrganizationId = null
|
|
320
316
|
}
|
|
321
317
|
|
|
322
|
-
const
|
|
323
|
-
? await rbac.
|
|
318
|
+
const grantedFeatures = allowNavigation
|
|
319
|
+
? await rbac.getEffectiveFeatures(auth.sub, {
|
|
324
320
|
tenantId: scopedTenantId,
|
|
325
321
|
organizationId: scopedOrganizationId,
|
|
326
322
|
})
|
|
327
|
-
:
|
|
328
|
-
|
|
329
|
-
const rawGrantedFeatures = acl.isSuperAdmin ? ['*'] : acl.features
|
|
330
|
-
const grantedFeatures = filterGrantsByEnabledModules(rawGrantedFeatures)
|
|
323
|
+
: []
|
|
331
324
|
const featureChecker = async (features: string[]): Promise<string[]> => {
|
|
332
325
|
if (!allowNavigation || !features.length) return []
|
|
333
326
|
const context = {
|
|
@@ -461,6 +454,7 @@ export async function resolveBackendChromePayload({
|
|
|
461
454
|
logo: {
|
|
462
455
|
src: organization.logoUrl,
|
|
463
456
|
alt: `${organization.name} logo`,
|
|
457
|
+
preserveAspectRatio: !!organization.logoPreserveAspectRatio,
|
|
464
458
|
},
|
|
465
459
|
}
|
|
466
460
|
}
|
|
@@ -5,8 +5,11 @@ import { UserAcl, RoleAcl, User, UserRole } from '@open-mercato/core/modules/aut
|
|
|
5
5
|
import { ApiKey } from '@open-mercato/core/modules/api_keys/data/entities'
|
|
6
6
|
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
7
7
|
import { buildOrgScopeUserCacheTag, buildOrgScopeTenantCacheTag } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
authorizeFeatures,
|
|
10
|
+
resolveEffectiveFeatures,
|
|
11
|
+
} from '@open-mercato/shared/security/featurePolicy'
|
|
12
|
+
import { filterGrantsByEnabledModules } from '@open-mercato/shared/security/enabledModulesRegistry'
|
|
10
13
|
|
|
11
14
|
interface AclData {
|
|
12
15
|
isSuperAdmin: boolean
|
|
@@ -43,32 +46,9 @@ export class RbacService {
|
|
|
43
46
|
this.cacheTtlMs = ttlMs
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
/**
|
|
47
|
-
* Checks if a required feature is satisfied by a granted feature permission.
|
|
48
|
-
*
|
|
49
|
-
* Wildcard patterns:
|
|
50
|
-
* - `*` (global wildcard): Grants access to all features
|
|
51
|
-
* - `prefix.*` (module wildcard): Grants access to all features starting with `prefix.`
|
|
52
|
-
* and also the exact prefix itself (e.g., `entities.*` matches both `entities` and `entities.records.view`)
|
|
53
|
-
* - Exact match: Feature must match exactly (e.g., `users.view` only matches `users.view`)
|
|
54
|
-
*
|
|
55
|
-
* @param required - The feature being requested (e.g., 'entities.records.view')
|
|
56
|
-
* @param granted - The feature permission granted (e.g., 'entities.*' or '*')
|
|
57
|
-
* @returns true if the granted permission satisfies the required feature
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* matchFeature('users.view', '*') // true - global wildcard
|
|
61
|
-
* matchFeature('entities.records.view', 'entities.*') // true - module wildcard
|
|
62
|
-
* matchFeature('entities', 'entities.*') // true - exact prefix match
|
|
63
|
-
* matchFeature('users.view', 'entities.*') // false - different module
|
|
64
|
-
* matchFeature('users.view', 'users.view') // true - exact match
|
|
65
|
-
*/
|
|
66
|
-
private matchFeature(required: string, granted: string): boolean {
|
|
67
|
-
return sharedMatchFeature(required, granted)
|
|
68
|
-
}
|
|
69
|
-
|
|
49
|
+
/** Compatibility wrapper for callers that already have both feature lists. */
|
|
70
50
|
public hasAllFeatures(required: string[], granted: string[]): boolean {
|
|
71
|
-
return
|
|
51
|
+
return authorizeFeatures(required, { grantedFeatures: granted })
|
|
72
52
|
}
|
|
73
53
|
|
|
74
54
|
private roleAclAllowsOrganization(acl: RoleAcl, organizationId: string | null | undefined): boolean {
|
|
@@ -380,9 +360,6 @@ export class RbacService {
|
|
|
380
360
|
): Promise<boolean> {
|
|
381
361
|
if (!tenantId || !feature) return false
|
|
382
362
|
|
|
383
|
-
const enabledIds = getEnabledModuleIds()
|
|
384
|
-
if (enabledIds.length && !enabledIds.includes(getOwningModuleId(feature))) return false
|
|
385
|
-
|
|
386
363
|
const em = this.em.fork()
|
|
387
364
|
const roleAcls = await em.find(RoleAcl, { tenantId, deletedAt: null } as any, {})
|
|
388
365
|
const list = Array.isArray(roleAcls) ? roleAcls : []
|
|
@@ -390,9 +367,13 @@ export class RbacService {
|
|
|
390
367
|
|
|
391
368
|
for (const acl of list) {
|
|
392
369
|
if (!this.roleAclAllowsOrganization(acl, organizationId)) continue
|
|
393
|
-
if (acl.isSuperAdmin) return true
|
|
394
370
|
const grants = Array.isArray(acl.featuresJson) ? acl.featuresJson : []
|
|
395
|
-
if (
|
|
371
|
+
if (authorizeFeatures([feature], {
|
|
372
|
+
grantedFeatures: grants,
|
|
373
|
+
unrestricted: acl.isSuperAdmin,
|
|
374
|
+
})) {
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
396
377
|
}
|
|
397
378
|
|
|
398
379
|
return false
|
|
@@ -436,23 +417,22 @@ export class RbacService {
|
|
|
436
417
|
async userHasAllFeatures(userId: string, required: string[], scope: { tenantId: string | null; organizationId: string | null }): Promise<boolean> {
|
|
437
418
|
if (!required.length) return true
|
|
438
419
|
const acl = await this.loadAcl(userId, scope)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
420
|
+
const organizationAllowed = acl.isSuperAdmin
|
|
421
|
+
|| !acl.organizations
|
|
422
|
+
|| !scope.organizationId
|
|
423
|
+
|| acl.organizations.includes(scope.organizationId)
|
|
424
|
+
|| acl.organizations.includes('__all__')
|
|
425
|
+
return authorizeFeatures(required, {
|
|
426
|
+
grantedFeatures: acl.features,
|
|
427
|
+
unrestricted: acl.isSuperAdmin,
|
|
428
|
+
scopeAllowed: organizationAllowed,
|
|
429
|
+
})
|
|
447
430
|
}
|
|
448
431
|
|
|
449
432
|
/**
|
|
450
|
-
* Returns the
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
* with `userHasAllFeatures` which also enforces disabled-module boundaries
|
|
454
|
-
* for super admins. Returns `[]` when the requested organization is outside
|
|
455
|
-
* the user's visibility list, or when the user does not exist.
|
|
433
|
+
* Returns the user's infrastructure grant list, filtered to enabled modules.
|
|
434
|
+
* The result may contain wildcards; authoritative checks must still go through
|
|
435
|
+
* `authorizeFeatures` or `userHasAllFeatures` so null overrides are enforced.
|
|
456
436
|
*/
|
|
457
437
|
async getGrantedFeatures(
|
|
458
438
|
userId: string,
|
|
@@ -470,4 +450,19 @@ export class RbacService {
|
|
|
470
450
|
}
|
|
471
451
|
return filterGrantsByEnabledModules(acl.features)
|
|
472
452
|
}
|
|
453
|
+
|
|
454
|
+
/** Returns concrete active feature IDs for browser capability payloads. */
|
|
455
|
+
async getEffectiveFeatures(
|
|
456
|
+
userId: string,
|
|
457
|
+
scope: { tenantId: string | null; organizationId: string | null },
|
|
458
|
+
): Promise<string[]> {
|
|
459
|
+
const acl = await this.loadAcl(userId, scope)
|
|
460
|
+
const organizationAllowed = acl.isSuperAdmin
|
|
461
|
+
|| !acl.organizations
|
|
462
|
+
|| !scope.organizationId
|
|
463
|
+
|| acl.organizations.includes(scope.organizationId)
|
|
464
|
+
|| acl.organizations.includes('__all__')
|
|
465
|
+
if (!organizationAllowed) return []
|
|
466
|
+
return resolveEffectiveFeatures(acl.isSuperAdmin ? ['*'] : acl.features)
|
|
467
|
+
}
|
|
473
468
|
}
|
|
@@ -8,7 +8,7 @@ const widget: InjectionWidgetModule<any, any> = {
|
|
|
8
8
|
id: 'catalog.injection.product-seo',
|
|
9
9
|
title: 'Product SEO Helper',
|
|
10
10
|
description: 'Helps optimize product metadata for search engines',
|
|
11
|
-
features: ['catalog.products.
|
|
11
|
+
features: ['catalog.products.manage'],
|
|
12
12
|
priority: 90,
|
|
13
13
|
enabled: true,
|
|
14
14
|
requiredFields: ['description'],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Per-user channel access control — service-layer authorization helpers.
|
|
@@ -67,7 +67,7 @@ export function assertCanAccessChannel(
|
|
|
67
67
|
* - **Tenant-wide / shared channel** (`userId == null`, e.g. WhatsApp
|
|
68
68
|
* Business / Slack workspaces): requires the route's `elevatedFeature`
|
|
69
69
|
* (`communication_channels.manage`, `…channel.push.manage`, or
|
|
70
|
-
* `…channel.import_history`).
|
|
70
|
+
* `…channel.import_history`). Evaluated by the shared feature policy.
|
|
71
71
|
*
|
|
72
72
|
* Throws `ChannelAccessDeniedError` (route handlers map it to 404, masking
|
|
73
73
|
* existence) when the caller may not manage the channel.
|
|
@@ -94,7 +94,7 @@ export function assertCanManageChannel(
|
|
|
94
94
|
}
|
|
95
95
|
// Tenant-wide / shared channel — requires the elevated management feature.
|
|
96
96
|
const grantedFeatures = Array.isArray(userFeatures) ? userFeatures : []
|
|
97
|
-
if (!
|
|
97
|
+
if (!authorizeFeatures([elevatedFeature], { grantedFeatures })) {
|
|
98
98
|
throw new ChannelAccessDeniedError(`Managing a shared channel requires '${elevatedFeature}'`)
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -136,7 +136,7 @@ Effective permissions = User ACL (if exists) OR aggregated Role ACLs.
|
|
|
136
136
|
|
|
137
137
|
### Portal Admin Flag
|
|
138
138
|
|
|
139
|
-
`isPortalAdmin: true` on a role/user ACL bypasses
|
|
139
|
+
`isPortalAdmin: true` on a role/user ACL bypasses stored-grant matching (equivalent to staff `isSuperAdmin`). Nulled ACL features and disabled modules remain denied.
|
|
140
140
|
|
|
141
141
|
### Default Roles (seeded on tenant creation)
|
|
142
142
|
|
|
@@ -150,7 +150,7 @@ Effective permissions = User ACL (if exists) OR aggregated Role ACLs.
|
|
|
150
150
|
|
|
151
151
|
Customer portal features use the `portal.<area>.<action>` naming convention (e.g., `portal.orders.view`, `portal.catalog.view`).
|
|
152
152
|
|
|
153
|
-
Treat `portal.*` and `*` as first-class ACL grants.
|
|
153
|
+
Treat `portal.*` and `*` as first-class stored ACL grants. Server authorization calls `CustomerRbacService.userHasAllFeatures`; already-loaded snapshots call shared `authorizeFeatures`. Portal UI and navigation consume `getEffectiveFeatures`, which returns concrete active IDs without wildcards.
|
|
154
154
|
|
|
155
155
|
### Cross-Module Feature Merging
|
|
156
156
|
|
|
@@ -172,7 +172,7 @@ const customerRbacService = container.resolve('customerRbacService')
|
|
|
172
172
|
const hasAccess = await customerRbacService.userHasAllFeatures(userId, ['portal.orders.view'], { tenantId, organizationId })
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
When a portal UI/client helper needs batch checks, prefer `/api/customer_accounts/portal/feature-check`.
|
|
175
|
+
When a portal UI/client helper needs batch checks, prefer `/api/customer_accounts/portal/feature-check`. Client helpers receive concrete effective features and may use the pure shared matcher without implementing portal-admin or removal bypasses.
|
|
176
176
|
|
|
177
177
|
## Services and DI
|
|
178
178
|
|
|
@@ -39,11 +39,10 @@ export async function POST(req: Request) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const { user, invitation } = result
|
|
42
|
-
const
|
|
42
|
+
const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {
|
|
43
43
|
tenantId: user.tenantId,
|
|
44
44
|
organizationId: user.organizationId,
|
|
45
45
|
})
|
|
46
|
-
const resolvedFeatures = acl.features
|
|
47
46
|
|
|
48
47
|
const ip = getClientIp(req, 0)
|
|
49
48
|
const userAgent = req.headers.get('user-agent') || null
|
|
@@ -116,8 +116,10 @@ export async function POST(req: Request) {
|
|
|
116
116
|
await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig)
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const
|
|
120
|
-
|
|
119
|
+
const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {
|
|
120
|
+
tenantId,
|
|
121
|
+
organizationId: user.organizationId,
|
|
122
|
+
})
|
|
121
123
|
|
|
122
124
|
const ip = getClientIp(req, 0)
|
|
123
125
|
const userAgent = req.headers.get('user-agent') || null
|
|
@@ -52,8 +52,10 @@ export async function POST(req: Request) {
|
|
|
52
52
|
await customerUserService.resetFailedAttempts(user)
|
|
53
53
|
await customerUserService.updateLastLoginAt(user)
|
|
54
54
|
|
|
55
|
-
const
|
|
56
|
-
|
|
55
|
+
const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {
|
|
56
|
+
tenantId: user.tenantId,
|
|
57
|
+
organizationId: user.organizationId,
|
|
58
|
+
})
|
|
57
59
|
|
|
58
60
|
const ip = getClientIp(req, 0)
|
|
59
61
|
const userAgent = req.headers.get('user-agent') || null
|
|
@@ -4,7 +4,7 @@ import type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib
|
|
|
4
4
|
import { getCustomerAuthFromRequest } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'
|
|
5
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
6
|
import { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'
|
|
7
|
-
import {
|
|
7
|
+
import { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'
|
|
8
8
|
|
|
9
9
|
export const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }
|
|
10
10
|
|
|
@@ -32,12 +32,10 @@ export async function POST(req: Request) {
|
|
|
32
32
|
tenantId: auth.tenantId,
|
|
33
33
|
organizationId: auth.orgId,
|
|
34
34
|
})
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
acl.features.some((g) => matchFeature(feature, g)),
|
|
40
|
-
)
|
|
35
|
+
const granted = body.features.filter((feature) => authorizeFeatures([feature], {
|
|
36
|
+
grantedFeatures: acl.features,
|
|
37
|
+
unrestricted: acl.isPortalAdmin,
|
|
38
|
+
}))
|
|
41
39
|
|
|
42
40
|
return NextResponse.json({ ok: true, granted })
|
|
43
41
|
}
|
|
@@ -51,13 +51,15 @@ export async function GET(req: Request) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId, organizationId: auth.orgId })
|
|
54
|
-
const grantedFeatures =
|
|
54
|
+
const grantedFeatures = await rbac.getEffectiveFeatures(auth.sub, {
|
|
55
|
+
tenantId: auth.tenantId,
|
|
56
|
+
organizationId: auth.orgId,
|
|
57
|
+
})
|
|
55
58
|
|
|
56
59
|
const groups = buildPortalNav({
|
|
57
60
|
routes: getFrontendRouteManifests(),
|
|
58
61
|
orgSlug,
|
|
59
62
|
grantedFeatures,
|
|
60
|
-
isPortalAdmin: acl.isPortalAdmin,
|
|
61
63
|
})
|
|
62
64
|
|
|
63
65
|
return NextResponse.json({
|
|
@@ -72,7 +74,7 @@ export async function GET(req: Request) {
|
|
|
72
74
|
const getMethodDoc: OpenApiMethodDoc = {
|
|
73
75
|
summary: 'Portal sidebar navigation',
|
|
74
76
|
description:
|
|
75
|
-
'Returns the portal sidebar for the authenticated customer. Items are derived from each portal page\'s `nav` metadata and filtered by `requireCustomerFeatures` against
|
|
77
|
+
'Returns the portal sidebar for the authenticated customer. Items are derived from each portal page\'s `nav` metadata and filtered by `requireCustomerFeatures` against concrete effective features.',
|
|
76
78
|
tags: ['Customer Portal'],
|
|
77
79
|
responses: [{ status: 200, description: 'Portal sidebar groups', schema: navResponseSchema }],
|
|
78
80
|
errors: [
|
|
@@ -27,6 +27,10 @@ export async function GET(req: Request) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const acl = await customerRbacService.loadAcl(user.id, { tenantId: user.tenantId, organizationId: user.organizationId })
|
|
30
|
+
const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {
|
|
31
|
+
tenantId: user.tenantId,
|
|
32
|
+
organizationId: user.organizationId,
|
|
33
|
+
})
|
|
30
34
|
|
|
31
35
|
const userRoles = await em.find(CustomerUserRole, {
|
|
32
36
|
user: user.id as any,
|
|
@@ -52,7 +56,7 @@ export async function GET(req: Request) {
|
|
|
52
56
|
createdAt: user.createdAt,
|
|
53
57
|
},
|
|
54
58
|
roles,
|
|
55
|
-
resolvedFeatures
|
|
59
|
+
resolvedFeatures,
|
|
56
60
|
isPortalAdmin: acl.isPortalAdmin,
|
|
57
61
|
})
|
|
58
62
|
}
|
|
@@ -36,17 +36,17 @@ export async function POST(req: Request) {
|
|
|
36
36
|
return NextResponse.json({ ok: false, error: 'Account not active' }, { status: 401 })
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
const
|
|
39
|
+
const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {
|
|
40
40
|
tenantId: user.tenantId,
|
|
41
41
|
organizationId: user.organizationId,
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
const result = await customerSessionService.refreshSession(decodedToken,
|
|
44
|
+
const result = await customerSessionService.refreshSession(decodedToken, resolvedFeatures)
|
|
45
45
|
if (!result) {
|
|
46
46
|
return NextResponse.json({ ok: false, error: 'Session refresh failed' }, { status: 401 })
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const res = NextResponse.json({ ok: true, resolvedFeatures
|
|
49
|
+
const res = NextResponse.json({ ok: true, resolvedFeatures })
|
|
50
50
|
|
|
51
51
|
res.cookies.set('customer_auth_token', result.jwt, {
|
|
52
52
|
httpOnly: true,
|
|
@@ -15,6 +15,7 @@ export interface CustomerAuthContext {
|
|
|
15
15
|
customerEntityId?: string | null
|
|
16
16
|
personEntityId?: string | null
|
|
17
17
|
resolvedFeatures: string[]
|
|
18
|
+
isPortalAdmin?: boolean
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
async function assertSessionStillActive(sessionId: string): Promise<boolean> {
|
|
@@ -48,7 +49,7 @@ export function readCookieFromHeader(header: string | null | undefined, name: st
|
|
|
48
49
|
|
|
49
50
|
export type UserValidationResult =
|
|
50
51
|
| { valid: false }
|
|
51
|
-
| { valid: true; resolvedFeatures: string[] }
|
|
52
|
+
| { valid: true; resolvedFeatures: string[]; isPortalAdmin: boolean }
|
|
52
53
|
|
|
53
54
|
export async function validateUserState(
|
|
54
55
|
sub: string,
|
|
@@ -77,7 +78,11 @@ export async function validateUserState(
|
|
|
77
78
|
)
|
|
78
79
|
const rbac = container.resolve('customerRbacService') as InstanceType<typeof CustomerRbacService>
|
|
79
80
|
const acl = await rbac.loadAcl(sub, { tenantId, organizationId: orgId })
|
|
80
|
-
|
|
81
|
+
const resolvedFeatures = await rbac.getEffectiveFeatures(sub, {
|
|
82
|
+
tenantId,
|
|
83
|
+
organizationId: orgId,
|
|
84
|
+
})
|
|
85
|
+
return { valid: true, resolvedFeatures, isPortalAdmin: acl.isPortalAdmin }
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
export async function getCustomerAuthFromRequest(req: Request): Promise<CustomerAuthContext | null> {
|
|
@@ -134,6 +139,7 @@ export async function getCustomerAuthFromRequest(req: Request): Promise<Customer
|
|
|
134
139
|
customerEntityId: payload.customerEntityId ? String(payload.customerEntityId) : null,
|
|
135
140
|
personEntityId: payload.personEntityId ? String(payload.personEntityId) : null,
|
|
136
141
|
resolvedFeatures: userState.resolvedFeatures,
|
|
142
|
+
isPortalAdmin: userState.isPortalAdmin,
|
|
137
143
|
}
|
|
138
144
|
} catch {
|
|
139
145
|
// Invalid or expired JWT — treat as unauthenticated
|
|
@@ -88,6 +88,7 @@ export async function getCustomerAuthFromCookies(
|
|
|
88
88
|
customerEntityId: payload.customerEntityId ? String(payload.customerEntityId) : null,
|
|
89
89
|
personEntityId: payload.personEntityId ? String(payload.personEntityId) : null,
|
|
90
90
|
resolvedFeatures: userState.resolvedFeatures,
|
|
91
|
+
isPortalAdmin: userState.isPortalAdmin,
|
|
91
92
|
}
|
|
92
93
|
} catch {
|
|
93
94
|
// Invalid or expired JWT — treat as unauthenticated
|
|
@@ -5,7 +5,10 @@ import {
|
|
|
5
5
|
CustomerRoleAcl,
|
|
6
6
|
CustomerUserRole,
|
|
7
7
|
} from '@open-mercato/core/modules/customer_accounts/data/entities'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
authorizeFeatures,
|
|
10
|
+
resolveEffectiveFeatures,
|
|
11
|
+
} from '@open-mercato/shared/security/featurePolicy'
|
|
9
12
|
|
|
10
13
|
interface CustomerAclData {
|
|
11
14
|
isPortalAdmin: boolean
|
|
@@ -129,8 +132,18 @@ export class CustomerRbacService {
|
|
|
129
132
|
): Promise<boolean> {
|
|
130
133
|
if (!required.length) return true
|
|
131
134
|
const acl = await this.loadAcl(userId, scope)
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
return authorizeFeatures(required, {
|
|
136
|
+
grantedFeatures: acl.features,
|
|
137
|
+
unrestricted: acl.isPortalAdmin,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async getEffectiveFeatures(
|
|
142
|
+
userId: string,
|
|
143
|
+
scope: { tenantId: string; organizationId: string },
|
|
144
|
+
): Promise<string[]> {
|
|
145
|
+
const acl = await this.loadAcl(userId, scope)
|
|
146
|
+
return resolveEffectiveFeatures(acl.isPortalAdmin ? ['portal.*'] : acl.features)
|
|
134
147
|
}
|
|
135
148
|
|
|
136
149
|
async invalidateUserCache(userId: string): Promise<void> {
|
|
@@ -137,6 +137,10 @@ const DEFAULT_ROLES = [
|
|
|
137
137
|
},
|
|
138
138
|
]
|
|
139
139
|
|
|
140
|
+
const DEFAULT_CUSTOMER_ROLE_FEATURES = Object.fromEntries(
|
|
141
|
+
DEFAULT_ROLES.map((role) => [role.slug, [...role.acl.features]]),
|
|
142
|
+
)
|
|
143
|
+
|
|
140
144
|
/**
|
|
141
145
|
* Collect defaultCustomerRoleFeatures from all enabled modules and merge
|
|
142
146
|
* them into the corresponding CustomerRoleAcl records.
|
|
@@ -220,6 +224,9 @@ export const setup: ModuleSetupConfig = {
|
|
|
220
224
|
superadmin: ['customer_accounts.*'],
|
|
221
225
|
admin: ['customer_accounts.*'],
|
|
222
226
|
},
|
|
227
|
+
// Keep the portal feature catalog declarative so the shared feature policy
|
|
228
|
+
// can project portal.* into concrete capabilities without importing core.
|
|
229
|
+
defaultCustomerRoleFeatures: DEFAULT_CUSTOMER_ROLE_FEATURES,
|
|
223
230
|
|
|
224
231
|
async onTenantCreated({ em, tenantId, organizationId }) {
|
|
225
232
|
await seedDefaultRoles(em, { tenantId, organizationId })
|
|
@@ -69,9 +69,7 @@ export default function PortalAiAssistantTriggerWidget({ context }: PortalAiAssi
|
|
|
69
69
|
const resolvedFeatures = Array.isArray(context?.resolvedFeatures)
|
|
70
70
|
? (context?.resolvedFeatures as string[])
|
|
71
71
|
: []
|
|
72
|
-
const featureAllowed =
|
|
73
|
-
context?.isPortalAdmin === true ||
|
|
74
|
-
hasFeature(resolvedFeatures, PORTAL_AI_INJECT_REQUIRED_FEATURE)
|
|
72
|
+
const featureAllowed = hasFeature(resolvedFeatures, PORTAL_AI_INJECT_REQUIRED_FEATURE)
|
|
75
73
|
|
|
76
74
|
const pageContext = React.useMemo<PortalAiInjectPageContext>(() => ({
|
|
77
75
|
view: 'portal.profile',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FilterQuery } from '@mikro-orm/postgresql'
|
|
2
|
-
import {
|
|
2
|
+
import { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'
|
|
3
3
|
import { CustomerInteraction } from '../data/entities'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -22,7 +22,9 @@ export const EMAIL_VIEW_PRIVATE_FEATURE = 'customers.email.view_private'
|
|
|
22
22
|
*/
|
|
23
23
|
export function callerHasEmailViewPrivate(userFeatures: string[] | null | undefined): boolean {
|
|
24
24
|
if (!Array.isArray(userFeatures) || userFeatures.length === 0) return false
|
|
25
|
-
return
|
|
25
|
+
return authorizeFeatures([EMAIL_VIEW_PRIVATE_FEATURE], {
|
|
26
|
+
grantedFeatures: userFeatures,
|
|
27
|
+
})
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -92,7 +94,7 @@ export function applyEmailVisibilityFilter<T extends { where: (...args: any[]) =
|
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
type RbacServiceLike = {
|
|
95
|
-
|
|
97
|
+
getEffectiveFeatures?: (
|
|
96
98
|
userId: string,
|
|
97
99
|
scope: { tenantId: string | null; organizationId: string | null },
|
|
98
100
|
) => Promise<string[] | undefined>
|
|
@@ -118,8 +120,8 @@ export async function resolveCallerEmailFeatures(
|
|
|
118
120
|
if (!userId) return undefined
|
|
119
121
|
try {
|
|
120
122
|
const rbac = container.resolve('rbacService') as RbacServiceLike | undefined
|
|
121
|
-
if (!rbac?.
|
|
122
|
-
return await rbac.
|
|
123
|
+
if (!rbac?.getEffectiveFeatures) return undefined
|
|
124
|
+
return await rbac.getEffectiveFeatures(userId, { tenantId, organizationId })
|
|
123
125
|
} catch {
|
|
124
126
|
return undefined
|
|
125
127
|
}
|
|
@@ -4,7 +4,6 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
|
4
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
5
|
import { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/entities'
|
|
6
6
|
import { dashboardLayoutItemPatchSchema } from '@open-mercato/core/modules/dashboards/data/validators'
|
|
7
|
-
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
8
7
|
import {
|
|
9
8
|
runCrudMutationGuardAfterSuccess,
|
|
10
9
|
validateCrudMutationGuard,
|
|
@@ -55,8 +54,12 @@ export async function PATCH(req: Request, ctx: { params?: { itemId?: string } })
|
|
|
55
54
|
organizationId: auth.orgId ?? null,
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
const
|
|
59
|
-
|
|
57
|
+
const canConfigure = await rbac.userHasAllFeatures(
|
|
58
|
+
scope.userId,
|
|
59
|
+
['dashboards.configure'],
|
|
60
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
61
|
+
)
|
|
62
|
+
if (!canConfigure) {
|
|
60
63
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -6,7 +6,7 @@ import { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/enti
|
|
|
6
6
|
import { dashboardLayoutSchema } from '@open-mercato/core/modules/dashboards/data/validators'
|
|
7
7
|
import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'
|
|
8
8
|
import { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'
|
|
9
|
-
import {
|
|
9
|
+
import { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'
|
|
10
10
|
import { User } from '@open-mercato/core/modules/auth/data/entities'
|
|
11
11
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
12
12
|
import {
|
|
@@ -88,7 +88,10 @@ export async function GET(req: Request) {
|
|
|
88
88
|
organizationId: auth.orgId ?? null,
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
const
|
|
91
|
+
const effectiveFeatures = await rbac.getEffectiveFeatures(scope.userId, {
|
|
92
|
+
tenantId: scope.tenantId,
|
|
93
|
+
organizationId: scope.organizationId,
|
|
94
|
+
})
|
|
92
95
|
const widgets = await loadAllWidgets()
|
|
93
96
|
const allowedIds = await resolveAllowedWidgetIds(
|
|
94
97
|
em,
|
|
@@ -96,8 +99,8 @@ export async function GET(req: Request) {
|
|
|
96
99
|
userId: scope.userId,
|
|
97
100
|
tenantId: scope.tenantId,
|
|
98
101
|
organizationId: scope.organizationId,
|
|
99
|
-
features:
|
|
100
|
-
isSuperAdmin:
|
|
102
|
+
features: effectiveFeatures,
|
|
103
|
+
isSuperAdmin: false,
|
|
101
104
|
},
|
|
102
105
|
widgets,
|
|
103
106
|
)
|
|
@@ -147,7 +150,9 @@ export async function GET(req: Request) {
|
|
|
147
150
|
await em.flush()
|
|
148
151
|
}
|
|
149
152
|
|
|
150
|
-
const canConfigure =
|
|
153
|
+
const canConfigure = authorizeFeatures(['dashboards.configure'], {
|
|
154
|
+
grantedFeatures: effectiveFeatures,
|
|
155
|
+
})
|
|
151
156
|
|
|
152
157
|
let userEmail: string | null = null
|
|
153
158
|
let userName: string | null = null
|
|
@@ -222,8 +227,12 @@ export async function PUT(req: Request) {
|
|
|
222
227
|
organizationId: auth.orgId ?? null,
|
|
223
228
|
}
|
|
224
229
|
|
|
225
|
-
const
|
|
226
|
-
|
|
230
|
+
const canConfigure = await rbac.userHasAllFeatures(
|
|
231
|
+
scope.userId,
|
|
232
|
+
['dashboards.configure'],
|
|
233
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
234
|
+
)
|
|
235
|
+
if (!canConfigure) {
|
|
227
236
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
228
237
|
}
|
|
229
238
|
|
|
@@ -243,14 +252,18 @@ export async function PUT(req: Request) {
|
|
|
243
252
|
}
|
|
244
253
|
|
|
245
254
|
const widgets = await loadAllWidgets()
|
|
255
|
+
const effectiveFeatures = await rbac.getEffectiveFeatures(scope.userId, {
|
|
256
|
+
tenantId: scope.tenantId,
|
|
257
|
+
organizationId: scope.organizationId,
|
|
258
|
+
})
|
|
246
259
|
const allowedIds = await resolveAllowedWidgetIds(
|
|
247
260
|
em,
|
|
248
261
|
{
|
|
249
262
|
userId: scope.userId,
|
|
250
263
|
tenantId: scope.tenantId,
|
|
251
264
|
organizationId: scope.organizationId,
|
|
252
|
-
features:
|
|
253
|
-
isSuperAdmin:
|
|
265
|
+
features: effectiveFeatures,
|
|
266
|
+
isSuperAdmin: false,
|
|
254
267
|
},
|
|
255
268
|
widgets,
|
|
256
269
|
)
|