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:
marcandClaude Opus 5 committed 2026-08-24 13:43:22 +02:00
1 parent 3019e84681
commit 9f44215698
18 files changed
+814 -26

No files matched your search

+13
View File
@@ -15,9 +15,22 @@ export interface User {
/** Konto meldet sich über den Identitätsanbieter an und hat hier kein
* Passwort – die Passwortänderung wird dann ausgeblendet. */
is_oidc?: boolean
/** Zeitstempel des Profilbildes in Sekunden; 0 oder fehlend = keins.
* Adresse dazu über avatarUrl(). */
avatar_ver?: number
created_at: string
}
/**
* Adresse des Profilbildes – oder null, wenn keines hinterlegt ist.
*
* Der Zeitstempel hängt als ?v= dran, damit der Browser das Bild lange behalten
* darf und es trotzdem sofort wechselt, sobald ein anderes hinterlegt wird.
*/
export function avatarUrl(user: Pick<User, 'id' | 'avatar_ver'>): string | null {
return user.avatar_ver ? `/api/v1/users/${user.id}/avatar?v=${user.avatar_ver}` : null
}
export interface FileItem {
id: string
/** Datum auf dem Beleg (YYYY-MM-DD) – nicht das des Uploads. */