feat(oidc): Profilbild vom Identitätsanbieter übernehmen
Schickt der Anbieter ein `picture`, wird es bei der Anmeldung geholt,
mittig auf 256x256 zugeschnitten und oben rechts angezeigt. Fehlt es
oder scheitert das Holen, bleibt es beim Buchstabenkreis - die
Anmeldung selbst hängt nicht davon ab.
Die Bildadresse kommt aus einem Token, also von außen. Erlaubt ist
deshalb ausschließlich der Ursprung aus OIDC_ISSUER, Weiterleitungen
eingeschlossen; dazu 4 MB und 5 Sekunden als Deckel. Ohne das wäre es
eine Aufforderung an den Server, für einen Fremden beliebige Adressen
im internen Netz anzufragen.
Neu: Migration 000019 (avatar_key/-source/-updated_at), Paket
internal/avatar, Route GET /api/v1/users/{id}/avatar. Der GC durchsucht
"avatars/" mit und kennt die Schlüssel - sonst räumte er die Bilder
nach einem Tag weg.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3019e84681
commit
9f44215698
18 files changed
+814
-26
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { createContext, useContext, useEffect, useRef, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { avatarUrl } from '../api/client'
|
||||
import type { User } from '../api/client'
|
||||
import type { View } from '../hooks/useNavHistory'
|
||||
|
||||
@@ -94,7 +95,7 @@ export default function UserMenu() {
|
||||
onMouseEnter={e => { if (!open) e.currentTarget.style.background = 'var(--color-surface-2)' }}
|
||||
onMouseLeave={e => { if (!open) e.currentTarget.style.background = 'transparent' }}
|
||||
>
|
||||
<Avatar name={name} size={26} />
|
||||
<Avatar name={name} src={avatarUrl(user)} size={26} />
|
||||
<span style={{
|
||||
fontSize: 13, fontWeight: 500, color: 'var(--color-text)',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
@@ -157,16 +158,39 @@ export default function UserMenu() {
|
||||
)
|
||||
}
|
||||
|
||||
/** Der farbige Kreis mit dem ersten Buchstaben. */
|
||||
export function Avatar({ name, size = 26 }: { name: string; size?: number }) {
|
||||
/**
|
||||
* Das Profilbild – und solange keines da ist, der farbige Kreis mit dem ersten
|
||||
* Buchstaben.
|
||||
*
|
||||
* Der Buchstabe liegt **unter** dem Bild statt an seiner Stelle: Lädt das Bild
|
||||
* nicht (Anbieter offline, Bild inzwischen gelöscht), steht der Kreis schon da,
|
||||
* statt dass ein Loch in der Leiste bleibt. Deshalb auch kein Zustand für
|
||||
* „fehlgeschlagen" – ein `<img>`, das nichts anzeigt, gibt den Kreis von selbst
|
||||
* frei.
|
||||
*/
|
||||
export function Avatar({ name, src, size = 26 }: { name: string; src?: string | null; size?: number }) {
|
||||
return (
|
||||
<span style={{
|
||||
width: size, height: size, borderRadius: '50%',
|
||||
position: 'relative', width: size, height: size, borderRadius: '50%',
|
||||
background: 'var(--color-primary)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: Math.round(size * 0.45), fontWeight: 700, color: '#fff', flexShrink: 0,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{(name[0] ?? '?').toUpperCase()}
|
||||
{src && (
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
width={size}
|
||||
height={size}
|
||||
style={{
|
||||
position: 'absolute', inset: 0,
|
||||
width: '100%', height: '100%', objectFit: 'cover',
|
||||
}}
|
||||
onError={e => { e.currentTarget.style.display = 'none' }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user