@open-mercato/core 0.4.2-canary-49d47ff90e → 0.4.2-canary-0ba39cdeb6
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/modules/auth/backend/auth/profile/page.js.map +1 -1
- package/dist/modules/auth/backend/roles/[id]/edit/page.js +4 -1
- package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +4 -1
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/cli.js +13 -12
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/business_rules/api/execute/route.js +7 -1
- package/dist/modules/business_rules/api/execute/route.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-engine.js +33 -3
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/configs/components/CachePanel.js +4 -4
- package/dist/modules/configs/components/CachePanel.js.map +2 -2
- package/dist/modules/configs/lib/system-status.js +48 -1
- package/dist/modules/configs/lib/system-status.js.map +2 -2
- package/dist/modules/dashboards/cli.js +12 -4
- package/dist/modules/dashboards/cli.js.map +2 -2
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +16 -11
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +3 -3
- package/dist/modules/dashboards/services/widgetDataService.js +110 -3
- package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
- package/dist/modules/notifications/data/validators.js +5 -1
- package/dist/modules/notifications/data/validators.js.map +2 -2
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +2 -1
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
- package/dist/modules/notifications/lib/deliveryConfig.js +4 -2
- package/dist/modules/notifications/lib/deliveryConfig.js.map +2 -2
- package/dist/modules/notifications/lib/deliveryStrategies.js +14 -0
- package/dist/modules/notifications/lib/deliveryStrategies.js.map +7 -0
- package/dist/modules/notifications/subscribers/deliver-notification.js +33 -7
- package/dist/modules/notifications/subscribers/deliver-notification.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +14 -6
- package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
- package/package.json +2 -2
- package/src/modules/auth/README.md +1 -1
- package/src/modules/auth/__tests__/cli-setup-acl.test.ts +1 -1
- package/src/modules/auth/backend/auth/profile/page.tsx +2 -2
- package/src/modules/auth/backend/roles/[id]/edit/page.tsx +4 -1
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +4 -1
- package/src/modules/auth/cli.ts +25 -12
- package/src/modules/business_rules/api/execute/route.ts +8 -1
- package/src/modules/business_rules/lib/__tests__/rule-engine.test.ts +51 -0
- package/src/modules/business_rules/lib/rule-engine.ts +57 -3
- package/src/modules/configs/components/CachePanel.tsx +4 -4
- package/src/modules/configs/i18n/en.json +12 -2
- package/src/modules/configs/i18n/pl.json +12 -2
- package/src/modules/configs/lib/system-status.ts +48 -1
- package/src/modules/configs/lib/system-status.types.ts +1 -0
- package/src/modules/dashboards/cli.ts +14 -4
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +22 -11
- package/src/modules/dashboards/services/widgetDataService.ts +132 -4
- package/src/modules/notifications/__tests__/deliver-notification.test.ts +195 -0
- package/src/modules/notifications/__tests__/deliveryStrategies.test.ts +19 -0
- package/src/modules/notifications/__tests__/notificationService.test.ts +208 -0
- package/src/modules/notifications/data/validators.ts +5 -0
- package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +2 -0
- package/src/modules/notifications/lib/deliveryConfig.ts +8 -0
- package/src/modules/notifications/lib/deliveryStrategies.ts +50 -0
- package/src/modules/notifications/subscribers/deliver-notification.ts +39 -10
- package/src/modules/workflows/lib/transition-handler.ts +18 -6
|
@@ -6,7 +6,7 @@ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
|
6
6
|
import { deleteCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
7
7
|
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
8
8
|
import { AclEditor, type AclData } from '@open-mercato/core/modules/auth/components/AclEditor'
|
|
9
|
-
import { WidgetVisibilityEditor } from '@open-mercato/core/modules/dashboards/components/WidgetVisibilityEditor'
|
|
9
|
+
import { WidgetVisibilityEditor, type WidgetVisibilityEditorHandle } from '@open-mercato/core/modules/dashboards/components/WidgetVisibilityEditor'
|
|
10
10
|
import { E } from '#generated/entities.ids.generated'
|
|
11
11
|
import { TenantSelect } from '@open-mercato/core/modules/directory/components/TenantSelect'
|
|
12
12
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -37,6 +37,7 @@ export default function EditRolePage({ params }: { params?: { id?: string } }) {
|
|
|
37
37
|
const [aclData, setAclData] = React.useState<AclData>({ isSuperAdmin: false, features: [], organizations: null })
|
|
38
38
|
const [actorIsSuperAdmin, setActorIsSuperAdmin] = React.useState(false)
|
|
39
39
|
const [selectedTenantId, setSelectedTenantId] = React.useState<string | null>(null)
|
|
40
|
+
const widgetEditorRef = React.useRef<WidgetVisibilityEditorHandle | null>(null)
|
|
40
41
|
|
|
41
42
|
React.useEffect(() => {
|
|
42
43
|
if (!id) return
|
|
@@ -153,6 +154,7 @@ export default function EditRolePage({ params }: { params?: { id?: string } }) {
|
|
|
153
154
|
kind="role"
|
|
154
155
|
targetId={String(id)}
|
|
155
156
|
tenantId={selectedTenantId ?? (initial?.tenantId ?? null)}
|
|
157
|
+
ref={widgetEditorRef}
|
|
156
158
|
/>
|
|
157
159
|
)
|
|
158
160
|
: null),
|
|
@@ -191,6 +193,7 @@ export default function EditRolePage({ params }: { params?: { id?: string } }) {
|
|
|
191
193
|
await updateCrud('auth/roles/acl', { roleId: id, tenantId: effectiveTenantId, ...aclData }, {
|
|
192
194
|
errorMessage: t('auth.roles.form.errors.aclUpdate', 'Failed to update role access control'),
|
|
193
195
|
})
|
|
196
|
+
await widgetEditorRef.current?.save()
|
|
194
197
|
try { window.dispatchEvent(new Event('om:refresh-sidebar')) } catch {}
|
|
195
198
|
}}
|
|
196
199
|
onDelete={async () => {
|
|
@@ -10,7 +10,7 @@ import { AclEditor, type AclData } from '@open-mercato/core/modules/auth/compone
|
|
|
10
10
|
import { OrganizationSelect } from '@open-mercato/core/modules/directory/components/OrganizationSelect'
|
|
11
11
|
import { TenantSelect } from '@open-mercato/core/modules/directory/components/TenantSelect'
|
|
12
12
|
import { fetchRoleOptions } from '@open-mercato/core/modules/auth/backend/users/roleOptions'
|
|
13
|
-
import { WidgetVisibilityEditor } from '@open-mercato/core/modules/dashboards/components/WidgetVisibilityEditor'
|
|
13
|
+
import { WidgetVisibilityEditor, type WidgetVisibilityEditorHandle } from '@open-mercato/core/modules/dashboards/components/WidgetVisibilityEditor'
|
|
14
14
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
15
15
|
import { formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'
|
|
16
16
|
|
|
@@ -109,6 +109,7 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
|
|
|
109
109
|
const [aclData, setAclData] = React.useState<AclData>({ isSuperAdmin: false, features: [], organizations: null })
|
|
110
110
|
const [customFieldValues, setCustomFieldValues] = React.useState<Record<string, unknown>>({})
|
|
111
111
|
const [actorIsSuperAdmin, setActorIsSuperAdmin] = React.useState(false)
|
|
112
|
+
const widgetEditorRef = React.useRef<WidgetVisibilityEditorHandle | null>(null)
|
|
112
113
|
const passwordPolicy = React.useMemo(() => getPasswordPolicy(), [])
|
|
113
114
|
const passwordRequirements = React.useMemo(
|
|
114
115
|
() => formatPasswordRequirements(passwordPolicy, t),
|
|
@@ -308,6 +309,7 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
|
|
|
308
309
|
targetId={String(id)}
|
|
309
310
|
tenantId={selectedTenantId ?? null}
|
|
310
311
|
organizationId={initialUser?.organizationId ?? null}
|
|
312
|
+
ref={widgetEditorRef}
|
|
311
313
|
/>
|
|
312
314
|
) : null
|
|
313
315
|
),
|
|
@@ -370,6 +372,7 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
|
|
|
370
372
|
await updateCrud('auth/users/acl', { userId: id, ...aclData }, {
|
|
371
373
|
errorMessage: t('auth.users.form.errors.aclUpdate', 'Failed to update user access control'),
|
|
372
374
|
})
|
|
375
|
+
await widgetEditorRef.current?.save()
|
|
373
376
|
try { window.dispatchEvent(new Event('om:refresh-sidebar')) } catch {}
|
|
374
377
|
}}
|
|
375
378
|
onDelete={async () => {
|
package/src/modules/auth/cli.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { env } from 'process'
|
|
|
17
17
|
import type { KmsService, TenantDek } from '@open-mercato/shared/lib/encryption/kms'
|
|
18
18
|
import crypto from 'node:crypto'
|
|
19
19
|
import { formatPasswordRequirements, getPasswordPolicy, validatePassword } from '@open-mercato/shared/lib/auth/passwordPolicy'
|
|
20
|
+
import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
|
|
20
21
|
|
|
21
22
|
const addUser: ModuleCli = {
|
|
22
23
|
command: 'add-user',
|
|
@@ -404,21 +405,33 @@ const addOrganization: ModuleCli = {
|
|
|
404
405
|
const setupApp: ModuleCli = {
|
|
405
406
|
command: 'setup',
|
|
406
407
|
async run(rest) {
|
|
407
|
-
const args
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
|
|
408
|
+
const args = parseArgs(rest)
|
|
409
|
+
const orgName = typeof args.orgName === 'string'
|
|
410
|
+
? args.orgName
|
|
411
|
+
: typeof args.name === 'string'
|
|
412
|
+
? args.name
|
|
413
|
+
: undefined
|
|
414
|
+
const email = typeof args.email === 'string' ? args.email : undefined
|
|
415
|
+
const password = typeof args.password === 'string' ? args.password : undefined
|
|
416
|
+
const rolesCsv = typeof args.roles === 'string'
|
|
417
|
+
? args.roles.trim()
|
|
418
|
+
: 'superadmin,admin,employee'
|
|
419
|
+
const skipPasswordPolicyRaw =
|
|
420
|
+
args['skip-password-policy'] ??
|
|
421
|
+
args.skipPasswordPolicy ??
|
|
422
|
+
args['allow-weak-password'] ??
|
|
423
|
+
args.allowWeakPassword
|
|
424
|
+
const skipPasswordPolicy = typeof skipPasswordPolicyRaw === 'boolean'
|
|
425
|
+
? skipPasswordPolicyRaw
|
|
426
|
+
: parseBooleanToken(typeof skipPasswordPolicyRaw === 'string' ? skipPasswordPolicyRaw : null) ?? false
|
|
417
427
|
if (!orgName || !email || !password) {
|
|
418
|
-
console.error('Usage: mercato auth setup --orgName <name> --email <email> --password <password> [--roles superadmin,admin,employee]')
|
|
428
|
+
console.error('Usage: mercato auth setup --orgName <name> --email <email> --password <password> [--roles superadmin,admin,employee] [--skip-password-policy]')
|
|
419
429
|
return
|
|
420
430
|
}
|
|
421
|
-
if (!ensurePasswordPolicy(password)) return
|
|
431
|
+
if (!skipPasswordPolicy && !ensurePasswordPolicy(password)) return
|
|
432
|
+
if (skipPasswordPolicy) {
|
|
433
|
+
console.warn('⚠️ Password policy validation skipped for setup.')
|
|
434
|
+
}
|
|
422
435
|
const { resolve } = await createRequestContainer()
|
|
423
436
|
const em = resolve<EntityManager>('em')
|
|
424
437
|
const roleNames = rolesCsv
|
|
@@ -4,6 +4,7 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
|
4
4
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
5
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
6
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
7
|
+
import type { EventBus } from '@open-mercato/events'
|
|
7
8
|
import { ruleEngineContextSchema } from '../../data/validators'
|
|
8
9
|
import * as ruleEngine from '../../lib/rule-engine'
|
|
9
10
|
|
|
@@ -46,6 +47,12 @@ export async function POST(req: Request) {
|
|
|
46
47
|
|
|
47
48
|
const container = await createRequestContainer()
|
|
48
49
|
const em = container.resolve('em') as EntityManager
|
|
50
|
+
let eventBus: EventBus | null = null
|
|
51
|
+
try {
|
|
52
|
+
eventBus = container.resolve('eventBus') as EventBus
|
|
53
|
+
} catch {
|
|
54
|
+
eventBus = null
|
|
55
|
+
}
|
|
49
56
|
|
|
50
57
|
let body: any
|
|
51
58
|
try {
|
|
@@ -85,7 +92,7 @@ export async function POST(req: Request) {
|
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
try {
|
|
88
|
-
const result = await ruleEngine.executeRules(em, context)
|
|
95
|
+
const result = await ruleEngine.executeRules(em, context, { eventBus })
|
|
89
96
|
|
|
90
97
|
const response = {
|
|
91
98
|
allowed: result.allowed,
|
|
@@ -654,6 +654,57 @@ describe('Rule Engine (Unit Tests)', () => {
|
|
|
654
654
|
expect(result.errors).toBeDefined()
|
|
655
655
|
expect(result.errors![0]).toContain('Rule count limit exceeded')
|
|
656
656
|
})
|
|
657
|
+
|
|
658
|
+
test('should emit execution_failed event when rule evaluation fails', async () => {
|
|
659
|
+
const mockRule: Partial<BusinessRule> = {
|
|
660
|
+
id: 'rule-1',
|
|
661
|
+
ruleId: 'TEST-001',
|
|
662
|
+
ruleName: 'Test Rule',
|
|
663
|
+
ruleType: 'ACTION',
|
|
664
|
+
entityType: 'WorkOrder',
|
|
665
|
+
conditionExpression: { field: 'status', operator: '=', value: 'RELEASED' },
|
|
666
|
+
enabled: true,
|
|
667
|
+
tenantId: testTenantId,
|
|
668
|
+
organizationId: testOrgId,
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
mockEm.find.mockResolvedValue([mockRule as BusinessRule])
|
|
672
|
+
mockEm.create.mockReturnValue({ id: 'log-1' } as any)
|
|
673
|
+
mockEm.persistAndFlush.mockResolvedValue(undefined)
|
|
674
|
+
|
|
675
|
+
jest.mocked(ruleEvaluator.evaluateSingleRule).mockResolvedValue({
|
|
676
|
+
rule: mockRule as BusinessRule,
|
|
677
|
+
conditionsPassed: false,
|
|
678
|
+
evaluationCompleted: false,
|
|
679
|
+
evaluationTime: 1,
|
|
680
|
+
error: 'Evaluation error',
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
const eventBus = { emitEvent: jest.fn().mockResolvedValue(undefined) }
|
|
684
|
+
|
|
685
|
+
const context: RuleEngineContext = {
|
|
686
|
+
entityType: 'WorkOrder',
|
|
687
|
+
entityId: testEntityId,
|
|
688
|
+
data: { status: 'RELEASED' },
|
|
689
|
+
tenantId: testTenantId,
|
|
690
|
+
organizationId: testOrgId,
|
|
691
|
+
dryRun: false,
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
await ruleEngine.executeRules(mockEm, context, { eventBus })
|
|
695
|
+
|
|
696
|
+
expect(eventBus.emitEvent).toHaveBeenCalledWith(
|
|
697
|
+
'business_rules.rule.execution_failed',
|
|
698
|
+
expect.objectContaining({
|
|
699
|
+
ruleId: 'TEST-001',
|
|
700
|
+
ruleName: 'Test Rule',
|
|
701
|
+
entityType: 'WorkOrder',
|
|
702
|
+
errorMessage: 'Evaluation error',
|
|
703
|
+
tenantId: testTenantId,
|
|
704
|
+
organizationId: testOrgId,
|
|
705
|
+
})
|
|
706
|
+
)
|
|
707
|
+
})
|
|
657
708
|
})
|
|
658
709
|
|
|
659
710
|
describe('logRuleExecution', () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/core'
|
|
2
|
+
import type { EventBus } from '@open-mercato/events'
|
|
2
3
|
import { BusinessRule, RuleExecutionLog, type RuleType } from '../data/entities'
|
|
3
4
|
import * as ruleEvaluator from './rule-evaluator'
|
|
4
5
|
import * as actionExecutor from './action-executor'
|
|
@@ -85,6 +86,19 @@ export interface RuleDiscoveryOptions {
|
|
|
85
86
|
ruleType?: RuleType
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
export type RuleEngineExecutionOptions = {
|
|
90
|
+
eventBus?: Pick<EventBus, 'emitEvent'> | null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type RuleExecutionFailedPayload = {
|
|
94
|
+
ruleId: string
|
|
95
|
+
ruleName: string
|
|
96
|
+
entityType?: string | null
|
|
97
|
+
errorMessage?: string | null
|
|
98
|
+
tenantId: string
|
|
99
|
+
organizationId?: string | null
|
|
100
|
+
}
|
|
101
|
+
|
|
88
102
|
/**
|
|
89
103
|
* Execute a function with a timeout
|
|
90
104
|
*/
|
|
@@ -113,7 +127,8 @@ async function withTimeout<T>(
|
|
|
113
127
|
*/
|
|
114
128
|
export async function executeRules(
|
|
115
129
|
em: EntityManager,
|
|
116
|
-
context: RuleEngineContext
|
|
130
|
+
context: RuleEngineContext,
|
|
131
|
+
options: RuleEngineExecutionOptions = {}
|
|
117
132
|
): Promise<RuleEngineResult> {
|
|
118
133
|
// Validate input
|
|
119
134
|
const validation = ruleEngineContextSchema.safeParse(context)
|
|
@@ -159,7 +174,7 @@ export async function executeRules(
|
|
|
159
174
|
const executionPromise = (async () => {
|
|
160
175
|
for (const rule of rules) {
|
|
161
176
|
try {
|
|
162
|
-
const ruleResult = await executeSingleRule(em, rule, context)
|
|
177
|
+
const ruleResult = await executeSingleRule(em, rule, context, options)
|
|
163
178
|
executedRules.push(ruleResult)
|
|
164
179
|
|
|
165
180
|
if (ruleResult.logId) {
|
|
@@ -177,6 +192,17 @@ export async function executeRules(
|
|
|
177
192
|
`Unexpected error in rule execution [ruleId=${rule.ruleId}, type=${rule.ruleType}]: ${errorMessage}`
|
|
178
193
|
)
|
|
179
194
|
|
|
195
|
+
if (!context.dryRun) {
|
|
196
|
+
await emitRuleExecutionFailed(options.eventBus, {
|
|
197
|
+
ruleId: rule.ruleId,
|
|
198
|
+
ruleName: rule.ruleName,
|
|
199
|
+
entityType: context.entityType ?? null,
|
|
200
|
+
errorMessage,
|
|
201
|
+
tenantId: context.tenantId,
|
|
202
|
+
organizationId: context.organizationId ?? null,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
180
206
|
executedRules.push({
|
|
181
207
|
rule,
|
|
182
208
|
conditionResult: false,
|
|
@@ -233,7 +259,8 @@ export async function executeRules(
|
|
|
233
259
|
export async function executeSingleRule(
|
|
234
260
|
em: EntityManager,
|
|
235
261
|
rule: BusinessRule,
|
|
236
|
-
context: RuleEngineContext
|
|
262
|
+
context: RuleEngineContext,
|
|
263
|
+
options: RuleEngineExecutionOptions = {}
|
|
237
264
|
): Promise<RuleExecutionResult> {
|
|
238
265
|
const startTime = Date.now()
|
|
239
266
|
|
|
@@ -269,6 +296,15 @@ export async function executeSingleRule(
|
|
|
269
296
|
executionTime,
|
|
270
297
|
error: result.error,
|
|
271
298
|
})
|
|
299
|
+
|
|
300
|
+
await emitRuleExecutionFailed(options.eventBus, {
|
|
301
|
+
ruleId: rule.ruleId,
|
|
302
|
+
ruleName: rule.ruleName,
|
|
303
|
+
entityType: context.entityType ?? null,
|
|
304
|
+
errorMessage: result.error ?? null,
|
|
305
|
+
tenantId: context.tenantId,
|
|
306
|
+
organizationId: context.organizationId ?? null,
|
|
307
|
+
})
|
|
272
308
|
}
|
|
273
309
|
|
|
274
310
|
return {
|
|
@@ -346,6 +382,15 @@ export async function executeSingleRule(
|
|
|
346
382
|
executionTime,
|
|
347
383
|
error: enhancedError,
|
|
348
384
|
})
|
|
385
|
+
|
|
386
|
+
await emitRuleExecutionFailed(options.eventBus, {
|
|
387
|
+
ruleId: rule.ruleId,
|
|
388
|
+
ruleName: rule.ruleName,
|
|
389
|
+
entityType: context.entityType ?? null,
|
|
390
|
+
errorMessage: enhancedError,
|
|
391
|
+
tenantId: context.tenantId,
|
|
392
|
+
organizationId: context.organizationId ?? null,
|
|
393
|
+
})
|
|
349
394
|
}
|
|
350
395
|
|
|
351
396
|
return {
|
|
@@ -544,3 +589,12 @@ export async function logRuleExecution(
|
|
|
544
589
|
|
|
545
590
|
return log.id
|
|
546
591
|
}
|
|
592
|
+
|
|
593
|
+
async function emitRuleExecutionFailed(
|
|
594
|
+
eventBus: Pick<EventBus, 'emitEvent'> | null | undefined,
|
|
595
|
+
payload: RuleExecutionFailedPayload
|
|
596
|
+
): Promise<void> {
|
|
597
|
+
if (!eventBus?.emitEvent) return
|
|
598
|
+
|
|
599
|
+
await eventBus.emitEvent('business_rules.rule.execution_failed', payload).catch(() => undefined)
|
|
600
|
+
}
|
|
@@ -194,7 +194,7 @@ export function CachePanel() {
|
|
|
194
194
|
<header className="space-y-1">
|
|
195
195
|
<h2 className="text-lg font-semibold">{t('configs.cache.title', 'Cache overview')}</h2>
|
|
196
196
|
<p className="text-sm text-muted-foreground">
|
|
197
|
-
{t('configs.cache.description', 'Inspect cached
|
|
197
|
+
{t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}
|
|
198
198
|
</p>
|
|
199
199
|
</header>
|
|
200
200
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
@@ -211,7 +211,7 @@ export function CachePanel() {
|
|
|
211
211
|
<header className="space-y-1">
|
|
212
212
|
<h2 className="text-lg font-semibold">{t('configs.cache.title', 'Cache overview')}</h2>
|
|
213
213
|
<p className="text-sm text-muted-foreground">
|
|
214
|
-
{t('configs.cache.description', 'Inspect cached
|
|
214
|
+
{t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}
|
|
215
215
|
</p>
|
|
216
216
|
</header>
|
|
217
217
|
<div className="rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700">
|
|
@@ -235,7 +235,7 @@ export function CachePanel() {
|
|
|
235
235
|
<div className="space-y-1">
|
|
236
236
|
<h2 className="text-lg font-semibold">{t('configs.cache.title', 'Cache overview')}</h2>
|
|
237
237
|
<p className="text-sm text-muted-foreground">
|
|
238
|
-
{t('configs.cache.description', 'Inspect cached
|
|
238
|
+
{t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}
|
|
239
239
|
</p>
|
|
240
240
|
{stats ? (
|
|
241
241
|
<>
|
|
@@ -342,7 +342,7 @@ export function CachePanel() {
|
|
|
342
342
|
</div>
|
|
343
343
|
) : (
|
|
344
344
|
<p className="text-sm text-muted-foreground">
|
|
345
|
-
{t('configs.cache.empty', 'No cached
|
|
345
|
+
{t('configs.cache.empty', 'No cached responses for this tenant.')}
|
|
346
346
|
</p>
|
|
347
347
|
)}
|
|
348
348
|
</div>
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"configs.systemStatus.actions.purgeCacheUnavailable": "Cache service is unavailable.",
|
|
36
36
|
"configs.config.nav.cache": "Cache",
|
|
37
37
|
"configs.cache.title": "Cache overview",
|
|
38
|
-
"configs.cache.description": "Inspect cached
|
|
38
|
+
"configs.cache.description": "Inspect cached responses and clear segments when necessary.",
|
|
39
39
|
"configs.cache.loading": "Loading cache statistics…",
|
|
40
40
|
"configs.cache.loadError": "Failed to load cache statistics.",
|
|
41
41
|
"configs.cache.retry": "Retry",
|
|
42
42
|
"configs.cache.refresh": "Refresh",
|
|
43
43
|
"configs.cache.generatedAt": "Stats generated {{timestamp}}",
|
|
44
44
|
"configs.cache.totalEntries": "{{count}} cached entries",
|
|
45
|
-
"configs.cache.empty": "No cached
|
|
45
|
+
"configs.cache.empty": "No cached responses for this tenant.",
|
|
46
46
|
"configs.cache.purgeAll": "Purge all cache",
|
|
47
47
|
"configs.cache.purgeAllLoading": "Purging…",
|
|
48
48
|
"configs.cache.purgeAllConfirm": "Purge all cached entries for this tenant?",
|
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
"configs.systemStatus.categories.profilingDescription": "Flags that control request and query profiling outputs.",
|
|
65
65
|
"configs.systemStatus.categories.logging": "Logging",
|
|
66
66
|
"configs.systemStatus.categories.loggingDescription": "Tune verbosity and SQL logging for diagnostics.",
|
|
67
|
+
"configs.systemStatus.categories.security": "Security",
|
|
68
|
+
"configs.systemStatus.categories.securityDescription": "Password policy requirements enforced at login and user creation.",
|
|
67
69
|
"configs.systemStatus.categories.caching": "Caching",
|
|
68
70
|
"configs.systemStatus.categories.cachingDescription": "Cache providers and TTL controls for backend responses.",
|
|
69
71
|
"configs.systemStatus.categories.queryIndex": "Query index",
|
|
@@ -84,6 +86,14 @@
|
|
|
84
86
|
"configs.systemStatus.variables.logVerbosity.description": "Overrides structured log verbosity such as debug or trace.",
|
|
85
87
|
"configs.systemStatus.variables.logLevel.label": "Log level",
|
|
86
88
|
"configs.systemStatus.variables.logLevel.description": "Fallback log level applied when verbosity is not set.",
|
|
89
|
+
"configs.systemStatus.variables.passwordMinLength.label": "Password min length",
|
|
90
|
+
"configs.systemStatus.variables.passwordMinLength.description": "Minimum number of characters required for passwords.",
|
|
91
|
+
"configs.systemStatus.variables.passwordRequireDigit.label": "Password requires digit",
|
|
92
|
+
"configs.systemStatus.variables.passwordRequireDigit.description": "Require at least one numeric character.",
|
|
93
|
+
"configs.systemStatus.variables.passwordRequireUppercase.label": "Password requires uppercase",
|
|
94
|
+
"configs.systemStatus.variables.passwordRequireUppercase.description": "Require at least one uppercase letter.",
|
|
95
|
+
"configs.systemStatus.variables.passwordRequireSpecial.label": "Password requires special",
|
|
96
|
+
"configs.systemStatus.variables.passwordRequireSpecial.description": "Require at least one special character.",
|
|
87
97
|
"configs.systemStatus.variables.enableCrudApiCache.label": "CRUD API cache",
|
|
88
98
|
"configs.systemStatus.variables.enableCrudApiCache.description": "Enable the CRUD API response cache layer.",
|
|
89
99
|
"configs.systemStatus.variables.cacheStrategy.label": "Cache strategy",
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"configs.systemStatus.actions.purgeCacheUnavailable": "Usługa pamięci podręcznej jest niedostępna.",
|
|
36
36
|
"configs.config.nav.cache": "Pamięć podręczna",
|
|
37
37
|
"configs.cache.title": "Podgląd pamięci podręcznej",
|
|
38
|
-
"configs.cache.description": "Przeglądaj zapisane odpowiedzi
|
|
38
|
+
"configs.cache.description": "Przeglądaj zapisane odpowiedzi i czyść segmenty w razie potrzeby.",
|
|
39
39
|
"configs.cache.loading": "Ładowanie statystyk pamięci podręcznej…",
|
|
40
40
|
"configs.cache.loadError": "Nie udało się wczytać statystyk pamięci podręcznej.",
|
|
41
41
|
"configs.cache.retry": "Spróbuj ponownie",
|
|
42
42
|
"configs.cache.refresh": "Odśwież",
|
|
43
43
|
"configs.cache.generatedAt": "Statystyki z {{timestamp}}",
|
|
44
44
|
"configs.cache.totalEntries": "{{count}} wpisów w pamięci podręcznej",
|
|
45
|
-
"configs.cache.empty": "Brak zapisanych odpowiedzi
|
|
45
|
+
"configs.cache.empty": "Brak zapisanych odpowiedzi dla tego tenanta.",
|
|
46
46
|
"configs.cache.purgeAll": "Wyczyść całą pamięć",
|
|
47
47
|
"configs.cache.purgeAllLoading": "Czyszczenie…",
|
|
48
48
|
"configs.cache.purgeAllConfirm": "Wyczyścić wszystkie wpisy pamięci podręcznej dla tego tenanta?",
|
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
"configs.systemStatus.categories.profilingDescription": "Flagi sterujące generowaniem danych z profilowania zapytań i żądań.",
|
|
65
65
|
"configs.systemStatus.categories.logging": "Logowanie",
|
|
66
66
|
"configs.systemStatus.categories.loggingDescription": "Dostosuj poziom logowania i zapisywanie zapytań SQL na potrzeby diagnostyki.",
|
|
67
|
+
"configs.systemStatus.categories.security": "Bezpieczeństwo",
|
|
68
|
+
"configs.systemStatus.categories.securityDescription": "Wymagania polityki haseł stosowane przy logowaniu i tworzeniu użytkowników.",
|
|
67
69
|
"configs.systemStatus.categories.caching": "Buforowanie",
|
|
68
70
|
"configs.systemStatus.categories.cachingDescription": "Mechanizmy cache oraz kontrola czasu życia odpowiedzi backendu.",
|
|
69
71
|
"configs.systemStatus.categories.queryIndex": "Indeks zapytań",
|
|
@@ -84,6 +86,14 @@
|
|
|
84
86
|
"configs.systemStatus.variables.logVerbosity.description": "Nadpisuje poziom szczegółowości logów, np. debug lub trace.",
|
|
85
87
|
"configs.systemStatus.variables.logLevel.label": "Poziom logowania",
|
|
86
88
|
"configs.systemStatus.variables.logLevel.description": "Domyślny poziom logowania używany, gdy nie ustawiono szczegółowości.",
|
|
89
|
+
"configs.systemStatus.variables.passwordMinLength.label": "Minimalna długość hasła",
|
|
90
|
+
"configs.systemStatus.variables.passwordMinLength.description": "Minimalna liczba znaków wymagana w haśle.",
|
|
91
|
+
"configs.systemStatus.variables.passwordRequireDigit.label": "Hasło wymaga cyfry",
|
|
92
|
+
"configs.systemStatus.variables.passwordRequireDigit.description": "Wymagaj co najmniej jednej cyfry.",
|
|
93
|
+
"configs.systemStatus.variables.passwordRequireUppercase.label": "Hasło wymaga wielkiej litery",
|
|
94
|
+
"configs.systemStatus.variables.passwordRequireUppercase.description": "Wymagaj co najmniej jednej wielkiej litery.",
|
|
95
|
+
"configs.systemStatus.variables.passwordRequireSpecial.label": "Hasło wymaga znaku specjalnego",
|
|
96
|
+
"configs.systemStatus.variables.passwordRequireSpecial.description": "Wymagaj co najmniej jednego znaku specjalnego.",
|
|
87
97
|
"configs.systemStatus.variables.enableCrudApiCache.label": "Cache API CRUD",
|
|
88
98
|
"configs.systemStatus.variables.enableCrudApiCache.description": "Włącza warstwę buforowania odpowiedzi API CRUD.",
|
|
89
99
|
"configs.systemStatus.variables.cacheStrategy.label": "Strategia cache",
|
|
@@ -19,7 +19,14 @@ type SystemStatusVariableDefinition = {
|
|
|
19
19
|
defaultValue: string | null
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const CATEGORY_ORDER: SystemStatusCategoryKey[] = [
|
|
22
|
+
const CATEGORY_ORDER: SystemStatusCategoryKey[] = [
|
|
23
|
+
'profiling',
|
|
24
|
+
'logging',
|
|
25
|
+
'security',
|
|
26
|
+
'caching',
|
|
27
|
+
'query_index',
|
|
28
|
+
'entities',
|
|
29
|
+
]
|
|
23
30
|
|
|
24
31
|
const CATEGORY_METADATA: Record<
|
|
25
32
|
SystemStatusCategoryKey,
|
|
@@ -33,6 +40,10 @@ const CATEGORY_METADATA: Record<
|
|
|
33
40
|
labelKey: 'configs.systemStatus.categories.logging',
|
|
34
41
|
descriptionKey: 'configs.systemStatus.categories.loggingDescription',
|
|
35
42
|
},
|
|
43
|
+
security: {
|
|
44
|
+
labelKey: 'configs.systemStatus.categories.security',
|
|
45
|
+
descriptionKey: 'configs.systemStatus.categories.securityDescription',
|
|
46
|
+
},
|
|
36
47
|
caching: {
|
|
37
48
|
labelKey: 'configs.systemStatus.categories.caching',
|
|
38
49
|
descriptionKey: 'configs.systemStatus.categories.cachingDescription',
|
|
@@ -113,6 +124,42 @@ export const SYSTEM_STATUS_VARIABLES: SystemStatusVariableDefinition[] = [
|
|
|
113
124
|
docUrl: `${SYSTEM_STATUS_DOC_BASE}#log_level`,
|
|
114
125
|
defaultValue: '',
|
|
115
126
|
},
|
|
127
|
+
{
|
|
128
|
+
key: 'OM_PASSWORD_MIN_LENGTH',
|
|
129
|
+
category: 'security',
|
|
130
|
+
kind: 'string',
|
|
131
|
+
labelKey: 'configs.systemStatus.variables.passwordMinLength.label',
|
|
132
|
+
descriptionKey: 'configs.systemStatus.variables.passwordMinLength.description',
|
|
133
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_min_length`,
|
|
134
|
+
defaultValue: '6',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
key: 'OM_PASSWORD_REQUIRE_DIGIT',
|
|
138
|
+
category: 'security',
|
|
139
|
+
kind: 'boolean',
|
|
140
|
+
labelKey: 'configs.systemStatus.variables.passwordRequireDigit.label',
|
|
141
|
+
descriptionKey: 'configs.systemStatus.variables.passwordRequireDigit.description',
|
|
142
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_digit`,
|
|
143
|
+
defaultValue: 'true',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
key: 'OM_PASSWORD_REQUIRE_UPPERCASE',
|
|
147
|
+
category: 'security',
|
|
148
|
+
kind: 'boolean',
|
|
149
|
+
labelKey: 'configs.systemStatus.variables.passwordRequireUppercase.label',
|
|
150
|
+
descriptionKey: 'configs.systemStatus.variables.passwordRequireUppercase.description',
|
|
151
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_uppercase`,
|
|
152
|
+
defaultValue: 'true',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
key: 'OM_PASSWORD_REQUIRE_SPECIAL',
|
|
156
|
+
category: 'security',
|
|
157
|
+
kind: 'boolean',
|
|
158
|
+
labelKey: 'configs.systemStatus.variables.passwordRequireSpecial.label',
|
|
159
|
+
descriptionKey: 'configs.systemStatus.variables.passwordRequireSpecial.description',
|
|
160
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_special`,
|
|
161
|
+
defaultValue: 'true',
|
|
162
|
+
},
|
|
116
163
|
{
|
|
117
164
|
key: 'ENABLE_CRUD_API_CACHE',
|
|
118
165
|
category: 'caching',
|
|
@@ -42,15 +42,25 @@ export async function seedDashboardDefaultsForTenant(
|
|
|
42
42
|
const widgetMap = new Map(widgets.map((widget) => [widget.metadata.id, widget]))
|
|
43
43
|
const resolvedWidgetIds = widgetIds && widgetIds.length
|
|
44
44
|
? widgetIds.filter((id) => widgetMap.has(id))
|
|
45
|
-
:
|
|
45
|
+
: null
|
|
46
|
+
const defaultWidgetIds = widgets
|
|
47
|
+
.filter((widget) => widget.metadata.defaultEnabled)
|
|
48
|
+
.map((widget) => widget.metadata.id)
|
|
49
|
+
const allWidgetIds = widgets.map((widget) => widget.metadata.id)
|
|
46
50
|
|
|
47
|
-
if (
|
|
51
|
+
if (resolvedWidgetIds && resolvedWidgetIds.length === 0) {
|
|
48
52
|
log('No widgets resolved for dashboard seeding.')
|
|
49
53
|
return false
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
await em.transactional(async (tem) => {
|
|
53
57
|
for (const roleName of roleNames) {
|
|
58
|
+
const isAdminRole = roleName === 'admin' || roleName === 'superadmin'
|
|
59
|
+
const roleWidgetIds = resolvedWidgetIds ?? (isAdminRole ? allWidgetIds : defaultWidgetIds)
|
|
60
|
+
if (!roleWidgetIds.length) {
|
|
61
|
+
log(`No widgets resolved for role "${roleName}".`)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
54
64
|
const role = await tem.findOne(Role, { name: roleName })
|
|
55
65
|
if (!role) {
|
|
56
66
|
log(`Skipping role "${roleName}" (not found)`)
|
|
@@ -63,7 +73,7 @@ export async function seedDashboardDefaultsForTenant(
|
|
|
63
73
|
deletedAt: null,
|
|
64
74
|
})
|
|
65
75
|
if (existing) {
|
|
66
|
-
existing.widgetIdsJson =
|
|
76
|
+
existing.widgetIdsJson = roleWidgetIds
|
|
67
77
|
tem.persist(existing)
|
|
68
78
|
log(`Updated dashboard widgets for role "${roleName}"`)
|
|
69
79
|
} else {
|
|
@@ -71,7 +81,7 @@ export async function seedDashboardDefaultsForTenant(
|
|
|
71
81
|
roleId: String(role.id),
|
|
72
82
|
tenantId,
|
|
73
83
|
organizationId,
|
|
74
|
-
widgetIdsJson:
|
|
84
|
+
widgetIdsJson: roleWidgetIds,
|
|
75
85
|
createdAt: new Date(),
|
|
76
86
|
updatedAt: null,
|
|
77
87
|
deletedAt: null,
|
|
@@ -44,9 +44,13 @@ type UserProps = BaseProps & {
|
|
|
44
44
|
|
|
45
45
|
type WidgetVisibilityEditorProps = RoleProps | UserProps
|
|
46
46
|
|
|
47
|
+
export type WidgetVisibilityEditorHandle = {
|
|
48
|
+
save: () => Promise<void>
|
|
49
|
+
}
|
|
50
|
+
|
|
47
51
|
const EMPTY: string[] = []
|
|
48
52
|
|
|
49
|
-
export function WidgetVisibilityEditor(props
|
|
53
|
+
export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHandle, WidgetVisibilityEditorProps>(function WidgetVisibilityEditor(props, ref) {
|
|
50
54
|
const t = useT()
|
|
51
55
|
const { kind, targetId, tenantId, organizationId } = props
|
|
52
56
|
const [catalog, setCatalog] = React.useState<WidgetCatalogItem[]>([])
|
|
@@ -60,6 +64,15 @@ export function WidgetVisibilityEditor(props: WidgetVisibilityEditorProps) {
|
|
|
60
64
|
const [originalMode, setOriginalMode] = React.useState<'inherit' | 'override'>('inherit')
|
|
61
65
|
const [effective, setEffective] = React.useState<string[]>(EMPTY)
|
|
62
66
|
|
|
67
|
+
const dirty = React.useMemo(() => {
|
|
68
|
+
if (kind === 'user') {
|
|
69
|
+
if (mode !== originalMode) return true
|
|
70
|
+
if (mode === 'override') return selected.join('|') !== original.join('|')
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
return selected.join('|') !== original.join('|')
|
|
74
|
+
}, [kind, mode, original, originalMode, selected])
|
|
75
|
+
|
|
63
76
|
const loadCatalog = React.useCallback(async () => {
|
|
64
77
|
const data = await readApiResultOrThrow<{ items?: unknown[] }>(
|
|
65
78
|
'/api/dashboards/widgets/catalog',
|
|
@@ -149,6 +162,9 @@ export function WidgetVisibilityEditor(props: WidgetVisibilityEditorProps) {
|
|
|
149
162
|
}, [original, originalMode])
|
|
150
163
|
|
|
151
164
|
const save = React.useCallback(async () => {
|
|
165
|
+
if (loading) return
|
|
166
|
+
if (error && catalog.length === 0) return
|
|
167
|
+
if (!dirty) return
|
|
152
168
|
setSaving(true)
|
|
153
169
|
setError(null)
|
|
154
170
|
try {
|
|
@@ -202,16 +218,9 @@ export function WidgetVisibilityEditor(props: WidgetVisibilityEditorProps) {
|
|
|
202
218
|
} finally {
|
|
203
219
|
setSaving(false)
|
|
204
220
|
}
|
|
205
|
-
}, [kind, mode, organizationId, selected, t, targetId, tenantId])
|
|
221
|
+
}, [catalog.length, dirty, error, kind, loading, mode, organizationId, selected, t, targetId, tenantId])
|
|
206
222
|
|
|
207
|
-
|
|
208
|
-
if (kind === 'user') {
|
|
209
|
-
if (mode !== originalMode) return true
|
|
210
|
-
if (mode === 'override') return selected.join('|') !== original.join('|')
|
|
211
|
-
return false
|
|
212
|
-
}
|
|
213
|
-
return selected.join('|') !== original.join('|')
|
|
214
|
-
}, [kind, mode, original, originalMode, selected])
|
|
223
|
+
React.useImperativeHandle(ref, () => ({ save }), [save])
|
|
215
224
|
|
|
216
225
|
if (loading) {
|
|
217
226
|
return (
|
|
@@ -297,4 +306,6 @@ export function WidgetVisibilityEditor(props: WidgetVisibilityEditorProps) {
|
|
|
297
306
|
</div>
|
|
298
307
|
</div>
|
|
299
308
|
)
|
|
300
|
-
}
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
WidgetVisibilityEditor.displayName = 'WidgetVisibilityEditor'
|