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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user