@open-mercato/core 0.6.3-develop.3562.1.71049275ff → 0.6.3-develop.3574.1.f3c5dcf345
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/helpers/integration/queue-runner.js +15 -3
- package/dist/helpers/integration/queue-runner.js.map +2 -2
- package/dist/helpers/integration/queue.js +1 -1
- package/dist/helpers/integration/queue.js.map +2 -2
- package/dist/modules/auth/api/roles/acl/route.js +37 -1
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/roles/route.js +36 -2
- package/dist/modules/auth/api/roles/route.js.map +2 -2
- package/dist/modules/auth/api/users/acl/route.js +36 -1
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +40 -2
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/commands/users.js +3 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/lib/grantChecks.js +97 -2
- package/dist/modules/auth/lib/grantChecks.js.map +2 -2
- package/dist/modules/business_rules/lib/expression-evaluator.js +9 -7
- package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +9 -4
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js +23 -11
- package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
- package/jest.config.cjs +1 -0
- package/package.json +7 -7
- package/src/helpers/integration/queue-runner.ts +21 -2
- package/src/helpers/integration/queue.ts +1 -1
- package/src/modules/auth/api/roles/acl/route.ts +39 -1
- package/src/modules/auth/api/roles/route.ts +38 -2
- package/src/modules/auth/api/users/acl/route.ts +41 -1
- package/src/modules/auth/api/users/route.ts +41 -2
- package/src/modules/auth/commands/users.ts +3 -0
- package/src/modules/auth/lib/grantChecks.ts +116 -2
- package/src/modules/business_rules/lib/expression-evaluator.ts +9 -13
- package/src/modules/translations/components/TranslationManager.tsx +9 -4
- package/src/modules/workflows/lib/event-trigger-service.ts +24 -11
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
2
2
|
import { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'
|
|
3
3
|
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
4
|
-
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
|
-
import { Role, RoleAcl } from '@open-mercato/core/modules/auth/data/entities'
|
|
4
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
|
+
import { Role, RoleAcl, UserAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'
|
|
6
6
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
7
7
|
|
|
8
8
|
type ActorAcl = {
|
|
@@ -33,6 +33,16 @@ type FeatureGrantCheckInput = GrantCheckContext & {
|
|
|
33
33
|
organizations?: string[] | null
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
type SuperAdminUserTargetInput = GrantCheckContext & {
|
|
37
|
+
targetUserId: string
|
|
38
|
+
actorIsSuperAdmin?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type SuperAdminRoleTargetInput = GrantCheckContext & {
|
|
42
|
+
targetRoleId: string
|
|
43
|
+
actorIsSuperAdmin?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
37
47
|
|
|
38
48
|
export async function assertActorCanGrantRoleTokens(input: RoleTokenGrantCheckInput): Promise<Role[]> {
|
|
@@ -99,6 +109,110 @@ export function normalizeGrantFeatureList(features: unknown): string[] {
|
|
|
99
109
|
return normalizeStringList(features)
|
|
100
110
|
}
|
|
101
111
|
|
|
112
|
+
export async function assertActorCanModifySuperAdminUserTarget(input: SuperAdminUserTargetInput): Promise<void> {
|
|
113
|
+
const actorIsSuperAdmin = await resolveActorIsSuperAdmin(input)
|
|
114
|
+
if (actorIsSuperAdmin) return
|
|
115
|
+
const targetIsSuperAdmin = await isUserEffectivelySuperAdmin(input.em, input.targetUserId)
|
|
116
|
+
if (targetIsSuperAdmin) {
|
|
117
|
+
throw forbidden('Only super administrators can modify super administrator accounts.')
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function assertActorCanModifySuperAdminRoleTarget(input: SuperAdminRoleTargetInput): Promise<void> {
|
|
122
|
+
const actorIsSuperAdmin = await resolveActorIsSuperAdmin(input)
|
|
123
|
+
if (actorIsSuperAdmin) return
|
|
124
|
+
const targetIsSuperAdmin = await isRoleEffectivelySuperAdmin(input.em, input.targetRoleId)
|
|
125
|
+
if (targetIsSuperAdmin) {
|
|
126
|
+
throw forbidden('Only super administrators can modify super administrator roles.')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function resolveActorIsSuperAdmin(input: GrantCheckContext & { actorIsSuperAdmin?: boolean }): Promise<boolean> {
|
|
131
|
+
if (typeof input.actorIsSuperAdmin === 'boolean') return input.actorIsSuperAdmin
|
|
132
|
+
const acl = await loadActorAcl(input)
|
|
133
|
+
return acl.isSuperAdmin
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function isUserEffectivelySuperAdmin(em: EntityManager, userId: string): Promise<boolean> {
|
|
137
|
+
const directGrant = await em.findOne(
|
|
138
|
+
UserAcl,
|
|
139
|
+
{ user: userId as unknown, isSuperAdmin: true } as FilterQuery<UserAcl>,
|
|
140
|
+
)
|
|
141
|
+
if (directGrant && (directGrant as { isSuperAdmin?: boolean }).isSuperAdmin === true) return true
|
|
142
|
+
const links = await findWithDecryption(
|
|
143
|
+
em,
|
|
144
|
+
UserRole,
|
|
145
|
+
{ user: userId as unknown } as FilterQuery<UserRole>,
|
|
146
|
+
{ populate: ['role'] },
|
|
147
|
+
{ tenantId: null, organizationId: null },
|
|
148
|
+
)
|
|
149
|
+
const roleIds = (Array.isArray(links) ? links : [])
|
|
150
|
+
.map((link) => {
|
|
151
|
+
const role = (link as { role?: { id?: unknown } | string | null }).role
|
|
152
|
+
if (!role) return null
|
|
153
|
+
if (typeof role === 'string') return role
|
|
154
|
+
return role.id ? String(role.id) : null
|
|
155
|
+
})
|
|
156
|
+
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
157
|
+
if (!roleIds.length) return false
|
|
158
|
+
const roleGrant = await em.findOne(
|
|
159
|
+
RoleAcl,
|
|
160
|
+
{ role: { $in: roleIds } as unknown, isSuperAdmin: true } as FilterQuery<RoleAcl>,
|
|
161
|
+
)
|
|
162
|
+
return !!roleGrant && (roleGrant as { isSuperAdmin?: boolean }).isSuperAdmin === true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function isRoleEffectivelySuperAdmin(em: EntityManager, roleId: string): Promise<boolean> {
|
|
166
|
+
const grant = await em.findOne(
|
|
167
|
+
RoleAcl,
|
|
168
|
+
{ role: roleId as unknown, isSuperAdmin: true } as FilterQuery<RoleAcl>,
|
|
169
|
+
)
|
|
170
|
+
return !!grant && (grant as { isSuperAdmin?: boolean }).isSuperAdmin === true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function listSuperAdminUserIds(em: EntityManager, tenantId: string | null): Promise<Set<string>> {
|
|
174
|
+
const ids = new Set<string>()
|
|
175
|
+
const userAclFilter: Record<string, unknown> = { isSuperAdmin: true }
|
|
176
|
+
if (tenantId) userAclFilter.tenantId = tenantId
|
|
177
|
+
const userAcls = await em.find(UserAcl, userAclFilter as FilterQuery<UserAcl>)
|
|
178
|
+
for (const acl of userAcls) {
|
|
179
|
+
const userRef = (acl as { user?: { id?: unknown } | string | null }).user
|
|
180
|
+
const userId = userRef && typeof userRef === 'object'
|
|
181
|
+
? userRef.id
|
|
182
|
+
: userRef
|
|
183
|
+
if (userId) ids.add(String(userId))
|
|
184
|
+
}
|
|
185
|
+
const roleAcls = await em.find(
|
|
186
|
+
RoleAcl,
|
|
187
|
+
{ isSuperAdmin: true } as FilterQuery<RoleAcl>,
|
|
188
|
+
)
|
|
189
|
+
const roleIds = roleAcls
|
|
190
|
+
.map((acl) => {
|
|
191
|
+
const roleRef = (acl as { role?: { id?: unknown } | string | null }).role
|
|
192
|
+
if (!roleRef) return null
|
|
193
|
+
if (typeof roleRef === 'string') return roleRef
|
|
194
|
+
return roleRef.id ? String(roleRef.id) : null
|
|
195
|
+
})
|
|
196
|
+
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
197
|
+
if (roleIds.length) {
|
|
198
|
+
const links = await findWithDecryption(
|
|
199
|
+
em,
|
|
200
|
+
UserRole,
|
|
201
|
+
{ role: { $in: roleIds } as unknown } as FilterQuery<UserRole>,
|
|
202
|
+
{},
|
|
203
|
+
{ tenantId: null, organizationId: null },
|
|
204
|
+
)
|
|
205
|
+
for (const link of Array.isArray(links) ? links : []) {
|
|
206
|
+
const userRef = (link as { user?: { id?: unknown } | string | null }).user
|
|
207
|
+
const userId = userRef && typeof userRef === 'object'
|
|
208
|
+
? userRef.id
|
|
209
|
+
: userRef
|
|
210
|
+
if (userId) ids.add(String(userId))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return ids
|
|
214
|
+
}
|
|
215
|
+
|
|
102
216
|
async function loadActorAcl(input: GrantCheckContext): Promise<ActorAcl> {
|
|
103
217
|
const actorUserId = normalizeNullableString(input.actorUserId)
|
|
104
218
|
if (!actorUserId) throw forbidden('Not authorized to grant ACL privileges.')
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComparisonOperator, LogicalOperator } from '../data/validators'
|
|
2
|
+
import { testLinearRegex } from '@open-mercato/shared/lib/regex/linear'
|
|
2
3
|
import { getNestedValue, resolveSpecialValue } from './value-resolver'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -319,11 +320,8 @@ function endsWith(str: any, suffix: any): boolean {
|
|
|
319
320
|
return String(str).endsWith(String(suffix))
|
|
320
321
|
}
|
|
321
322
|
|
|
322
|
-
/**
|
|
323
|
-
* Security limits for regex operations
|
|
324
|
-
*/
|
|
325
|
-
const REGEX_TIMEOUT_MS = 100
|
|
326
323
|
const MAX_REGEX_LENGTH = 200
|
|
324
|
+
const MAX_REGEX_INPUT_LENGTH = 10_000
|
|
327
325
|
|
|
328
326
|
/**
|
|
329
327
|
* Check if string matches regex pattern (with ReDoS protection)
|
|
@@ -345,18 +343,16 @@ function matches(str: any, pattern: any): boolean {
|
|
|
345
343
|
return false
|
|
346
344
|
}
|
|
347
345
|
|
|
348
|
-
const regex = new RegExp(patternStr)
|
|
349
346
|
const testStr = String(str)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const startTime = Date.now()
|
|
353
|
-
const result = regex.test(testStr)
|
|
354
|
-
|
|
355
|
-
if (Date.now() - startTime > REGEX_TIMEOUT_MS) {
|
|
356
|
-
throw new Error('Regex execution timeout - potential ReDoS pattern detected')
|
|
347
|
+
if (testStr.length > MAX_REGEX_INPUT_LENGTH) {
|
|
348
|
+
return false
|
|
357
349
|
}
|
|
358
350
|
|
|
359
|
-
|
|
351
|
+
const result = testLinearRegex(patternStr, testStr, {
|
|
352
|
+
maxPatternLength: MAX_REGEX_LENGTH,
|
|
353
|
+
maxInputLength: MAX_REGEX_INPUT_LENGTH,
|
|
354
|
+
})
|
|
355
|
+
return result.ok ? result.matched : false
|
|
360
356
|
} catch (error) {
|
|
361
357
|
// Log the error for debugging but don't expose to user
|
|
362
358
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -69,6 +69,7 @@ export function TranslationManager({
|
|
|
69
69
|
const [editedTranslations, setEditedTranslations] = React.useState<Record<string, Record<string, string>>>({})
|
|
70
70
|
const editedTranslationsRef = React.useRef<Record<string, Record<string, string>>>({})
|
|
71
71
|
const [hasUserEdited, setHasUserEdited] = React.useState(false)
|
|
72
|
+
const hasUserEditedRef = React.useRef(false)
|
|
72
73
|
|
|
73
74
|
const entityType = isEmbedded ? (propEntityType ?? '') : selectedEntityType
|
|
74
75
|
const recordId = isEmbedded ? (propRecordId ?? '') : selectedRecordId
|
|
@@ -191,11 +192,11 @@ export function TranslationManager({
|
|
|
191
192
|
|
|
192
193
|
React.useEffect(() => {
|
|
193
194
|
const sig = translationSignature
|
|
194
|
-
if (sig === lastTranslationSignatureRef.current &&
|
|
195
|
+
if (sig === lastTranslationSignatureRef.current && hasUserEditedRef.current) return
|
|
195
196
|
lastTranslationSignatureRef.current = sig
|
|
196
197
|
|
|
197
198
|
if (!translationData?.translations) {
|
|
198
|
-
if (!
|
|
199
|
+
if (!hasUserEditedRef.current) {
|
|
199
200
|
editedTranslationsRef.current = {}
|
|
200
201
|
setEditedTranslations({})
|
|
201
202
|
}
|
|
@@ -210,11 +211,11 @@ export function TranslationManager({
|
|
|
210
211
|
parsed[locale][key] = typeof val === 'string' ? val : ''
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
|
-
if (!
|
|
214
|
+
if (!hasUserEditedRef.current) {
|
|
214
215
|
editedTranslationsRef.current = parsed
|
|
215
216
|
setEditedTranslations(parsed)
|
|
216
217
|
}
|
|
217
|
-
}, [translationSignature, translationData
|
|
218
|
+
}, [translationSignature, translationData])
|
|
218
219
|
|
|
219
220
|
const mutation = useMutation({
|
|
220
221
|
mutationFn: async () => {
|
|
@@ -252,6 +253,7 @@ export function TranslationManager({
|
|
|
252
253
|
},
|
|
253
254
|
onSuccess: () => {
|
|
254
255
|
flash(t('translations.manager.flash.saved', 'Translations saved'), 'success')
|
|
256
|
+
hasUserEditedRef.current = false
|
|
255
257
|
setHasUserEdited(false)
|
|
256
258
|
void refetchTranslation()
|
|
257
259
|
},
|
|
@@ -262,6 +264,7 @@ export function TranslationManager({
|
|
|
262
264
|
})
|
|
263
265
|
|
|
264
266
|
const updateFieldValue = (locale: string, fieldKey: string, value: string) => {
|
|
267
|
+
hasUserEditedRef.current = true
|
|
265
268
|
setHasUserEdited(true)
|
|
266
269
|
const next = {
|
|
267
270
|
...editedTranslationsRef.current,
|
|
@@ -288,6 +291,7 @@ export function TranslationManager({
|
|
|
288
291
|
value={selectedRecordId}
|
|
289
292
|
onChange={(next) => {
|
|
290
293
|
setSelectedRecordId(next)
|
|
294
|
+
hasUserEditedRef.current = false
|
|
291
295
|
setHasUserEdited(false)
|
|
292
296
|
}}
|
|
293
297
|
placeholder={t('translations.manager.searchRecords', 'Search records...')}
|
|
@@ -482,6 +486,7 @@ export function TranslationManager({
|
|
|
482
486
|
onChange={(next) => {
|
|
483
487
|
setSelectedEntityType(next)
|
|
484
488
|
setSelectedRecordId('')
|
|
489
|
+
hasUserEditedRef.current = false
|
|
485
490
|
setHasUserEdited(false)
|
|
486
491
|
}}
|
|
487
492
|
placeholder={t('translations.manager.placeholder', 'Select an entity')}
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { EntityManager } from '@mikro-orm/core'
|
|
9
|
+
import type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'
|
|
9
10
|
import type { AwilixContainer } from 'awilix'
|
|
10
11
|
import type { CacheService } from '@open-mercato/cache'
|
|
11
12
|
import { matchEventPattern } from '@open-mercato/shared/lib/events/patterns'
|
|
13
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
14
|
+
import { testLinearRegex } from '@open-mercato/shared/lib/regex/linear'
|
|
12
15
|
import {
|
|
13
16
|
WorkflowEventTrigger,
|
|
14
17
|
WorkflowDefinition,
|
|
@@ -257,11 +260,12 @@ function evaluateCondition(condition: TriggerFilterCondition, payload: Record<st
|
|
|
257
260
|
if (typeof value !== 'string' || typeof expected !== 'string') return false
|
|
258
261
|
if (value.length > MAX_WORKFLOW_REGEX_INPUT_LENGTH) return false
|
|
259
262
|
if (!isSafeWorkflowRegexPattern(expected)) return false
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
{
|
|
264
|
+
const result = testLinearRegex(expected, value, {
|
|
265
|
+
maxPatternLength: MAX_WORKFLOW_REGEX_PATTERN_LENGTH,
|
|
266
|
+
maxInputLength: MAX_WORKFLOW_REGEX_INPUT_LENGTH,
|
|
267
|
+
})
|
|
268
|
+
return result.ok ? result.matched : false
|
|
265
269
|
}
|
|
266
270
|
|
|
267
271
|
default:
|
|
@@ -325,7 +329,9 @@ async function loadLegacyTriggers(
|
|
|
325
329
|
tenantId: string,
|
|
326
330
|
organizationId: string
|
|
327
331
|
): Promise<UnifiedTrigger[]> {
|
|
328
|
-
const
|
|
332
|
+
const postgresEm = em as unknown as PostgreSqlEntityManager
|
|
333
|
+
const legacyTriggers = await findWithDecryption(
|
|
334
|
+
postgresEm,
|
|
329
335
|
WorkflowEventTrigger,
|
|
330
336
|
{
|
|
331
337
|
tenantId,
|
|
@@ -335,16 +341,19 @@ async function loadLegacyTriggers(
|
|
|
335
341
|
},
|
|
336
342
|
{
|
|
337
343
|
orderBy: { priority: 'DESC', createdAt: 'ASC' },
|
|
338
|
-
}
|
|
344
|
+
},
|
|
345
|
+
{ tenantId, organizationId },
|
|
339
346
|
)
|
|
340
347
|
|
|
341
348
|
// Get definitions for these triggers to get workflowId
|
|
342
349
|
const definitionIds = [...new Set(legacyTriggers.map(t => t.workflowDefinitionId))]
|
|
343
|
-
const definitions = definitionIds.length > 0 ? await
|
|
350
|
+
const definitions = definitionIds.length > 0 ? await findWithDecryption(postgresEm, WorkflowDefinition, {
|
|
344
351
|
id: { $in: definitionIds },
|
|
352
|
+
tenantId,
|
|
353
|
+
organizationId,
|
|
345
354
|
enabled: true,
|
|
346
355
|
deletedAt: null,
|
|
347
|
-
}) : []
|
|
356
|
+
}, {}, { tenantId, organizationId }) : []
|
|
348
357
|
const definitionMap = new Map(definitions.map(d => [d.id, d]))
|
|
349
358
|
|
|
350
359
|
return legacyTriggers
|
|
@@ -379,15 +388,19 @@ async function loadEmbeddedTriggers(
|
|
|
379
388
|
tenantId: string,
|
|
380
389
|
organizationId: string
|
|
381
390
|
): Promise<UnifiedTrigger[]> {
|
|
391
|
+
const postgresEm = em as unknown as PostgreSqlEntityManager
|
|
382
392
|
// Load all enabled definitions that may have triggers
|
|
383
|
-
const definitions = await
|
|
393
|
+
const definitions = await findWithDecryption(
|
|
394
|
+
postgresEm,
|
|
384
395
|
WorkflowDefinition,
|
|
385
396
|
{
|
|
386
397
|
tenantId,
|
|
387
398
|
organizationId,
|
|
388
399
|
enabled: true,
|
|
389
400
|
deletedAt: null,
|
|
390
|
-
}
|
|
401
|
+
},
|
|
402
|
+
{},
|
|
403
|
+
{ tenantId, organizationId },
|
|
391
404
|
)
|
|
392
405
|
|
|
393
406
|
const triggers: UnifiedTrigger[] = []
|