Initial commit: runnable Ask a Plumber First server.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user