fix: /api/auth/me 从 LLDAP 实时读取 displayName(非 JWT 缓存)

- 修改显示名后右上角个人信息即时更新
- RoleManager 支持 onUserUpdated 回调刷新用户信息
This commit is contained in:
aiyimickey 2026-05-18 18:14:17 +08:00
parent 868c79891f
commit ae1e58a595
3 changed files with 12 additions and 11 deletions

View File

@ -264,7 +264,7 @@ export default function AdminUsersPage() {
)} )}
{/* ====== 权限管理 ====== */} {/* ====== 权限管理 ====== */}
{tab === 'roles' && <RoleManager setResult={showResult} />} {tab === 'roles' && <RoleManager setResult={showResult} onUserUpdated={fetchLoginUser} />}
</div> </div>
</div> </div>
</div> </div>

View File

@ -9,7 +9,7 @@ interface RoleData {
emails: Record<string, string> emails: Record<string, string>
} }
interface Props { setResult: (ok: boolean, msg: string) => void } interface Props { setResult: (ok: boolean, msg: string) => void; onUserUpdated?: () => void }
const ss = { const ss = {
bar: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 } as React.CSSProperties, bar: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 } as React.CSSProperties,
@ -23,7 +23,7 @@ const ss = {
miniBtn: (primary: boolean) => ({ padding: '4px 12px', borderRadius: 5, border: primary ? 'none' : '1px solid var(--border)', background: primary ? '#2563eb' : 'var(--bg-card)', color: primary ? '#fff' : 'var(--text-secondary)', fontSize: 11, cursor: 'pointer', fontWeight: 500 } as React.CSSProperties), miniBtn: (primary: boolean) => ({ padding: '4px 12px', borderRadius: 5, border: primary ? 'none' : '1px solid var(--border)', background: primary ? '#2563eb' : 'var(--bg-card)', color: primary ? '#fff' : 'var(--text-secondary)', fontSize: 11, cursor: 'pointer', fontWeight: 500 } as React.CSSProperties),
} }
export default function RoleManager({ setResult }: Props) { export default function RoleManager({ setResult, onUserUpdated }: Props) {
const [roleData, setRoleData] = useState<RoleData | null>(null) const [roleData, setRoleData] = useState<RoleData | null>(null)
const [pending, setPending] = useState<Record<string, { site: string; newRole: string }>>({}) const [pending, setPending] = useState<Record<string, { site: string; newRole: string }>>({})
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
@ -69,7 +69,7 @@ export default function RoleManager({ setResult }: Props) {
assetsUsers: prev.assetsUsers.map(u => u.username === target ? { ...u, display_name: d.displayName } : u), 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), issueUsers: prev.issueUsers.map(u => u.username === target ? { ...u, display_name: d.displayName } : u),
} : null) } : null)
setEditingDisplayName(null); setResult(true, `${target} 显示名已更新`) setEditingDisplayName(null); setResult(true, `${target} 显示名已更新`); onUserUpdated?.()
} else { setResult(false, d.error || '修改失败') } } else { setResult(false, d.error || '修改失败') }
} catch { setResult(false, '网络错误') } } catch { setResult(false, '网络错误') }
finally { setSavingDisplayName(false) } finally { setSavingDisplayName(false) }

View File

@ -7,15 +7,16 @@ import { isLldapAdmin } from '@/lib/ldap'
const execAsync = promisify(exec) const execAsync = promisify(exec)
async function getLldapEmail(username: string): Promise<string> { async function getLldapInfo(username: string): Promise<{ email: string; displayName: string }> {
try { try {
const safeUser = username.replace(/'/g, "''") const safeUser = username.replace(/'/g, "''")
const { stdout } = await execAsync( const { stdout } = await execAsync(
`docker exec lldap /bin/sh -c "echo 'SELECT email FROM users WHERE user_id='\\''${safeUser}'\\'';' | sqlite3 /data/users.db"`, `docker exec lldap /bin/sh -c "echo 'SELECT email, display_name FROM users WHERE user_id='\\''${safeUser}'\\'';' | sqlite3 /data/users.db"`,
{ timeout: 3000 } { timeout: 3000 }
) )
return stdout.trim() || '' const parts = stdout.trim().split('|')
} catch { return '' } return { email: parts[0] || '', displayName: parts[1] || username }
} catch { return { email: '', displayName: username } }
} }
export async function GET() { export async function GET() {
@ -27,13 +28,13 @@ export async function GET() {
const payload = verifySharedJwt(token) const payload = verifySharedJwt(token)
if (!payload) return NextResponse.json({ error: '会话已过期' }, { status: 401 }) if (!payload) return NextResponse.json({ error: '会话已过期' }, { status: 401 })
const [admin, email] = await Promise.all([ const [admin, info] = await Promise.all([
isLldapAdmin(payload.username), isLldapAdmin(payload.username),
getLldapEmail(payload.username), getLldapInfo(payload.username),
]) ])
return NextResponse.json({ return NextResponse.json({
user: { username: payload.username, displayName: payload.displayName, email, isAdmin: admin }, user: { username: payload.username, displayName: info.displayName, email: info.email, isAdmin: admin },
}) })
} catch { } catch {
return NextResponse.json({ error: '获取用户信息失败' }, { status: 500 }) return NextResponse.json({ error: '获取用户信息失败' }, { status: 500 })