@open-mercato/core 0.6.7-develop.6593.1.0ecf630f1e → 0.6.7-develop.6595.1.e93ecd3cfa
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/generated/entities/custom_entity/index.js +2 -0
- package/dist/generated/entities/custom_entity/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/api/features.js +14 -0
- package/dist/modules/auth/api/features.js.map +2 -2
- package/dist/modules/entities/api/entities.js +20 -0
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/entity-settings.js +89 -0
- package/dist/modules/entities/api/entity-settings.js.map +7 -0
- package/dist/modules/entities/api/records.js +46 -25
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js +13 -4
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/create/page.js +27 -2
- package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
- package/dist/modules/entities/data/entities.js +4 -0
- package/dist/modules/entities/data/entities.js.map +2 -2
- package/dist/modules/entities/lib/entityAcl.js +17 -4
- package/dist/modules/entities/lib/entityAcl.js.map +3 -3
- package/dist/modules/entities/lib/install-from-ce.js +2 -0
- package/dist/modules/entities/lib/install-from-ce.js.map +2 -2
- package/dist/modules/entities/lib/recordFeatures.js +24 -0
- package/dist/modules/entities/lib/recordFeatures.js.map +7 -0
- package/dist/modules/entities/lib/register.js +3 -0
- package/dist/modules/entities/lib/register.js.map +2 -2
- package/dist/modules/entities/lib/restrictedEntityFeatures.js +62 -0
- package/dist/modules/entities/lib/restrictedEntityFeatures.js.map +7 -0
- package/dist/modules/entities/migrations/Migration20260716120000.js +13 -0
- package/dist/modules/entities/migrations/Migration20260716120000.js.map +7 -0
- package/generated/entities/custom_entity/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/auth/api/features.ts +17 -0
- package/src/modules/customers/i18n/pl.json +484 -484
- package/src/modules/entities/api/entities.ts +28 -0
- package/src/modules/entities/api/entity-settings.ts +103 -0
- package/src/modules/entities/api/records.ts +70 -29
- package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +12 -3
- package/src/modules/entities/backend/entities/user/create/page.tsx +33 -1
- package/src/modules/entities/data/entities.ts +6 -0
- package/src/modules/entities/i18n/de.json +3 -0
- package/src/modules/entities/i18n/en.json +3 -0
- package/src/modules/entities/i18n/es.json +3 -0
- package/src/modules/entities/i18n/pl.json +3 -0
- package/src/modules/entities/lib/entityAcl.ts +27 -5
- package/src/modules/entities/lib/install-from-ce.ts +2 -0
- package/src/modules/entities/lib/recordFeatures.ts +48 -0
- package/src/modules/entities/lib/register.ts +4 -0
- package/src/modules/entities/lib/restrictedEntityFeatures.ts +98 -0
- package/src/modules/entities/migrations/.snapshot-open-mercato.json +10 -0
- package/src/modules/entities/migrations/Migration20260716120000.ts +13 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Pure, dependency-free helpers describing the ACL features that gate
|
|
2
|
+
// custom-entity records. Kept import-free so both the enforcement path
|
|
3
|
+
// (`entityAcl.ts`) and the feature catalog (`auth/api/features.ts`) can derive
|
|
4
|
+
// the SAME feature id without coupling to each other or to the ORM.
|
|
5
|
+
//
|
|
6
|
+
// Coarse (route-level) features stay entity-agnostic:
|
|
7
|
+
// entities.records.view — read any non-restricted custom entity's records
|
|
8
|
+
// entities.records.manage — write any non-restricted custom entity's records
|
|
9
|
+
//
|
|
10
|
+
// A custom entity flagged `access_restricted` ALSO requires a synthesized
|
|
11
|
+
// per-entity feature keyed on its immutable entityId:
|
|
12
|
+
// entities.records.<entityId>.view
|
|
13
|
+
// entities.records.<entityId>.manage
|
|
14
|
+
//
|
|
15
|
+
// entityId always matches `^[a-z0-9_]+:[a-z0-9_]+$` (it contains a `:` and no
|
|
16
|
+
// `.`), so it can never collide with the coarse `view`/`manage` segments and
|
|
17
|
+
// stays a single dotted segment for the wildcard matcher.
|
|
18
|
+
|
|
19
|
+
export type RecordsAction = 'view' | 'manage'
|
|
20
|
+
|
|
21
|
+
export const RECORDS_VIEW_FEATURE = 'entities.records.view'
|
|
22
|
+
export const RECORDS_MANAGE_FEATURE = 'entities.records.manage'
|
|
23
|
+
|
|
24
|
+
export function coarseRecordFeature(action: RecordsAction): string {
|
|
25
|
+
return action === 'manage' ? RECORDS_MANAGE_FEATURE : RECORDS_VIEW_FEATURE
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function deriveCustomEntityRecordFeature(entityId: string, action: RecordsAction): string {
|
|
29
|
+
return `entities.records.${entityId}.${action}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SynthesizedRecordFeature = {
|
|
33
|
+
id: string
|
|
34
|
+
action: RecordsAction
|
|
35
|
+
dependsOn: string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The per-entity features an admin can grant for a restricted custom entity.
|
|
39
|
+
// `dependsOn` mirrors the coarse prerequisite composition: the per-entity grant
|
|
40
|
+
// is meaningful only alongside the coarse route-level feature.
|
|
41
|
+
export function synthesizedRecordFeatures(entityId: string): SynthesizedRecordFeature[] {
|
|
42
|
+
const view = deriveCustomEntityRecordFeature(entityId, 'view')
|
|
43
|
+
const manage = deriveCustomEntityRecordFeature(entityId, 'manage')
|
|
44
|
+
return [
|
|
45
|
+
{ id: view, action: 'view', dependsOn: [RECORDS_VIEW_FEATURE] },
|
|
46
|
+
{ id: manage, action: 'manage', dependsOn: [view, RECORDS_MANAGE_FEATURE] },
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -7,6 +7,7 @@ export type UpsertEntityOptions = {
|
|
|
7
7
|
organizationId?: string | null
|
|
8
8
|
tenantId?: string | null
|
|
9
9
|
showInSidebar?: boolean
|
|
10
|
+
accessRestricted?: boolean
|
|
10
11
|
labelField?: string | null
|
|
11
12
|
defaultEditor?: string | null
|
|
12
13
|
isActive?: boolean
|
|
@@ -29,6 +30,7 @@ export async function upsertCustomEntity(em: EntityManager, entityId: string, op
|
|
|
29
30
|
description: opts.description ?? null,
|
|
30
31
|
isActive: opts.isActive ?? true,
|
|
31
32
|
showInSidebar: !!opts.showInSidebar,
|
|
33
|
+
accessRestricted: !!opts.accessRestricted,
|
|
32
34
|
labelField: opts.labelField ?? null,
|
|
33
35
|
defaultEditor: opts.defaultEditor ?? null,
|
|
34
36
|
}
|
|
@@ -40,6 +42,7 @@ export async function upsertCustomEntity(em: EntityManager, entityId: string, op
|
|
|
40
42
|
ent.description = desired.description
|
|
41
43
|
ent.isActive = desired.isActive
|
|
42
44
|
ent.showInSidebar = desired.showInSidebar
|
|
45
|
+
ent.accessRestricted = desired.accessRestricted
|
|
43
46
|
ent.labelField = desired.labelField
|
|
44
47
|
ent.defaultEditor = desired.defaultEditor
|
|
45
48
|
ent.updatedAt = new Date()
|
|
@@ -51,6 +54,7 @@ export async function upsertCustomEntity(em: EntityManager, entityId: string, op
|
|
|
51
54
|
if ((ent.description ?? null) !== desired.description) return true
|
|
52
55
|
if ((ent.isActive ?? true) !== desired.isActive) return true
|
|
53
56
|
if ((ent.showInSidebar ?? false) !== desired.showInSidebar) return true
|
|
57
|
+
if ((ent.accessRestricted ?? false) !== desired.accessRestricted) return true
|
|
54
58
|
if ((ent.labelField ?? null) !== desired.labelField) return true
|
|
55
59
|
if ((ent.defaultEditor ?? null) !== desired.defaultEditor) return true
|
|
56
60
|
if (ent.deletedAt != null) return true
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { getModules } from '@open-mercato/shared/lib/i18n/server'
|
|
2
|
+
import { synthesizedRecordFeatures } from './recordFeatures'
|
|
3
|
+
|
|
4
|
+
export type SynthesizedFeatureItem = {
|
|
5
|
+
id: string
|
|
6
|
+
title: string
|
|
7
|
+
module: string
|
|
8
|
+
dependsOn?: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type RestrictedEntity = { entityId: string; label: string }
|
|
12
|
+
|
|
13
|
+
type DeclaredEntities = { restricted: RestrictedEntity[]; ids: Set<string> }
|
|
14
|
+
|
|
15
|
+
// Reads module-declared (ce.ts) custom entities. `restricted` holds those flagged
|
|
16
|
+
// accessRestricted; `ids` holds ALL declared ids so the DB path can defer to the
|
|
17
|
+
// declaration — the declared registry is authoritative for its own ids, matching
|
|
18
|
+
// the records-route enforcement precedence (declared wins over the DB row).
|
|
19
|
+
function declaredEntities(): DeclaredEntities {
|
|
20
|
+
const restricted: RestrictedEntity[] = []
|
|
21
|
+
const ids = new Set<string>()
|
|
22
|
+
try {
|
|
23
|
+
const mods = getModules() as Array<{
|
|
24
|
+
customEntities?: Array<{ id?: string; label?: string; accessRestricted?: boolean }>
|
|
25
|
+
}>
|
|
26
|
+
for (const mod of mods || []) {
|
|
27
|
+
for (const spec of mod?.customEntities ?? []) {
|
|
28
|
+
if (!spec?.id) continue
|
|
29
|
+
ids.add(spec.id)
|
|
30
|
+
if (spec.accessRestricted === true) restricted.push({ entityId: spec.id, label: spec.label || spec.id })
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch {}
|
|
34
|
+
return { restricted, ids }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Reads user-registered custom entities flagged access_restricted for the given
|
|
38
|
+
// tenant scope. Uses the raw table (not the ORM class) to keep this helper cheap
|
|
39
|
+
// and avoid pulling entity metadata into unrelated callers.
|
|
40
|
+
async function registeredRestrictedEntities(em: any, tenantId: string): Promise<RestrictedEntity[]> {
|
|
41
|
+
try {
|
|
42
|
+
const db = em.getKysely()
|
|
43
|
+
const rows = await db
|
|
44
|
+
.selectFrom('custom_entities' as any)
|
|
45
|
+
.select(['entity_id' as any, 'label' as any])
|
|
46
|
+
.where('access_restricted' as any, '=', true)
|
|
47
|
+
.where('deleted_at' as any, 'is', null as any)
|
|
48
|
+
.where((eb: any) =>
|
|
49
|
+
eb.or([
|
|
50
|
+
eb('tenant_id' as any, '=', tenantId),
|
|
51
|
+
eb('tenant_id' as any, 'is', null as any),
|
|
52
|
+
]),
|
|
53
|
+
)
|
|
54
|
+
.execute()
|
|
55
|
+
return (rows as Array<{ entity_id?: unknown; label?: unknown }>).map((row) => ({
|
|
56
|
+
entityId: String(row.entity_id ?? ''),
|
|
57
|
+
label: typeof row.label === 'string' && row.label.length ? row.label : String(row.entity_id ?? ''),
|
|
58
|
+
})).filter((row) => row.entityId.length > 0)
|
|
59
|
+
} catch {
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Feature-catalog contribution for restricted custom entities: two synthesized
|
|
66
|
+
* per-entity features (view/manage) per restricted entity in the tenant scope,
|
|
67
|
+
* so admins can grant them in the Role/User ACL editor. Fails safe (returns what
|
|
68
|
+
* it can) — never throws — so the static feature catalog is unaffected on error.
|
|
69
|
+
*/
|
|
70
|
+
export async function synthesizeRestrictedEntityFeatures(
|
|
71
|
+
em: any,
|
|
72
|
+
tenantId: string | null | undefined,
|
|
73
|
+
): Promise<SynthesizedFeatureItem[]> {
|
|
74
|
+
if (!tenantId) return []
|
|
75
|
+
const declared = declaredEntities()
|
|
76
|
+
const byEntityId = new Map<string, RestrictedEntity>()
|
|
77
|
+
for (const entity of declared.restricted) byEntityId.set(entity.entityId, entity)
|
|
78
|
+
// DB rows are authoritative only for ids the declared registry does not own, so
|
|
79
|
+
// the catalog and the records-route enforcement agree on the same source.
|
|
80
|
+
for (const entity of await registeredRestrictedEntities(em, tenantId)) {
|
|
81
|
+
if (declared.ids.has(entity.entityId)) continue
|
|
82
|
+
byEntityId.set(entity.entityId, entity)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const items: SynthesizedFeatureItem[] = []
|
|
86
|
+
for (const { entityId, label } of byEntityId.values()) {
|
|
87
|
+
for (const feature of synthesizedRecordFeatures(entityId)) {
|
|
88
|
+
const verb = feature.action === 'manage' ? 'Manage records' : 'View records'
|
|
89
|
+
items.push({
|
|
90
|
+
id: feature.id,
|
|
91
|
+
title: `${verb}: ${label}`,
|
|
92
|
+
module: 'entities',
|
|
93
|
+
dependsOn: feature.dependsOn,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return items
|
|
98
|
+
}
|
|
@@ -71,6 +71,16 @@
|
|
|
71
71
|
"default": "false",
|
|
72
72
|
"mappedType": "boolean"
|
|
73
73
|
},
|
|
74
|
+
"access_restricted": {
|
|
75
|
+
"name": "access_restricted",
|
|
76
|
+
"type": "boolean",
|
|
77
|
+
"unsigned": false,
|
|
78
|
+
"autoincrement": false,
|
|
79
|
+
"primary": false,
|
|
80
|
+
"nullable": false,
|
|
81
|
+
"default": "false",
|
|
82
|
+
"mappedType": "boolean"
|
|
83
|
+
},
|
|
74
84
|
"organization_id": {
|
|
75
85
|
"name": "organization_id",
|
|
76
86
|
"type": "uuid",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260716120000 extends Migration {
|
|
4
|
+
|
|
5
|
+
override async up(): Promise<void> {
|
|
6
|
+
this.addSql(`alter table "custom_entities" add column "access_restricted" boolean not null default false;`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
override async down(): Promise<void> {
|
|
10
|
+
this.addSql(`alter table "custom_entities" drop column "access_restricted";`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
}
|