interface AvatarUserState { avatarProvider?: string | null avatarProviderUserId?: string | null email?: string | null username?: string | null name?: string | null id?: string | number | null } interface IdentityRow { provider: string providerUserId: string avatar?: string | null createdAt: Date | string providerUsername?: string | null } export interface AvatarSource { provider: string | null providerUserId: string | null } export function resolveAvatarSource( user: AvatarUserState, identities: IdentityRow[] ): AvatarSource | null { if (user.avatarProvider) { return { provider: user.avatarProvider, providerUserId: user.avatarProviderUserId ?? null } } const sorted = [...identities].sort((a, b) => { const at = new Date(a.createdAt).getTime() const bt = new Date(b.createdAt).getTime() return at - bt }) for (const identity of sorted) { if (identity.avatar) { return { provider: identity.provider, providerUserId: identity.providerUserId } } } 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) } }