Remove superseded role administration panels
Why: The Backend-authoritative role and account views replace the legacy local role and user panels. Evidence: npm test; npm run build; node scripts/check-knip.mjs; UI npm test. Impact: Keeps one role administration surface and restores the unused-export baseline. Refs: feature/iam-u4-role-authority
This commit is contained in:
+2
-2
@@ -20,7 +20,7 @@ export interface RoleRecord {
|
||||
roleId: string; name: string; description: string; enabled: boolean; roleVersion: number
|
||||
bindingCount: number; permissions: RolePermissionRecord[]
|
||||
}
|
||||
export interface RoleListPage { roles: RoleRecord[]; nextCursor?: string }
|
||||
interface RoleListPage { roles: RoleRecord[]; nextCursor?: string }
|
||||
export interface UserRoleBinding {
|
||||
bindingId: string; userId: string; roleId: string; active: boolean; bindingVersion: number
|
||||
assignedBy: string; assignedAt: string; userStateVersion: number; authorityEpoch: number
|
||||
@@ -31,7 +31,7 @@ export interface AccountRecord {
|
||||
userId: string; owner: string; profile: AccountProfile; profileVersion: number; enabled: boolean; stateVersion: number
|
||||
isAdmin: boolean; passwordSet: boolean; initializationState: string; initializationGeneration: number; createdAt?: string
|
||||
}
|
||||
export interface AccountSearchPage { accounts: AccountRecord[]; nextCursor?: string; directoryVersion: string }
|
||||
interface AccountSearchPage { accounts: AccountRecord[]; nextCursor?: string; directoryVersion: string }
|
||||
|
||||
async function checked(res: Response): Promise<Response> {
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// U6/KTD5:RDM 管理与项目页面组件。系统管理「RDM 角色权限」+ 项目设置「成员 /
|
||||
// 文档职责 · 整文审签」。全部数据经 api.rdmCommand(命令总线),载荷构造与能力
|
||||
// 门控集中在 src/rdmManagement.ts;分区可见性由 AdminZone/ProjectSettingsModal
|
||||
// 按 store 的 rdm*Available 门控(命令面不齐不渲染,普通基座零暴露)。样式只复用
|
||||
// 既有 ad-*/lg-*/settings-* 类(design-guard 棘轮:不新增裸色值/字号)。
|
||||
// RDM 项目设置组件:成员、文档职责与整文审签。全部数据经 api.rdmCommand
|
||||
//(命令总线),载荷构造与能力门控集中在 src/rdmManagement.ts。
|
||||
import { useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { api } from '../api'
|
||||
import type { Project } from '../contracts'
|
||||
@@ -10,142 +7,11 @@ import { projectWorkError } from '../projectmeta'
|
||||
import { useStore } from '../store'
|
||||
import { Btn, Collapsible, EmptyState, Icon, Picker } from '@ui'
|
||||
import {
|
||||
ROLE_ACTIONS,
|
||||
roleCreatePayload, roleActionPayload, roleAssignPayload, roleDisablePayload,
|
||||
memberAddPayload, memberRemovePayload, memberSetRolePayload,
|
||||
assignDocDutiesPayload, docSubmitPayload, docApprovePayload, docRejectPayload,
|
||||
docFinalPayload, docStatusPayload, permCheckPayload, describeSource,
|
||||
rolesListPlaceholder, describeRoleListError,
|
||||
} from '../rdmManagement'
|
||||
|
||||
// ---- 系统管理:RDM 角色权限(仅管理员 + rdmRolesAvailable) ----
|
||||
|
||||
interface RoleView {
|
||||
roleId: string
|
||||
name: string
|
||||
disabled: boolean
|
||||
version: number
|
||||
actions: Record<string, [boolean, boolean]> | null
|
||||
accounts: string[] | null
|
||||
}
|
||||
|
||||
export function RdmRolesPanel() {
|
||||
const me = useStore(s => s.me)
|
||||
const [roles, setRoles] = useState<RoleView[] | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
const [notice, setNotice] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [newName, setNewName] = useState('')
|
||||
// 动作编辑器
|
||||
const [editRole, setEditRole] = useState('')
|
||||
const [editAction, setEditAction] = useState<string>(ROLE_ACTIONS[0])
|
||||
const [editDef, setEditDef] = useState(false)
|
||||
const [editCap, setEditCap] = useState(true)
|
||||
// 绑定
|
||||
const [assignOwner, setAssignOwner] = useState('')
|
||||
|
||||
const refresh = async () => {
|
||||
try {
|
||||
const out = await api.rdmCommand<{ roles: RoleView[] }>('rdm.role.list')
|
||||
setErr(''); setRoles(out.roles ?? [])
|
||||
} catch (e) { setRoles(null); setErr(describeRoleListError(String(e))) }
|
||||
}
|
||||
useEffect(() => { refresh() }, [])
|
||||
|
||||
const run = async (label: string, fn: () => Promise<unknown>) => {
|
||||
setErr(''); setNotice(''); setBusy(true)
|
||||
try { await fn(); setNotice(label); await refresh() }
|
||||
catch (e) { setErr(describeRoleListError(String(e))) } finally { setBusy(false) }
|
||||
}
|
||||
|
||||
if (!me?.isAdmin) return <EmptyState icon="shield">仅平台管理员可管理专业角色</EmptyState>
|
||||
|
||||
// 占位行单一出口:加载中仅在无错误时成立(拒绝后永远转圈是误导——见
|
||||
// rolesListPlaceholder 注释)。
|
||||
const placeholder = rolesListPlaceholder(roles, err)
|
||||
|
||||
const sel = roles?.find(r => r.roleId === editRole)
|
||||
const editCurrent = sel?.actions?.[editAction]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="ad-create">
|
||||
<div className="ad-row">
|
||||
<input className="lg-input" placeholder="新角色名(如:编制工程师)" value={newName}
|
||||
onChange={e => setNewName(e.target.value)} />
|
||||
<button className="lg-submit ad-create-btn" disabled={busy || !newName.trim()}
|
||||
onClick={() => run(`已创建角色 ${newName.trim()}`, async () => {
|
||||
await api.rdmCommand('rdm.role.create', roleCreatePayload(newName.trim()))
|
||||
setNewName('')
|
||||
})}>创建角色</button>
|
||||
</div>
|
||||
<p className="ad-hint">新角色动作未配置=全部拒绝(fail-closed),创建后经下方编辑器逐动作配置默认与上限。</p>
|
||||
</div>
|
||||
|
||||
<table className="ad-table">
|
||||
<thead><tr><th>角色</th><th>状态</th><th>版本</th><th>动作配置</th><th>绑定账号</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{(roles ?? []).map(r => (
|
||||
<tr key={r.roleId}>
|
||||
<td><span className="ad-mono">{r.name}</span><br /><span className="ad-dim">{r.roleId}</span></td>
|
||||
<td>{r.disabled ? <span className="ad-chip ad-chip-warn">已停用</span> : <span className="ad-chip">生效</span>}</td>
|
||||
<td className="ad-dim">v{r.version}</td>
|
||||
<td>{Object.keys(r.actions ?? {}).length} 项</td>
|
||||
<td className="ad-mono">{(r.accounts ?? []).join('、') || '—'}</td>
|
||||
<td><button className="ad-mini" disabled={busy}
|
||||
onClick={() => run(r.disabled ? `已启用 ${r.name}` : `已停用 ${r.name}(绑定账号回落内置默认)`,
|
||||
() => api.rdmCommand('rdm.role.disable', roleDisablePayload(r.roleId, !r.disabled)))}>
|
||||
{r.disabled ? '启用' : '停用'}</button></td>
|
||||
</tr>
|
||||
))}
|
||||
{roles !== null && roles.length === 0 && <tr><td colSpan={6} className="ad-dim">暂无专业角色</td></tr>}
|
||||
{placeholder && <tr><td colSpan={6} className="ad-dim">{placeholder}</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4 className="settings-group-title" style={{ marginTop: 18 }}>动作配置(默认 ∩ 上限)</h4>
|
||||
<div className="ad-row">
|
||||
<select className="lg-input" value={editRole} onChange={e => setEditRole(e.target.value)}>
|
||||
<option value="">选择角色…</option>
|
||||
{(roles ?? []).map(r => <option key={r.roleId} value={r.roleId}>{r.name}</option>)}
|
||||
</select>
|
||||
<select className="lg-input" value={editAction}
|
||||
onChange={e => {
|
||||
setEditAction(e.target.value)
|
||||
const cur = sel?.actions?.[e.target.value]
|
||||
setEditDef(!!cur?.[0]); setEditCap(cur ? cur[1] : true)
|
||||
}}>
|
||||
{ROLE_ACTIONS.map(a => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
<label className="ad-check"><input type="checkbox" checked={editDef}
|
||||
onChange={e => setEditDef(e.target.checked)} />默认允许</label>
|
||||
<label className="ad-check"><input type="checkbox" checked={editCap}
|
||||
onChange={e => setEditCap(e.target.checked)} />上限允许</label>
|
||||
<button className="lg-submit ad-create-btn" disabled={busy || !editRole}
|
||||
onClick={() => run(`已配置 ${editRole} · ${editAction}`,
|
||||
() => api.rdmCommand('rdm.role.action', roleActionPayload(editRole, editAction, editDef, editCap)))}>保存动作</button>
|
||||
</div>
|
||||
{sel && editCurrent && <p className="ad-hint">当前值:默认={String(editCurrent[0])},上限={String(editCurrent[1])}</p>}
|
||||
|
||||
<h4 className="settings-group-title" style={{ marginTop: 18 }}>绑定账号(每账号一个生效角色)</h4>
|
||||
<div className="ad-row">
|
||||
<input className="lg-input lg-mono" placeholder="账号(owner)" value={assignOwner}
|
||||
onChange={e => setAssignOwner(e.target.value)} />
|
||||
<select className="lg-input" value={editRole} onChange={e => setEditRole(e.target.value)}>
|
||||
<option value="">选择角色…</option>
|
||||
{(roles ?? []).filter(r => !r.disabled).map(r => <option key={r.roleId} value={r.roleId}>{r.name}</option>)}
|
||||
</select>
|
||||
<button className="lg-submit ad-create-btn" disabled={busy || !assignOwner.trim() || !editRole}
|
||||
onClick={() => run(`已把 ${assignOwner.trim()} 绑定到 ${editRole}`,
|
||||
() => api.rdmCommand('rdm.role.assign', roleAssignPayload(assignOwner.trim(), editRole)))}>绑定</button>
|
||||
</div>
|
||||
|
||||
{notice && <div className="ad-notice"><Icon name="check" size={14} />{notice}</div>}
|
||||
{err && <div className="lg-err"><Icon name="alert" size={14} />{err}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---- 项目设置:成员(P7:可搜索人员选择器 + 专业角色/项目身份两列 + 显式身份调整)----
|
||||
|
||||
interface MemberRow {
|
||||
|
||||
@@ -9,159 +9,7 @@ import { Btn, EmptyState, Icon, SettingRow, SettingsGroup, UIRenderer, type DocB
|
||||
import { SettingInput, flatten, unflatten } from './SettingsModal'
|
||||
import type { SettingDesc } from '../contracts'
|
||||
import { docBridge } from './Workbench'
|
||||
import { randomPassword } from '../passwords'
|
||||
|
||||
// ---- 用户管理(原 AdminUsersModal 主体,R2 管理员发放制 + 2026-10-13 恢复 Key)----
|
||||
interface AdminUserRow { owner: string; name: string; isAdmin: boolean; mustChangePassword: boolean; hasPassword: boolean; passwordSetAt?: string; createdAt: string }
|
||||
interface LoginKeyRow { id: string; owner: string; purpose: string; status: string; expiresAt: string; createdAt: string; createdBy?: string }
|
||||
|
||||
/** Key 状态徽标:复用既有 chip 体系,不新增颜色。 */
|
||||
function keyStatusChip(status: string) {
|
||||
if (status === 'unused') return <span className="ad-chip ad-chip-warn">待使用</span>
|
||||
if (status === 'consumed') return <span className="ad-chip">已使用</span>
|
||||
if (status === 'expired') return <span className="ad-chip">已过期</span>
|
||||
return <span className="ad-chip">已撤销</span>
|
||||
}
|
||||
|
||||
export function UsersPanel() {
|
||||
const me = useStore(s => s.me)
|
||||
const [users, setUsers] = useState<AdminUserRow[] | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
const [notice, setNotice] = useState('')
|
||||
const [owner, setOwner] = useState('')
|
||||
const [name, setName] = useState('')
|
||||
const [pw, setPw] = useState('')
|
||||
const [asAdmin, setAsAdmin] = useState(false)
|
||||
const [busy, setBusy] = useState(false)
|
||||
// 登录 Key 面板:keyFor 打开指定用户的签发/状态视图;issued 明文仅签发响应持有一次。
|
||||
const [keyFor, setKeyFor] = useState<string | null>(null)
|
||||
const [issued, setIssued] = useState<{ key: string; expiresAt: string } | null>(null)
|
||||
const [keys, setKeys] = useState<LoginKeyRow[] | null>(null)
|
||||
|
||||
const refresh = async () => {
|
||||
try { setUsers(await api.listAdminUsers()) } catch (e) { setErr(String(e)) }
|
||||
}
|
||||
const refreshKeys = async (owner: string) => {
|
||||
try { setKeys(await api.adminListLoginKeys(owner)) } catch (e) { setErr(String(e)) }
|
||||
}
|
||||
useEffect(() => { refresh() }, [])
|
||||
|
||||
const openKeyPanel = async (owner: string) => {
|
||||
setErr(''); setNotice(''); setIssued(null); setKeyFor(owner); setKeys(null)
|
||||
await refreshKeys(owner)
|
||||
}
|
||||
|
||||
const create = async () => {
|
||||
setErr(''); setNotice('')
|
||||
if (pw.length < 8) { setErr('初始密码至少 8 位'); return }
|
||||
setBusy(true)
|
||||
try {
|
||||
await api.createAdminUser(owner.trim(), name.trim(), pw, asAdmin)
|
||||
setNotice(`已创建 ${owner.trim()}:首次登录须改密`)
|
||||
setOwner(''); setName(''); setPw('')
|
||||
await refresh()
|
||||
} catch (e) { setErr(String(e).includes('409') ? '用户名已存在' : String(e)) } finally { setBusy(false) }
|
||||
}
|
||||
|
||||
const issue = async () => {
|
||||
if (!keyFor) return
|
||||
setErr(''); setNotice(''); setBusy(true)
|
||||
try {
|
||||
const out = await api.adminIssueLoginKey(keyFor)
|
||||
setIssued({ key: out.key, expiresAt: out.expiresAt })
|
||||
await refreshKeys(keyFor)
|
||||
} catch (e) { setErr(String(e)) } finally { setBusy(false) }
|
||||
}
|
||||
|
||||
const copyKey = async () => {
|
||||
if (!issued) return
|
||||
try { await navigator.clipboard.writeText(issued.key); setNotice('已复制到剪贴板,请安全交付给该用户') } catch { setErr('复制失败,请手动选择复制') }
|
||||
}
|
||||
|
||||
const revoke = async (id: string) => {
|
||||
if (!keyFor) return
|
||||
setErr(''); setBusy(true)
|
||||
try {
|
||||
await api.adminRevokeLoginKey(keyFor, id)
|
||||
await refreshKeys(keyFor)
|
||||
} catch (e) { setErr(String(e)) } finally { setBusy(false) }
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{me?.isAdmin && (
|
||||
<div className="ad-create">
|
||||
<div className="ad-row">
|
||||
<input className="lg-input" placeholder="用户名(登录名)" value={owner} onChange={e => setOwner(e.target.value)} />
|
||||
<input className="lg-input" placeholder="显示名(可空)" value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
<div className="ad-row">
|
||||
<input className="lg-input lg-mono" placeholder="初始密码(≥8 位)" value={pw} onChange={e => setPw(e.target.value)} />
|
||||
<button type="button" className="ad-mini" title="生成随机密码" onClick={() => setPw(randomPassword())}>随机</button>
|
||||
<label className="ad-check"><input type="checkbox" checked={asAdmin} onChange={e => setAsAdmin(e.target.checked)} />管理员</label>
|
||||
<button className="lg-submit ad-create-btn" disabled={busy || !owner.trim() || !pw} onClick={create}>创建账号</button>
|
||||
</div>
|
||||
<p className="ad-hint">创建后该用户首次登录必须修改此初始密码;忘记密码时为其生成一次性登录 Key。</p>
|
||||
</div>
|
||||
)}
|
||||
{notice && <div className="ad-notice"><Icon name="check" size={14} />{notice}</div>}
|
||||
{err && <div className="lg-err"><Icon name="alert" size={14} />{err}</div>}
|
||||
{keyFor ? (
|
||||
<div className="ad-reset">
|
||||
<div className="ad-row">
|
||||
<span><b>{keyFor}</b> 的一次性登录 Key(忘记密码恢复)</span>
|
||||
<button className="lg-submit ad-create-btn" disabled={busy} onClick={issue}>{issued ? '重新签发(旧 Key 与受限会话立即失效)' : '生成 Key'}</button>
|
||||
<button type="button" className="ad-mini" onClick={() => { setKeyFor(null); setIssued(null); setKeys(null) }}>关闭</button>
|
||||
</div>
|
||||
{issued && (
|
||||
<div className="ad-row">
|
||||
<input className="lg-input lg-mono" readOnly value={issued.key} aria-label="一次性登录 Key(仅显示一次)" onFocus={e => e.currentTarget.select()} />
|
||||
<button type="button" className="ad-mini" onClick={copyKey}>复制</button>
|
||||
</div>
|
||||
)}
|
||||
{issued && <p className="ad-hint">明文仅此一次显示,关闭后不可再取;单次有效,{new Date(issued.expiresAt).toLocaleString()} 过期。请经安全渠道交付本人。</p>}
|
||||
{keys !== null && keys.length > 0 && (
|
||||
<table className="ad-table">
|
||||
<thead><tr><th>签发时间</th><th>用途</th><th>状态</th><th>过期时间</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{keys.map(k => (
|
||||
<tr key={k.id}>
|
||||
<td className="ad-dim">{new Date(k.createdAt).toLocaleString()}</td>
|
||||
<td>{k.purpose === 'recover' ? '恢复' : '激活'}</td>
|
||||
<td>{keyStatusChip(k.status)}</td>
|
||||
<td className="ad-dim">{new Date(k.expiresAt).toLocaleString()}</td>
|
||||
<td>{k.status === 'unused' && <button className="ad-mini" disabled={busy} onClick={() => revoke(k.id)}>撤销</button>}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<table className="ad-table">
|
||||
<thead><tr><th>用户名</th><th>显示名</th><th>角色</th><th>状态</th><th>密码设置于</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{(users ?? []).map(u => (
|
||||
<tr key={u.owner}>
|
||||
<td className="ad-user-cell"><span className="ad-mono">{u.owner}</span></td>
|
||||
<td>{u.name || '—'}</td>
|
||||
<td>{u.isAdmin ? <span className="ad-chip ad-chip-admin">ADMIN</span> : <span className="ad-chip">USER</span>}</td>
|
||||
<td>{u.mustChangePassword ? <span className="ad-chip ad-chip-warn">待改密</span> : u.hasPassword ? '正常' : '未设密码'}</td>
|
||||
<td className="ad-dim">{u.passwordSetAt ? new Date(u.passwordSetAt).toLocaleString() : '—'}</td>
|
||||
<td>{me?.isAdmin && (
|
||||
<button className="ad-mini" onClick={() => openKeyPanel(u.owner)}>生成登录 Key</button>
|
||||
)}</td>
|
||||
</tr>
|
||||
))}
|
||||
{users !== null && users.length === 0 && <tr><td colSpan={6} className="ad-dim">暂无账号</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---- 智能体画像(原 SettingsModal ProfilesSection:数据化导入导出 + 编辑)----
|
||||
export function ProfilesSection({ profiles }: { profiles: AgentProfile[] }) {
|
||||
const [editing, setEditing] = useState<AgentProfile | null>(null)
|
||||
const [text, setText] = useState('')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type RolePermissionValue = { allowed: boolean; capAllowed: boolean }
|
||||
export type RolePermissionChange = { actionId: string; allowed: boolean; capAllowed: boolean }
|
||||
type RolePermissionChange = { actionId: string; allowed: boolean; capAllowed: boolean }
|
||||
|
||||
export interface RolePermissionDraft {
|
||||
baseline: Record<string, RolePermissionValue>
|
||||
|
||||
Reference in New Issue
Block a user