feat: import row validation + KB/process per-item ACLs
Continuing the audit backlog and finishing the permissions UI. Import validation (recurring audit finding): - Added importMembersSchema / importPaymentsCsvSchema. importMembers and importPaymentsCsv now validate per row (a single bad row no longer rejects the whole file), lowercase emails, normalize dates through flexibleDateTime(), enforce tier/platform enums and positive finite amounts, and return a `skipped` count instead of silently dropping invalid rows. Permissions UI completion: - OpsAclEditor is now wired into the KB/process viewer modal (admin/board), with entity type derived from the `process` tag. Per-item Ops access control now covers secrets + KB + processes, matching "all ops facilities stuff". Left as-is with rationale in HANDOFF: /ops + /import denial paths (unreachable behind the layout gate or already a clear non-leaking denial); members-row make-area-lead shortcut (Customize -> Area leads fully covers assignment); types/database.ts + docs regen (pure hygiene, zero runtime/security impact, still tracked). 271/271 tests pass. Build clean. No migration this pass.
This commit is contained in:
@@ -430,12 +430,20 @@ function KbEntryRow({
|
||||
entry,
|
||||
onEdit,
|
||||
onDelete,
|
||||
canManageAcl,
|
||||
aclRoleOptions,
|
||||
aclByEntity,
|
||||
}: {
|
||||
entry: KbEntry
|
||||
onEdit: (e: KbEntry) => void
|
||||
onDelete: (id: string) => void
|
||||
canManageAcl?: boolean
|
||||
aclRoleOptions?: { value: string; label: string }[]
|
||||
aclByEntity?: Record<string, string[]>
|
||||
}) {
|
||||
const [viewing, setViewing] = useState(false)
|
||||
const aclEntityType: 'kb' | 'process' = entry.tags?.includes('process') ? 'process' : 'kb'
|
||||
const aclInitial = aclByEntity?.[`${aclEntityType}:${entry.id}`] ?? []
|
||||
|
||||
async function handleDelete(e: React.MouseEvent) {
|
||||
e.stopPropagation()
|
||||
@@ -525,6 +533,14 @@ function KbEntryRow({
|
||||
<div className="prose prose-sm prose-neutral dark:prose-invert max-w-none break-words">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{entry.content ?? ''}</ReactMarkdown>
|
||||
</div>
|
||||
{canManageAcl && (
|
||||
<OpsAclEditor
|
||||
entityType={aclEntityType}
|
||||
entityId={entry.id}
|
||||
options={aclRoleOptions ?? []}
|
||||
initial={aclInitial}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -671,6 +687,9 @@ export function OpsClient({ member, spaceId, kbEntries: initial, areaLeads: init
|
||||
entry={entry}
|
||||
onEdit={e => { setEditingKb(e); setShowKbModal(true) }}
|
||||
onDelete={id => setKbEntries(prev => prev.filter(e => e.id !== id))}
|
||||
canManageAcl={canManageAcl}
|
||||
aclRoleOptions={aclRoleOptions}
|
||||
aclByEntity={aclByEntity}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -686,6 +705,9 @@ export function OpsClient({ member, spaceId, kbEntries: initial, areaLeads: init
|
||||
entry={entry}
|
||||
onEdit={e => { setEditingKb(e); setShowKbModal(true) }}
|
||||
onDelete={id => setKbEntries(prev => prev.filter(e => e.id !== id))}
|
||||
canManageAcl={canManageAcl}
|
||||
aclRoleOptions={aclRoleOptions}
|
||||
aclByEntity={aclByEntity}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -720,6 +742,9 @@ export function OpsClient({ member, spaceId, kbEntries: initial, areaLeads: init
|
||||
entry={entry}
|
||||
onEdit={e => { setEditingKb(e); setShowKbModal(true) }}
|
||||
onDelete={id => setKbEntries(prev => prev.filter(e => e.id !== id))}
|
||||
canManageAcl={canManageAcl}
|
||||
aclRoleOptions={aclRoleOptions}
|
||||
aclByEntity={aclByEntity}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user