@open-mercato/core 0.6.8-develop.7067.1.82e9867ea1 → 0.6.8-develop.7071.1.58862d0c9f

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 (54) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/audit_logs/services/actionLogService.js +57 -12
  3. package/dist/modules/audit_logs/services/actionLogService.js.map +3 -3
  4. package/dist/modules/auth/backend/roles/[id]/edit/page.meta.js +5 -1
  5. package/dist/modules/auth/backend/roles/[id]/edit/page.meta.js.map +2 -2
  6. package/dist/modules/auth/backend/users/[id]/edit/page.meta.js +5 -1
  7. package/dist/modules/auth/backend/users/[id]/edit/page.meta.js.map +2 -2
  8. package/dist/modules/auth/components/AclEditor.js +39 -30
  9. package/dist/modules/auth/components/AclEditor.js.map +2 -2
  10. package/dist/modules/customers/api/companies/[id]/people/route.js +1 -1
  11. package/dist/modules/customers/api/companies/[id]/people/route.js.map +2 -2
  12. package/dist/modules/customers/api/companies/[id]/route.js +8 -9
  13. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  14. package/dist/modules/customers/api/deals/[id]/companies/route.js +1 -1
  15. package/dist/modules/customers/api/deals/[id]/companies/route.js.map +2 -2
  16. package/dist/modules/customers/api/deals/[id]/people/route.js +1 -1
  17. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  18. package/dist/modules/customers/api/deals/[id]/route.js +8 -25
  19. package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
  20. package/dist/modules/customers/api/deals/[id]/stats/route.js +3 -22
  21. package/dist/modules/customers/api/deals/[id]/stats/route.js.map +2 -2
  22. package/dist/modules/customers/api/entity-roles-factory.js +4 -4
  23. package/dist/modules/customers/api/entity-roles-factory.js.map +2 -2
  24. package/dist/modules/customers/api/people/[id]/companies/context.js +1 -1
  25. package/dist/modules/customers/api/people/[id]/companies/context.js.map +2 -2
  26. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +1 -1
  27. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
  28. package/dist/modules/customers/api/people/[id]/route.js +11 -10
  29. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  30. package/dist/modules/customers/lib/detailReadAccess.js +12 -0
  31. package/dist/modules/customers/lib/detailReadAccess.js.map +7 -0
  32. package/package.json +7 -7
  33. package/src/modules/audit_logs/services/actionLogService.ts +74 -11
  34. package/src/modules/auth/backend/roles/[id]/edit/page.meta.ts +5 -2
  35. package/src/modules/auth/backend/users/[id]/edit/page.meta.ts +5 -2
  36. package/src/modules/auth/components/AclEditor.tsx +63 -33
  37. package/src/modules/auth/i18n/de.json +380 -0
  38. package/src/modules/auth/i18n/en.json +380 -0
  39. package/src/modules/auth/i18n/es.json +380 -0
  40. package/src/modules/auth/i18n/ko.json +380 -0
  41. package/src/modules/auth/i18n/pl.json +382 -2
  42. package/src/modules/customers/api/companies/[id]/people/route.ts +4 -1
  43. package/src/modules/customers/api/companies/[id]/route.ts +13 -10
  44. package/src/modules/customers/api/deals/[id]/companies/route.ts +4 -1
  45. package/src/modules/customers/api/deals/[id]/people/route.ts +4 -1
  46. package/src/modules/customers/api/deals/[id]/route.ts +17 -28
  47. package/src/modules/customers/api/deals/[id]/stats/route.ts +10 -25
  48. package/src/modules/customers/api/entity-roles-factory.ts +8 -4
  49. package/src/modules/customers/api/people/[id]/companies/context.ts +4 -1
  50. package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +4 -1
  51. package/src/modules/customers/api/people/[id]/route.ts +16 -11
  52. package/src/modules/customers/lib/detailReadAccess.ts +35 -0
  53. package/src/modules/dashboards/i18n/es.json +4 -4
  54. package/src/modules/dashboards/i18n/pl.json +7 -7
@@ -5,6 +5,7 @@ import { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'
5
5
  import {
6
6
  actionLogCreateSchema,
7
7
  actionLogListSchema,
8
+ uuid,
8
9
  type ActionLogCreateInput,
9
10
  type ActionLogListQuery,
10
11
  } from '@open-mercato/core/modules/audit_logs/data/validators'
@@ -34,6 +35,55 @@ const SORT_FIELDS = {
34
35
  createdAt: 'action_logs.created_at',
35
36
  } as const
36
37
  const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/
38
+ // Mirrors what `uuid` (`data/validators.ts`) accepts — versions 1-8 plus the nil and
39
+ // max UUIDs — and is used only when the zod runtime is unavailable, so the actor
40
+ // sanitizer can never reject a value `actionLogCreateSchema` would have accepted.
41
+ export const SCHEMA_UUID_REGEX = /^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/
42
+ const API_KEY_ACTOR_PREFIX = 'api_key:'
43
+ const SYSTEM_ACTOR_PREFIX = 'system:'
44
+ // `context.systemActor` names the automated principal behind an entry; `context.source`
45
+ // (read by `deriveActionLogSource`) names the channel it arrived through. They are
46
+ // siblings on purpose and never contradict each other: an entry carrying `systemActor`
47
+ // has a null actor column, which already derives the `system` source.
48
+ const SYSTEM_ACTOR_CONTEXT_KEY = 'systemActor'
49
+ const SYSTEM_ACTOR_REFERENCE_MAX_LENGTH = 255
50
+
51
+ function toNullableUuid(value: unknown): string | null {
52
+ return typeof value === 'string' && UUID_REGEX.test(value) ? value : null
53
+ }
54
+
55
+ function isSchemaUuid(value: string): boolean {
56
+ if (runtimeValidationAvailable === false) return SCHEMA_UUID_REGEX.test(value)
57
+
58
+ try {
59
+ return uuid.safeParse(value).success
60
+ } catch (err) {
61
+ if (isZodRuntimeMissing(err)) runtimeValidationAvailable = false
62
+ return SCHEMA_UUID_REGEX.test(value)
63
+ }
64
+ }
65
+
66
+ function readActorCandidate(value: unknown): string | null {
67
+ if (typeof value !== 'string') return null
68
+ const trimmed = value.trim()
69
+ return trimmed.length > 0 ? trimmed : null
70
+ }
71
+
72
+ function normalizeActorUserId(value: unknown): string | null {
73
+ const candidate = readActorCandidate(value)
74
+ if (!candidate) return null
75
+ const unwrapped = candidate.startsWith(API_KEY_ACTOR_PREFIX)
76
+ ? candidate.slice(API_KEY_ACTOR_PREFIX.length)
77
+ : candidate
78
+
79
+ return isSchemaUuid(unwrapped) ? unwrapped : null
80
+ }
81
+
82
+ function toSystemActorReference(candidate: string): string | null {
83
+ if (!candidate.startsWith(SYSTEM_ACTOR_PREFIX)) return null
84
+ if (candidate.length === SYSTEM_ACTOR_PREFIX.length) return null
85
+ return candidate.slice(0, SYSTEM_ACTOR_REFERENCE_MAX_LENGTH)
86
+ }
37
87
 
38
88
  type ActionLogProjectionBackfillOptions = {
39
89
  batchSize?: number
@@ -202,6 +252,7 @@ export class ActionLogService {
202
252
  }
203
253
 
204
254
  private parseCreateInput(input: ActionLogCreateInput): ActionLogCreateInput {
255
+ const sanitized = this.sanitizeActor(input)
205
256
  let data: ActionLogCreateInput
206
257
  const schema = actionLogCreateSchema as typeof actionLogCreateSchema & { _zod?: unknown }
207
258
  const canValidate = Boolean(schema && typeof schema.parse === 'function')
@@ -209,7 +260,7 @@ export class ActionLogService {
209
260
 
210
261
  if (shouldValidate) {
211
262
  try {
212
- data = schema.parse(input)
263
+ data = schema.parse(sanitized)
213
264
  runtimeValidationAvailable = true
214
265
  } catch (err) {
215
266
  if (!isZodRuntimeMissing(err) && !validationWarningLogged) {
@@ -217,15 +268,34 @@ export class ActionLogService {
217
268
  logger.warn('Falling back to permissive action log payload parser', { err })
218
269
  }
219
270
  if (isZodRuntimeMissing(err)) runtimeValidationAvailable = false
220
- data = this.normalizeInput(input)
271
+ data = this.normalizeInput(sanitized)
221
272
  }
222
273
  } else {
223
- data = this.normalizeInput(input)
274
+ data = this.normalizeInput(sanitized)
224
275
  }
225
276
 
226
277
  return data
227
278
  }
228
279
 
280
+ private sanitizeActor(input: ActionLogCreateInput): ActionLogCreateInput {
281
+ if (!input) return input
282
+ const candidate = readActorCandidate(input.actorUserId)
283
+ const actorUserId = normalizeActorUserId(candidate)
284
+ const systemActorReference = candidate && !actorUserId ? toSystemActorReference(candidate) : null
285
+
286
+ if (!systemActorReference) {
287
+ if (actorUserId === (input.actorUserId ?? null)) return input
288
+ return { ...input, actorUserId }
289
+ }
290
+
291
+ const context = isRecord(input.context) ? { ...input.context } : {}
292
+ if (context[SYSTEM_ACTOR_CONTEXT_KEY] === undefined) {
293
+ context[SYSTEM_ACTOR_CONTEXT_KEY] = systemActorReference
294
+ }
295
+
296
+ return { ...input, actorUserId, context }
297
+ }
298
+
229
299
  private createLogEntity(fork: EntityManager, data: ActionLogCreateInput): ActionLog {
230
300
  const projection = deriveActionLogProjection({
231
301
  actorUserId: data.actorUserId ?? null,
@@ -286,13 +356,6 @@ export class ActionLogService {
286
356
  }
287
357
  }
288
358
 
289
- const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/
290
- const toNullableUuid = (value: unknown) => {
291
- if (typeof value !== 'string' || value.length === 0) return null
292
- const candidate = value.startsWith('api_key:') ? value.slice('api_key:'.length) : value
293
- return UUID_REGEX.test(candidate) ? candidate : null
294
- }
295
-
296
359
  const normalizeRecordLike = (value: unknown): ActionLogCreateInput['changes'] => {
297
360
  if (value === null) return null
298
361
  if (Array.isArray(value)) return value
@@ -309,7 +372,7 @@ export class ActionLogService {
309
372
  return {
310
373
  tenantId: toNullableUuid(input.tenantId),
311
374
  organizationId: toNullableUuid(input.organizationId),
312
- actorUserId: toNullableUuid(input.actorUserId),
375
+ actorUserId: normalizeActorUserId(input.actorUserId),
313
376
  commandId: typeof input.commandId === 'string' && input.commandId.length > 0 ? input.commandId : 'unknown',
314
377
  actionLabel: toOptionalString(input.actionLabel) ?? undefined,
315
378
  resourceKind: toOptionalString(input.resourceKind) ?? undefined,
@@ -2,7 +2,10 @@ export const metadata = {
2
2
  requireAuth: true,
3
3
  requireFeatures: ['auth.roles.manage'],
4
4
  pageTitle: 'Edit Role',
5
- breadcrumb: [ { label: 'Roles', href: '/backend/roles' }, { label: 'Edit' } ],
5
+ pageTitleKey: 'auth.roles.form.title.edit',
6
+ breadcrumb: [
7
+ { label: 'Roles', labelKey: 'auth.nav.roles', href: '/backend/roles' },
8
+ { label: 'Edit', labelKey: 'common.edit' },
9
+ ],
6
10
  }
7
11
 
8
-
@@ -2,7 +2,10 @@ export const metadata = {
2
2
  requireAuth: true,
3
3
  requireFeatures: ['auth.users.edit'],
4
4
  pageTitle: 'Edit User',
5
- breadcrumb: [ { label: 'Users', href: '/backend/users' }, { label: 'Edit' } ],
5
+ pageTitleKey: 'auth.users.form.title.edit',
6
+ breadcrumb: [
7
+ { label: 'Users', labelKey: 'auth.nav.users', href: '/backend/users' },
8
+ { label: 'Edit', labelKey: 'common.edit' },
9
+ ],
6
10
  }
7
11
 
8
-
@@ -4,14 +4,10 @@ import { Button } from '@open-mercato/ui/primitives/button'
4
4
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
5
5
  import Link from 'next/link'
6
6
  import { hasFeature, matchFeature } from '@open-mercato/shared/security/features'
7
- import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
8
8
  import type { FeatureDescriptor } from '@open-mercato/shared/security/aclDependencies'
9
9
  import { AclDependencyDiagnosticsPanel } from './AclDependencyDiagnosticsPanel'
10
10
 
11
- function toTitleCase(value: string): string {
12
- return value.replace(/[-_.]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase())
13
- }
14
-
15
11
  function normalizeFeatureArray(input: unknown): string[] {
16
12
  if (!Array.isArray(input)) return []
17
13
  const dedup = new Set<string>()
@@ -30,12 +26,16 @@ function isTenantRestrictedFeature(feature: string): boolean {
30
26
  return false
31
27
  }
32
28
 
33
- function formatWildcardLabel(moduleId: string, wildcard: string): string {
29
+ function formatWildcardLabel(t: TranslateFn, moduleId: string, wildcard: string): string {
34
30
  if (!wildcard.endsWith('.*')) return wildcard
35
31
  const prefix = `${moduleId}.`
36
32
  const suffix = wildcard.startsWith(prefix) ? wildcard.slice(prefix.length, -2) : wildcard.slice(0, -2)
37
- if (!suffix) return 'All features'
38
- return `All ${suffix.split('.').map(toTitleCase).join(' / ')}`
33
+ if (!suffix) return t('auth.acl.wildcards.allFeatures', 'All features')
34
+ const group = suffix
35
+ .split('.')
36
+ .map((segment) => segment.replace(/[-_]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()))
37
+ .join(' / ')
38
+ return t('auth.acl.wildcards.allGroup', 'All permissions in {group}', { group })
39
39
  }
40
40
 
41
41
  type Feature = { id: string; title: string; module: string; dependsOn?: string[] }
@@ -257,19 +257,28 @@ export function AclEditor({
257
257
  const grouped = React.useMemo(() => {
258
258
  const moduleMap = new Map<string, string>()
259
259
  for (const m of modules) {
260
- moduleMap.set(m.id, m.title)
260
+ moduleMap.set(m.id, t(`auth.acl.modules.${m.id}`, m.title))
261
261
  }
262
262
  const map = new Map<string, { moduleId: string; moduleTitle: string; features: Feature[] }>()
263
263
  for (const f of features) {
264
264
  const moduleId = f.module
265
265
  const moduleTitle = moduleMap.get(moduleId) || moduleId
266
+ const localizedFeature = {
267
+ ...f,
268
+ title: t(`auth.acl.features.${f.id}`, f.title),
269
+ }
266
270
  if (!map.has(moduleId)) {
267
271
  map.set(moduleId, { moduleId, moduleTitle, features: [] })
268
272
  }
269
- map.get(moduleId)!.features.push(f)
273
+ map.get(moduleId)!.features.push(localizedFeature)
270
274
  }
271
275
  return Array.from(map.values()).sort((a, b) => a.moduleTitle.localeCompare(b.moduleTitle))
272
- }, [features, modules])
276
+ }, [features, modules, t])
277
+
278
+ const localizedFeatures = React.useMemo(
279
+ () => grouped.flatMap((group) => group.features),
280
+ [grouped],
281
+ )
273
282
 
274
283
  const hasGlobalWildcard = granted.includes('*')
275
284
  const hasOrganizationRestriction = Array.isArray(organizations) && organizations.length > 0
@@ -305,22 +314,28 @@ export function AclEditor({
305
314
 
306
315
  const isFeatureChecked = (featureId: string) => hasFeature(granted, featureId)
307
316
 
308
- if (loading) return <div className="text-sm text-muted-foreground">Loading ACL…</div>
317
+ if (loading) {
318
+ return (
319
+ <div className="text-sm text-muted-foreground">
320
+ {t('auth.acl.loading', 'Loading access control…')}
321
+ </div>
322
+ )
323
+ }
309
324
 
310
325
  const showRoleBanner = kind === 'user' && !hasCustomAcl && !overrideEnabled
311
326
 
312
327
  return (
313
328
  <div className="space-y-4">
314
329
  {showRoleBanner && (
315
- <div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
316
- <div className="text-sm font-medium text-blue-900 mb-2">
317
- Permissions inherited from roles
330
+ <div className="rounded-lg border border-status-info-border bg-status-info-bg p-4">
331
+ <div className="text-sm font-medium text-status-info-text mb-2">
332
+ {t('auth.acl.inherited.title', 'Permissions inherited from roles')}
318
333
  </div>
319
- <div className="text-sm text-blue-700 mb-3">
320
- This user currently inherits permissions from their assigned roles.
334
+ <div className="text-sm text-status-info-text mb-3">
335
+ {t('auth.acl.inherited.description', 'This user currently inherits permissions from their assigned roles.')}
321
336
  {roleDetails.length > 0 && (
322
337
  <span>
323
- {' '}Assigned roles:{' '}
338
+ {' '}{t('auth.acl.inherited.assignedRoles', 'Assigned roles:')}{' '}
324
339
  {roleDetails.map((role, idx) => {
325
340
  const roleId = typeof role?.id === 'string' && role.id.length > 0 ? role.id : `role-${idx}`
326
341
  const roleName = typeof role?.name === 'string' && role.name.length > 0 ? role.name : roleId
@@ -329,7 +344,7 @@ export function AclEditor({
329
344
  {idx > 0 && ', '}
330
345
  <Link
331
346
  href={`/backend/roles/${roleId}/edit`}
332
- className="font-semibold text-blue-900 underline hover:text-blue-950 transition-colors"
347
+ className="font-semibold text-status-info-text underline"
333
348
  >
334
349
  {roleName}
335
350
  </Link>
@@ -347,8 +362,8 @@ export function AclEditor({
347
362
  checked={overrideEnabled}
348
363
  onChange={(e) => setOverrideEnabled(e.target.checked)}
349
364
  />
350
- <label htmlFor="overrideAcl" className="text-sm text-blue-900 font-medium">
351
- Override permissions for this user only
365
+ <label htmlFor="overrideAcl" className="text-sm text-status-info-text font-medium">
366
+ {t('auth.acl.inherited.override', 'Override permissions for this user only')}
352
367
  </label>
353
368
  </div>
354
369
  </div>
@@ -364,31 +379,39 @@ export function AclEditor({
364
379
  disabled={!actorIsSuperAdmin}
365
380
  onChange={(e) => setIsSuperAdmin(!!e.target.checked)}
366
381
  />
367
- <label htmlFor="isSuperAdmin" className="text-sm">Super Admin (all features)</label>
382
+ <label htmlFor="isSuperAdmin" className="text-sm">
383
+ {t('auth.acl.superAdmin.label', 'Super Admin (all features)')}
384
+ </label>
368
385
  </div>
369
386
  {!actorIsSuperAdmin && (
370
- <p className="text-xs text-muted-foreground">Only super administrators can change this option.</p>
387
+ <p className="text-xs text-muted-foreground">
388
+ {t('auth.acl.superAdmin.hint', 'Only super administrators can change this option.')}
389
+ </p>
371
390
  )}
372
391
  {!isSuperAdmin && (
373
392
  <>
374
393
  {hasGlobalWildcard && (
375
- <div className="rounded border border-blue-200 bg-blue-50 p-3">
376
- <div className="text-sm font-medium text-blue-900">Global wildcard (*) enabled</div>
377
- <div className="text-xs text-blue-700 mt-1">This grants access to all features in the system.</div>
394
+ <div className="rounded border border-status-info-border bg-status-info-bg p-3">
395
+ <div className="text-sm font-medium text-status-info-text">
396
+ {t('auth.acl.globalWildcard.title', 'Global wildcard (*) enabled')}
397
+ </div>
398
+ <div className="text-xs text-status-info-text mt-1">
399
+ {t('auth.acl.globalWildcard.description', 'This grants access to all features in the system.')}
400
+ </div>
378
401
  <Button
379
402
  variant="outline"
380
403
  size="sm"
381
404
  className="mt-2"
382
405
  onClick={() => updateGranted((prev) => prev.filter((x) => x !== '*'))}
383
406
  >
384
- Remove global wildcard
407
+ {t('auth.acl.globalWildcard.remove', 'Remove global wildcard')}
385
408
  </Button>
386
409
  </div>
387
410
  )}
388
411
  {!hasGlobalWildcard && (
389
412
  <AclDependencyDiagnosticsPanel
390
413
  granted={granted}
391
- catalog={features as readonly FeatureDescriptor[]}
414
+ catalog={localizedFeatures as readonly FeatureDescriptor[]}
392
415
  onGrantedChange={updateGranted}
393
416
  hideUnknownReferences={process.env.NODE_ENV === 'production'}
394
417
  />
@@ -433,8 +456,13 @@ export function AclEditor({
433
456
  onChange={(e) => toggleModuleWildcard(group.moduleId, e.target.checked)}
434
457
  />
435
458
  <label htmlFor={`module-${group.moduleId}`} className="text-sm text-muted-foreground">
436
- All {moduleWildcard && !hasGlobalWildcard ? <span className="font-medium text-blue-600">({group.moduleId}.*)</span> : ''}
437
- {moduleRestricted ? <span className="ml-2 text-xs font-medium text-muted-foreground">(manage via super admin)</span> : null}
459
+ {t('auth.acl.allModuleFeatures', 'All')}{' '}
460
+ {moduleWildcard && !hasGlobalWildcard ? <span className="font-medium text-status-info-text">({group.moduleId}.*)</span> : ''}
461
+ {moduleRestricted ? (
462
+ <span className="ml-2 text-xs font-medium text-muted-foreground">
463
+ {t('auth.acl.manageViaSuperAdmin', '(manage via super admin)')}
464
+ </span>
465
+ ) : null}
438
466
  </label>
439
467
  </div>
440
468
  </div>
@@ -459,7 +487,7 @@ export function AclEditor({
459
487
  htmlFor={`wildcard-${wildcard}`}
460
488
  className={`text-sm ${disabled ? 'text-muted-foreground' : ''}`}
461
489
  >
462
- {formatWildcardLabel(group.moduleId, wildcard)}{' '}
490
+ {formatWildcardLabel(t, group.moduleId, wildcard)}{' '}
463
491
  <span className="text-muted-foreground text-xs font-mono">({wildcard})</span>
464
492
  {wildcardRestricted ? (
465
493
  <span className="ml-2 text-xs font-medium text-muted-foreground">
@@ -535,7 +563,9 @@ export function AclEditor({
535
563
  <div className="text-sm font-medium mb-2">
536
564
  {t('auth.acl.organizationsScope', 'Organizations scope')}
537
565
  </div>
538
- <div className="text-xs text-muted-foreground mb-2">Empty = all organizations. Select one or more to restrict.</div>
566
+ <div className="text-xs text-muted-foreground mb-2">
567
+ {t('auth.acl.organizationsScopeHint', 'Empty means all organizations. Select one or more to restrict access.')}
568
+ </div>
539
569
  <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
540
570
  {orgOptions.map((o) => {
541
571
  const checked = organizations == null ? false : (organizations || []).includes(o.id)
@@ -557,7 +587,7 @@ export function AclEditor({
557
587
  <Button variant="outline" onClick={() => setOrganizations(null)}>{t('auth.acl.allowAllOrganizations', 'Allow all organizations')}</Button>
558
588
  </div>
559
589
  {showOrganizationWarning && (
560
- <div className="mt-3 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900">
590
+ <div className="mt-3 rounded border border-status-warning-border bg-status-warning-bg px-3 py-2 text-sm text-status-warning-text">
561
591
  {t('auth.acl.organizationWarning', 'Organization restrictions are saved only when at least one feature override is selected. Add a feature or enable a module wildcard before saving.')}
562
592
  </div>
563
593
  )}