Files
plumber/internal/web/profile.go
T

143 lines
4.0 KiB
Go

package web
import (
"io"
"net/http"
"path"
"strings"
"github.com/google/uuid"
"plumber/internal/geo"
"plumber/internal/store"
)
type profilePage struct {
page
States []struct{ Code, Name string }
Questions []store.RankedQuestion
QuestionsLabel string
UploadsEnabled bool
Error string
StateVal string
}
func (s *Server) handleProfileForm(w http.ResponseWriter, r *http.Request) {
u := currentUser(r)
if u == nil {
http.Redirect(w, r, "/login?next=/profile", http.StatusSeeOther)
return
}
s.renderProfile(w, r, u, "", u.State)
}
func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) {
u := currentUser(r)
if u == nil {
http.Redirect(w, r, "/login?next=/profile", http.StatusSeeOther)
return
}
if err := r.ParseMultipartForm(3 << 20); err != nil {
s.renderProfile(w, r, u, "Could not read form (max 2MB for images).", u.State)
return
}
want := s.sessions.GetString(r.Context(), "csrf")
got := r.FormValue("_csrf")
if want == "" || got != want {
http.Error(w, "invalid csrf token", http.StatusForbidden)
return
}
state := geo.NormalizeState(r.FormValue("state"))
if !geo.ValidState(state) {
s.renderProfile(w, r, u, "Choose a valid US state or leave it blank.", state)
return
}
avatarURL := ""
file, hdr, err := r.FormFile("avatar")
if err == nil {
defer file.Close()
if !s.cfg.Blob.Enabled() {
s.renderProfile(w, r, u, "Avatar uploads are not configured on this server.", state)
return
}
ct := hdr.Header.Get("Content-Type")
ext, contentType, ok := avatarType(hdr.Filename, ct)
if !ok {
s.renderProfile(w, r, u, "Avatar must be a JPEG, PNG, or WebP image.", state)
return
}
if hdr.Size > 2<<20 {
s.renderProfile(w, r, u, "Avatar must be 2MB or smaller.", state)
return
}
key := path.Join("avatars", u.ID, uuid.NewString()+ext)
limited := io.LimitReader(file, (2<<20)+1)
url, upErr := s.cfg.Blob.Upload(r.Context(), key, limited, contentType, hdr.Size)
if upErr != nil {
s.renderProfile(w, r, u, "Could not upload avatar. Try again later.", state)
return
}
avatarURL = url
} else if err != http.ErrMissingFile {
s.renderProfile(w, r, u, "Could not read avatar file.", state)
return
}
if err := s.store.UpdateProfile(r.Context(), u.ID, state, avatarURL); err != nil {
http.Error(w, "could not save profile", http.StatusInternalServerError)
return
}
s.sessions.Put(r.Context(), "flash", "Profile saved.")
http.Redirect(w, r, "/profile", http.StatusSeeOther)
}
func avatarType(filename, contentType string) (ext, normalized string, ok bool) {
contentType = strings.ToLower(strings.TrimSpace(contentType))
filename = strings.ToLower(filename)
switch {
case strings.HasPrefix(contentType, "image/jpeg"), strings.HasSuffix(filename, ".jpg"), strings.HasSuffix(filename, ".jpeg"):
return ".jpg", "image/jpeg", true
case strings.HasPrefix(contentType, "image/png"), strings.HasSuffix(filename, ".png"):
return ".png", "image/png", true
case strings.HasPrefix(contentType, "image/webp"), strings.HasSuffix(filename, ".webp"):
return ".webp", "image/webp", true
default:
return "", "", false
}
}
func (s *Server) renderProfile(w http.ResponseWriter, r *http.Request, u *store.User, errMsg, stateVal string) {
var (
questions []store.RankedQuestion
label string
err error
)
if u.Admin() {
label = "Questions you answered"
questions, err = s.store.ListQuestionsAnsweredBy(r.Context(), u.ID)
} else {
label = "Your questions"
questions, err = s.store.ListQuestionsByAuthor(r.Context(), u.ID)
}
if err != nil {
http.Error(w, "could not load questions", http.StatusInternalServerError)
return
}
if fresh, e := s.store.UserByID(r.Context(), u.ID); e == nil {
u = fresh
}
p := s.basePage(r, "Profile")
p.User = u
s.exec(w, "profile", profilePage{
page: p,
States: geo.States,
Questions: questions,
QuestionsLabel: label,
UploadsEnabled: s.cfg.Blob.Enabled(),
Error: errMsg,
StateVal: stateVal,
})
}