diff --git a/src/app/admin/create-user/role-manager.tsx b/src/app/admin/create-user/role-manager.tsx index 14e26a9..4526192 100644 --- a/src/app/admin/create-user/role-manager.tsx +++ b/src/app/admin/create-user/role-manager.tsx @@ -30,6 +30,9 @@ export default function RoleManager({ setResult }: Props) { const [editingEmail, setEditingEmail] = useState(null) const [editEmailValue, setEditEmailValue] = useState('') const [savingEmail, setSavingEmail] = useState(false) + const [editingDisplayName, setEditingDisplayName] = useState(null) + const [editDisplayNameValue, setEditDisplayNameValue] = useState('') + const [savingDisplayName, setSavingDisplayName] = useState(false) const fetchRoleData = useCallback(async () => { try { @@ -54,6 +57,24 @@ export default function RoleManager({ setResult }: Props) { finally { setSavingEmail(false) } } + async function handleEditDisplayName(target: string) { + if (!editDisplayNameValue.trim()) { setResult(false, '显示名不能为空'); return } + setSavingDisplayName(true) + try { + const res = await fetch('/api/admin/users', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: target, displayName: editDisplayNameValue.trim() }) }) + const d = await res.json() + if (res.ok) { + setRoleData(prev => prev ? { + ...prev, + assetsUsers: prev.assetsUsers.map(u => u.username === target ? { ...u, display_name: d.displayName } : u), + issueUsers: prev.issueUsers.map(u => u.username === target ? { ...u, display_name: d.displayName } : u), + } : null) + setEditingDisplayName(null); setResult(true, `${target} 显示名已更新`) + } else { setResult(false, d.error || '修改失败') } + } catch { setResult(false, '网络错误') } + finally { setSavingDisplayName(false) } + } + function handleSelect(site: string, username: string, newRole: string, originalRole: string) { if (newRole === originalRole) { const next = { ...pending }; delete next[`${site}:${username}`]; setPending(next) } else { setPending({ ...pending, [`${site}:${username}`]: { site, newRole } }) } @@ -107,7 +128,22 @@ export default function RoleManager({ setResult }: Props) { onMouseEnter={e => (e.currentTarget.style.background = 'var(--bg-hover)')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}> {uname} - {info.displayName} + + {editingDisplayName === uname ? ( + + setEditDisplayNameValue(e.target.value)} style={ss.emailInput} /> + + + + ) : ( + + {info.displayName} + {(uname !== 'admin' && uname !== 'localadmin') && ( + + )} + + )} + {editingEmail === uname ? ( diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts index 8d621ee..913b070 100644 --- a/src/app/api/admin/users/route.ts +++ b/src/app/api/admin/users/route.ts @@ -76,39 +76,56 @@ export async function DELETE(request: Request) { } } -// PATCH — 修改用户邮箱(admin 权限) +// PATCH — 修改用户信息(admin 权限) export async function PATCH(request: Request) { const isAdmin = await checkAdmin()() if (!isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) try { - const { username, email } = await request.json() + const { username, email, displayName } = await request.json() if (!username) return NextResponse.json({ error: '用户名不能为空' }, { status: 400 }) - if (email !== '' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + if (email === undefined && displayName === undefined) { + return NextResponse.json({ error: '至少需要 email 或 displayName' }, { status: 400 }) + } + if (email !== undefined && email !== '' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { return NextResponse.json({ error: '邮箱格式不合法' }, { status: 400 }) } const safeUser = username.replace(/'/g, "''") - const safeEmail = (email || '').replace(/'/g, "''") const d = new Date() const now = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}` - const updateSQL = `UPDATE users SET email = '${safeEmail}', lowercase_email = LOWER('${safeEmail}'), modified_date = '${now}' WHERE user_id = '${safeUser}';` + // 更新 LLDAP + let lldapSets: string[] = [] + let siteSets: string[] = [] + if (email !== undefined) { + const safeEmail = (email || '').replace(/'/g, "''") + lldapSets.push(`email = '${safeEmail}'`, `lowercase_email = LOWER('${safeEmail}')`) + siteSets.push(`email = '${safeEmail}'`) + } + if (displayName !== undefined) { + const safeName = displayName.replace(/'/g, "''") + lldapSets.push(`display_name = '${safeName}'`) + siteSets.push(`display_name = '${safeName}'`) + } + lldapSets.push(`modified_date = '${now}'`) + siteSets.push(`updated_at = datetime('now', '+8 hours')`) + + const lldapSQL = `UPDATE users SET ${lldapSets.join(', ')} WHERE user_id = '${safeUser}';` await execAsync( - `docker exec lldap /bin/sh -c "cat > /tmp/uea.sql <<'EOSQL'\n${updateSQL}\nEOSQL\nsqlite3 /data/users.db < /tmp/uea.sql"`, + `docker exec lldap /bin/sh -c "cat > /tmp/up.sql <<'EOSQL'\n${lldapSQL}\nEOSQL\nsqlite3 /data/users.db < /tmp/up.sql"`, { timeout: 5000 } ) - // 同步更新 assets / issue 本地用户表 + // 同步更新 assets / issue const assetsDb = process.env.ASSETS_DB_PATH || '/Users/niuniu/programs/docker/assets-ai/data/assets.db' const issueDb = process.env.ISSUE_DB_PATH || '/Users/niuniu/programs/docker/issue-ai/data/issue.db' + const siteSQL = `UPDATE users SET ${siteSets.join(', ')} WHERE username = '${safeUser}';` for (const dbPath of [assetsDb, issueDb]) { - try { - await execAsync(`sqlite3 "${dbPath}" "UPDATE users SET email = '${safeEmail}', updated_at = datetime('now', '+8 hours') WHERE username = '${safeUser}';"`, { timeout: 3000 }) - } catch {} + try { await execAsync(`sqlite3 "${dbPath}" "${siteSQL}"`, { timeout: 3000 }) } catch {} } - return NextResponse.json({ success: true, username, email: email || '' }) + return NextResponse.json({ success: true, username, email, displayName }) } catch (e) { return NextResponse.json({ error: '修改失败' }, { status: 500 }) }