feat(profil): eigenes Profilbild hochladen

Jeder setzt sein Bild unter "Mein Konto", Vorgesetzte und Administratoren
auch fuer die Benutzer, die sie verwalten. Wo bisher die Initialen standen,
steht jetzt das Bild - Seitenleiste, Kopfzeile, Team, Dashboard, Reports,
Projekte, Kunden und die Buchungsliste. Ohne Bild bleiben die Initialen.

Der Browser verkleinert das Bild vor dem Hochladen auf ein Quadrat von
256 px und schneidet mittig zu (lib/imageCompress.ts): aus einem 4-MB-Foto
werden wenige Kilobyte, und der Server muss kein Bild verarbeiten.

Gespeichert wird es als Data-URL in pulse_users.avatar (LONGTEXT). Kein
Upload-Verzeichnis, das gesichert und beim Deployment am Leben gehalten
werden muesste.

Geprueft wird serverseitig in lib/avatar.js: nur PNG, JPEG, GIF, WebP und
AVIF als Data-URL, hoechstens 2 MB. SVG ist bewusst nicht erlaubt, es kann
Skripte tragen. Externe http(s)-Adressen ebenso wenig - sonst laedt jeder
Seitenaufruf ein Bild von einem fremden Server und verraet, wer wann mit
Pulse arbeitet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-19 23:06:02 +02:00
co-authored by Claude Opus 5
parent 2d3d843988
commit 9576d18abc
32 changed files with 397 additions and 44 deletions
+2 -1
View File
@@ -25,7 +25,7 @@ export function verifyShort(token) {
}
export const USER_COLUMNS = `id, username, email, name, role, manager_id, job_title, is_external, company,
source, external_id, weekly_hours, color, theme, theme_mode, is_active, last_login_at, created_at, updated_at`;
source, external_id, weekly_hours, color, avatar, theme, theme_mode, is_active, last_login_at, created_at, updated_at`;
/**
* Prueft das Bearer-Token und laedt den Benutzer bei jeder Anfrage frisch aus
@@ -84,6 +84,7 @@ export function publicUser(row) {
source: row.source,
weekly_hours: row.weekly_hours == null ? null : Number(row.weekly_hours),
color: row.color ?? null,
avatar: row.avatar ?? null,
theme: row.theme ?? null,
theme_mode: row.theme_mode ?? null,
is_active: Boolean(row.is_active),