@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/audit_logs/services/actionLogService.js +57 -12
- package/dist/modules/audit_logs/services/actionLogService.js.map +3 -3
- package/dist/modules/auth/backend/roles/[id]/edit/page.meta.js +5 -1
- package/dist/modules/auth/backend/roles/[id]/edit/page.meta.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.meta.js +5 -1
- package/dist/modules/auth/backend/users/[id]/edit/page.meta.js.map +2 -2
- package/dist/modules/auth/components/AclEditor.js +39 -30
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/people/route.js +1 -1
- package/dist/modules/customers/api/companies/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +8 -9
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/companies/route.js +1 -1
- package/dist/modules/customers/api/deals/[id]/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +1 -1
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +8 -25
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/stats/route.js +3 -22
- package/dist/modules/customers/api/deals/[id]/stats/route.js.map +2 -2
- package/dist/modules/customers/api/entity-roles-factory.js +4 -4
- package/dist/modules/customers/api/entity-roles-factory.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/context.js +1 -1
- package/dist/modules/customers/api/people/[id]/companies/context.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +1 -1
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +11 -10
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/lib/detailReadAccess.js +12 -0
- package/dist/modules/customers/lib/detailReadAccess.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/audit_logs/services/actionLogService.ts +74 -11
- package/src/modules/auth/backend/roles/[id]/edit/page.meta.ts +5 -2
- package/src/modules/auth/backend/users/[id]/edit/page.meta.ts +5 -2
- package/src/modules/auth/components/AclEditor.tsx +63 -33
- package/src/modules/auth/i18n/de.json +380 -0
- package/src/modules/auth/i18n/en.json +380 -0
- package/src/modules/auth/i18n/es.json +380 -0
- package/src/modules/auth/i18n/ko.json +380 -0
- package/src/modules/auth/i18n/pl.json +382 -2
- package/src/modules/customers/api/companies/[id]/people/route.ts +4 -1
- package/src/modules/customers/api/companies/[id]/route.ts +13 -10
- package/src/modules/customers/api/deals/[id]/companies/route.ts +4 -1
- package/src/modules/customers/api/deals/[id]/people/route.ts +4 -1
- package/src/modules/customers/api/deals/[id]/route.ts +17 -28
- package/src/modules/customers/api/deals/[id]/stats/route.ts +10 -25
- package/src/modules/customers/api/entity-roles-factory.ts +8 -4
- package/src/modules/customers/api/people/[id]/companies/context.ts +4 -1
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +4 -1
- package/src/modules/customers/api/people/[id]/route.ts +16 -11
- package/src/modules/customers/lib/detailReadAccess.ts +35 -0
- package/src/modules/dashboards/i18n/es.json +4 -4
- 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(
|
|
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(
|
|
271
|
+
data = this.normalizeInput(sanitized)
|
|
221
272
|
}
|
|
222
273
|
} else {
|
|
223
|
-
data = this.normalizeInput(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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)
|
|
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-
|
|
316
|
-
<div className="text-sm font-medium text-
|
|
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-
|
|
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-
|
|
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-
|
|
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">
|
|
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">
|
|
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-
|
|
376
|
-
<div className="text-sm font-medium text-
|
|
377
|
-
|
|
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={
|
|
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
|
-
|
|
437
|
-
{
|
|
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">
|
|
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-
|
|
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
|
)}
|