149 lines
4.1 KiB
Go
149 lines
4.1 KiB
Go
package web
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"plumber/internal/blob"
|
|
"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(), blob.Object{
|
|
Key: key,
|
|
Body: limited,
|
|
ContentType: contentType,
|
|
Size: 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,
|
|
})
|
|
}
|