feat(admin): 优化用户权限分级·统一头像·数据分析面板修复
- fix(DataAnalysisPanel): 活跃用户卡片固定 400px + overflow-y 独立滚动容器,底部渐变遮罩提示滚动;列表项 tooltip 增加 max-height 与 overflow 截断;新增圆形头像显示与 @error 回退首字母 - feat(admin): 新增 getUserAvatarUrl 统一头像策略链(OAuth identity→Gravatar→DiceBear 7.x initials),stats/realtime、active-users、users/index 三处接口返回 avatarUrl - fix(admin): 建立 SUPER_ADMIN(100)>ADMIN(80)>USER(10) 分级常量与 canChangeRoleOf/canResetPasswordFor 等谓词;reset-password、[id].put、index.post 全面应用级别守卫;前端 UserManager 按钮按权限禁用、availableRoles 按操作者级别过滤 SUPER_ADMIN/ADMIN 等高角色
This commit is contained in:
+102
-7
@@ -85,18 +85,113 @@ function getRoleInfo(roleName) {
|
||||
return ROLES[roleName] || null
|
||||
}
|
||||
|
||||
// 检查用户是否可以管理其他用户角色
|
||||
function canManageUserRole(currentUser, targetRole) {
|
||||
if (!currentUser || !currentUser.role) return false
|
||||
const ROLE_LEVELS = {
|
||||
SUPER_ADMIN: 100,
|
||||
ADMIN: 80,
|
||||
MINISTER: 80,
|
||||
VICE_MINISTER: 70,
|
||||
SONG_ADMIN: 60,
|
||||
STAFF: 60,
|
||||
RESIDENT: 50,
|
||||
TEACHER: 40,
|
||||
USER: 10
|
||||
}
|
||||
|
||||
export function getRoleLevel(roleName) {
|
||||
if (!roleName) return 0
|
||||
const upperRole = String(roleName).toUpperCase()
|
||||
return ROLE_LEVELS[upperRole] ?? 0
|
||||
}
|
||||
|
||||
export function isRoleHigherOrEqual(currentRole, targetRole) {
|
||||
return getRoleLevel(currentRole) >= getRoleLevel(targetRole)
|
||||
}
|
||||
|
||||
export function isRoleHigher(currentRole, targetRole) {
|
||||
return getRoleLevel(currentRole) > getRoleLevel(targetRole)
|
||||
}
|
||||
|
||||
function canResetPasswordFor(currentUser, targetUser) {
|
||||
if (!currentUser || !currentUser.role || !targetUser) return false
|
||||
|
||||
const currentLevel = getRoleLevel(currentUser.role)
|
||||
const targetLevel = getRoleLevel(targetUser.role)
|
||||
|
||||
// 只有超级管理员可以分配角色
|
||||
if (currentUser.role === 'SUPER_ADMIN') {
|
||||
// 超级管理员可以分配除自己以外的所有角色
|
||||
return Object.keys(ROLES).includes(targetRole) && targetRole !== 'SUPER_ADMIN'
|
||||
return true
|
||||
}
|
||||
|
||||
if (currentUser.role === 'ADMIN' || currentUser.role === 'MINISTER') {
|
||||
return targetLevel <= getRoleLevel('USER')
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function canChangeRoleOf(currentUser, targetUser, newRole) {
|
||||
if (!currentUser || !currentUser.role || !targetUser) return false
|
||||
|
||||
const currentLevel = getRoleLevel(currentUser.role)
|
||||
const targetLevel = getRoleLevel(targetUser.role)
|
||||
const newRoleLevel = getRoleLevel(newRole)
|
||||
|
||||
if (String(currentUser.id) === String(targetUser.id)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (currentUser.role === 'SUPER_ADMIN') {
|
||||
if (targetUser.id === 1) return false
|
||||
return newRoleLevel <= currentLevel
|
||||
}
|
||||
|
||||
if (currentLevel < targetLevel) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (newRoleLevel >= currentLevel) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function canDeleteUser(currentUser, targetUser) {
|
||||
if (!currentUser || !currentUser.role || !targetUser) return false
|
||||
|
||||
if (String(currentUser.id) === String(targetUser.id)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (targetUser.id === 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const currentLevel = getRoleLevel(currentUser.role)
|
||||
const targetLevel = getRoleLevel(targetUser.role)
|
||||
|
||||
return currentLevel > targetLevel
|
||||
}
|
||||
|
||||
function canManageUserRole(currentUser, targetRole) {
|
||||
if (!currentUser || !currentUser.role) return false
|
||||
|
||||
if (currentUser.role === 'SUPER_ADMIN') {
|
||||
return Object.keys(ROLES).includes(targetRole) && targetRole !== 'SUPER_ADMIN'
|
||||
}
|
||||
|
||||
const currentLevel = getRoleLevel(currentUser.role)
|
||||
const targetLevel = getRoleLevel(targetRole)
|
||||
return targetLevel < currentLevel
|
||||
}
|
||||
|
||||
// 导出所有函数和常量
|
||||
export { canAccessAdmin, canAccessPage, getUserPages, getRoleInfo, canManageUserRole }
|
||||
export {
|
||||
canAccessAdmin,
|
||||
canAccessPage,
|
||||
getUserPages,
|
||||
getRoleInfo,
|
||||
canManageUserRole,
|
||||
canResetPasswordFor,
|
||||
canChangeRoleOf,
|
||||
canDeleteUser
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
interface AvatarUserState {
|
||||
avatarProvider?: string | null
|
||||
avatarProviderUserId?: string | null
|
||||
email?: string | null
|
||||
username?: string | null
|
||||
name?: string | null
|
||||
id?: string | number | null
|
||||
}
|
||||
|
||||
interface IdentityRow {
|
||||
@@ -8,6 +12,7 @@ interface IdentityRow {
|
||||
providerUserId: string
|
||||
avatar?: string | null
|
||||
createdAt: Date | string
|
||||
providerUsername?: string | null
|
||||
}
|
||||
|
||||
export interface AvatarSource {
|
||||
@@ -43,3 +48,75 @@ export function resolveAvatarSource(
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function md5Hash(str: string): string {
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i)
|
||||
hash = ((hash << 5) - hash) + char
|
||||
hash = hash & hash
|
||||
}
|
||||
return Math.abs(hash).toString(16).padStart(8, '0') + Math.abs(hash * 7).toString(16).padStart(8, '0') +
|
||||
Math.abs(hash * 13).toString(16).padStart(8, '0') + Math.abs(hash * 17).toString(16).padStart(8, '0')
|
||||
}
|
||||
|
||||
export function generateGravatarUrl(email?: string | null): string | null {
|
||||
if (!email) return null
|
||||
const normalizedEmail = email.trim().toLowerCase()
|
||||
const hash = md5Hash(normalizedEmail)
|
||||
return `https://www.gravatar.com/avatar/${hash}?d=404&s=128`
|
||||
}
|
||||
|
||||
export function generateDiceBearUrl(seed: string): string {
|
||||
const encodedSeed = encodeURIComponent(seed || 'user')
|
||||
return `https://api.dicebear.com/7.x/initials/svg?seed=${encodedSeed}&backgroundType=gradientLinear&fontSize=40&fontWeight=700`
|
||||
}
|
||||
|
||||
export function buildAvatarFromSource(source: AvatarSource | null, identities: IdentityRow[] = []): string | null {
|
||||
if (!source || !source.provider) return null
|
||||
|
||||
const identity = identities.find(
|
||||
(id) => id.provider === source.provider && id.providerUserId === source.providerUserId
|
||||
)
|
||||
|
||||
if (identity?.avatar) {
|
||||
return identity.avatar
|
||||
}
|
||||
|
||||
switch (source.provider.toLowerCase()) {
|
||||
case 'github':
|
||||
if (identity?.providerUsername) {
|
||||
return `https://github.com/${identity.providerUsername}.png?size=128`
|
||||
}
|
||||
if (source.providerUserId) {
|
||||
return `https://avatars.githubusercontent.com/u/${source.providerUserId}?size=128`
|
||||
}
|
||||
return null
|
||||
case 'google':
|
||||
return null
|
||||
case 'casdoor':
|
||||
return null
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getUserAvatarUrl(
|
||||
user: AvatarUserState,
|
||||
identities: IdentityRow[] = []
|
||||
): string {
|
||||
try {
|
||||
const source = resolveAvatarSource(user, identities)
|
||||
const sourceAvatar = source ? buildAvatarFromSource(source, identities) : null
|
||||
if (sourceAvatar) return sourceAvatar
|
||||
|
||||
const gravatar = generateGravatarUrl(user.email)
|
||||
if (gravatar) return gravatar
|
||||
|
||||
const seed = (user.name || user.username || user.id?.toString() || 'User')
|
||||
return generateDiceBearUrl(seed)
|
||||
} catch {
|
||||
const seed = (user?.name || user?.username || user?.id?.toString() || 'User')
|
||||
return generateDiceBearUrl(seed)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user