@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.
Files changed (60) hide show
  1. package/dist/modules/auth/backend/auth/profile/page.js.map +1 -1
  2. package/dist/modules/auth/backend/roles/[id]/edit/page.js +4 -1
  3. package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
  4. package/dist/modules/auth/backend/users/[id]/edit/page.js +4 -1
  5. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  6. package/dist/modules/auth/cli.js +13 -12
  7. package/dist/modules/auth/cli.js.map +2 -2
  8. package/dist/modules/business_rules/api/execute/route.js +7 -1
  9. package/dist/modules/business_rules/api/execute/route.js.map +2 -2
  10. package/dist/modules/business_rules/lib/rule-engine.js +33 -3
  11. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  12. package/dist/modules/configs/components/CachePanel.js +4 -4
  13. package/dist/modules/configs/components/CachePanel.js.map +2 -2
  14. package/dist/modules/configs/lib/system-status.js +48 -1
  15. package/dist/modules/configs/lib/system-status.js.map +2 -2
  16. package/dist/modules/dashboards/cli.js +12 -4
  17. package/dist/modules/dashboards/cli.js.map +2 -2
  18. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +16 -11
  19. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +3 -3
  20. package/dist/modules/dashboards/services/widgetDataService.js +110 -3
  21. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  22. package/dist/modules/notifications/data/validators.js +5 -1
  23. package/dist/modules/notifications/data/validators.js.map +2 -2
  24. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +2 -1
  25. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
  26. package/dist/modules/notifications/lib/deliveryConfig.js +4 -2
  27. package/dist/modules/notifications/lib/deliveryConfig.js.map +2 -2
  28. package/dist/modules/notifications/lib/deliveryStrategies.js +14 -0
  29. package/dist/modules/notifications/lib/deliveryStrategies.js.map +7 -0
  30. package/dist/modules/notifications/subscribers/deliver-notification.js +33 -7
  31. package/dist/modules/notifications/subscribers/deliver-notification.js.map +2 -2
  32. package/dist/modules/workflows/lib/transition-handler.js +14 -6
  33. package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
  34. package/package.json +2 -2
  35. package/src/modules/auth/README.md +1 -1
  36. package/src/modules/auth/__tests__/cli-setup-acl.test.ts +1 -1
  37. package/src/modules/auth/backend/auth/profile/page.tsx +2 -2
  38. package/src/modules/auth/backend/roles/[id]/edit/page.tsx +4 -1
  39. package/src/modules/auth/backend/users/[id]/edit/page.tsx +4 -1
  40. package/src/modules/auth/cli.ts +25 -12
  41. package/src/modules/business_rules/api/execute/route.ts +8 -1
  42. package/src/modules/business_rules/lib/__tests__/rule-engine.test.ts +51 -0
  43. package/src/modules/business_rules/lib/rule-engine.ts +57 -3
  44. package/src/modules/configs/components/CachePanel.tsx +4 -4
  45. package/src/modules/configs/i18n/en.json +12 -2
  46. package/src/modules/configs/i18n/pl.json +12 -2
  47. package/src/modules/configs/lib/system-status.ts +48 -1
  48. package/src/modules/configs/lib/system-status.types.ts +1 -0
  49. package/src/modules/dashboards/cli.ts +14 -4
  50. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +22 -11
  51. package/src/modules/dashboards/services/widgetDataService.ts +132 -4
  52. package/src/modules/notifications/__tests__/deliver-notification.test.ts +195 -0
  53. package/src/modules/notifications/__tests__/deliveryStrategies.test.ts +19 -0
  54. package/src/modules/notifications/__tests__/notificationService.test.ts +208 -0
  55. package/src/modules/notifications/data/validators.ts +5 -0
  56. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +2 -0
  57. package/src/modules/notifications/lib/deliveryConfig.ts +8 -0
  58. package/src/modules/notifications/lib/deliveryStrategies.ts +50 -0
  59. package/src/modules/notifications/subscribers/deliver-notification.ts +39 -10
  60. 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 () => {
@@ -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: Record<string, string> = {}
408
- for (let i = 0; i < rest.length; i += 2) {
409
- const k = rest[i]?.replace(/^--/, '')
410
- const v = rest[i + 1]
411
- if (k) args[k] = v
412
- }
413
- const orgName = args.orgName || args.name
414
- const email = args.email
415
- const password = args.password
416
- const rolesCsv = (args.roles ?? 'superadmin,admin,employee').trim()
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 CRUD responses and clear segments when necessary.')}
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 CRUD responses and clear segments when necessary.')}
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 CRUD responses and clear segments when necessary.')}
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 CRUD responses for this tenant.')}
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 CRUD responses and clear segments when necessary.",
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 CRUD responses for this tenant.",
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 CRUD i czyść segmenty w razie potrzeby.",
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 CRUD dla tego tenanta.",
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[] = ['profiling', 'logging', 'caching', 'query_index', 'entities']
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',
@@ -1,6 +1,7 @@
1
1
  export type SystemStatusCategoryKey =
2
2
  | 'profiling'
3
3
  | 'logging'
4
+ | 'security'
4
5
  | 'caching'
5
6
  | 'query_index'
6
7
  | 'entities'
@@ -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
- : widgets.filter((widget) => widget.metadata.defaultEnabled).map((widget) => widget.metadata.id)
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 (!resolvedWidgetIds.length) {
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 = resolvedWidgetIds
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: resolvedWidgetIds,
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: WidgetVisibilityEditorProps) {
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
- const dirty = React.useMemo(() => {
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'