Profilbilder: Avatar-Upload, Anzeige auf Profil & Feed

- user.avatar-Spalte (Schema, migrate.sh, Migrationsdoku)
- POST /user/avatar (geschützt): Upload via storeImage, ersetzt/löscht altes Bild
- avatar in /u/{name}/info, /user/info und im entrySelect-Join (Feed-Karten)
- Account-Löschung entfernt auch das Avatar-File
- Frontend: Avatar im Profilkopf + Byline der Karten (50px), Upload-Form,
  Platzhalterbild no_profile_pic.jpg als Fallback
- .card img per :not(.avatar) von Beitragsbildern getrennt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 18:20:14 +02:00
parent cad1fa5c5c
commit 15ef7db5f7
11 changed files with 153 additions and 20 deletions
+47 -5
View File
@@ -56,7 +56,8 @@ func handleUserPage(w http.ResponseWriter, r *http.Request) {
username := chi.URLParam(r, "username")
var uid, createdAt int64
err := db.QueryRow(`SELECT uid, created_at FROM user WHERE username = ?`, username).Scan(&uid, &createdAt)
var avatar string
err := db.QueryRow(`SELECT uid, created_at, avatar FROM user WHERE username = ?`, username).Scan(&uid, &createdAt, &avatar)
if err != nil {
writeError(w, http.StatusNotFound, "Nutzer nicht gefunden")
return
@@ -65,6 +66,7 @@ func handleUserPage(w http.ResponseWriter, r *http.Request) {
"uid": uid,
"username": username,
"created_at": createdAt,
"avatar": avatar,
})
}
@@ -72,10 +74,10 @@ func handleUserInfo(w http.ResponseWriter, r *http.Request) {
uid := uidFromContext(r.Context())
var createdAt, lastLogin sql.NullInt64
var username string
var username, avatar string
err := db.QueryRow(
`SELECT created_at, last_login, username FROM user WHERE uid = ?`, uid,
).Scan(&createdAt, &lastLogin, &username)
`SELECT created_at, last_login, username, avatar FROM user WHERE uid = ?`, uid,
).Scan(&createdAt, &lastLogin, &username, &avatar)
if err != nil {
writeError(w, http.StatusNotFound, "Nutzer nicht gefunden")
return
@@ -85,6 +87,7 @@ func handleUserInfo(w http.ResponseWriter, r *http.Request) {
"username": username,
"created_at": createdAt.Int64,
"last_login": lastLogin.Int64,
"avatar": avatar,
})
}
@@ -120,6 +123,40 @@ func handleUserRename(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"username": name})
}
// handleSetAvatar speichert ein hochgeladenes Profilbild des eingeloggten
// Nutzers (über storeImage skaliert) und ersetzt ein eventuell vorhandenes.
func handleSetAvatar(w http.ResponseWriter, r *http.Request) {
uid := uidFromContext(r.Context())
if err := r.ParseMultipartForm(16 << 20); err != nil {
writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
return
}
if r.MultipartForm == nil || len(r.MultipartForm.File["avatar"]) == 0 {
writeError(w, http.StatusBadRequest, "Kein Bild hochgeladen.")
return
}
stored, err := storeImage(r.MultipartForm.File["avatar"][0])
if err != nil {
writeError(w, http.StatusBadRequest, "Bild konnte nicht verarbeitet werden.")
return
}
var old string
db.QueryRow(`SELECT avatar FROM user WHERE uid = ?`, uid).Scan(&old)
if _, err := db.Exec(`UPDATE user SET avatar = ? WHERE uid = ?`, stored, uid); err != nil {
os.Remove(stored)
writeError(w, http.StatusInternalServerError, "Speichern fehlgeschlagen")
return
}
if old != "" && old != stored {
os.Remove(old)
}
writeJSON(w, http.StatusOK, map[string]string{"avatar": stored})
}
// handleUserDelete löscht den eingeloggten Account dauerhaft, nach Bestätigung
// durch das Passwort (Formularfeld pass1). Sessions und Votes werden in einer
// Transaktion mitgelöscht; die eigenen Beiträge werden soft-gelöscht (als
@@ -139,7 +176,8 @@ func handleUserDelete(w http.ResponseWriter, r *http.Request) {
return
}
// Mediendateien der Beiträge einsammeln, bevor die Pfade geleert werden.
// Mediendateien der Beiträge (und das Profilbild) einsammeln, bevor die
// Pfade geleert bzw. die user-Zeile gelöscht wird.
var media []string
if rows, err := db.Query(`SELECT filepath FROM entry WHERE uid = ?`, uid); err == nil {
for rows.Next() {
@@ -150,6 +188,10 @@ func handleUserDelete(w http.ResponseWriter, r *http.Request) {
}
rows.Close()
}
var avatar string
if db.QueryRow(`SELECT avatar FROM user WHERE uid = ?`, uid).Scan(&avatar) == nil && avatar != "" {
media = append(media, avatar)
}
tx, err := db.Begin()
if err != nil {