119 lines
3.5 KiB
Go
119 lines
3.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"go-backend/internal/auth"
|
|
"go-backend/internal/services"
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
)
|
|
|
|
type UserResponse struct {
|
|
ID string `json:"id"`
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
// PatchUserHandler applies a partial update to the logged-in user. A field
|
|
// is only touched when its form key is present in the request body — an
|
|
// omitted key leaves that column unchanged, while an empty value is treated
|
|
// as an explicit (and rejected) attempt to blank out a required field.
|
|
func (h *Handler) PatchUserHandler(w http.ResponseWriter, r *http.Request) {
|
|
cookie, err := r.Cookie("session")
|
|
if err != nil {
|
|
http.Error(w, "Unauthorized, no session token", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
session, bump, err := h.auth.ValidateSessionToken(r.Context(), cookie.Value)
|
|
if err != nil {
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if session == nil {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if bump {
|
|
http.SetCookie(w, auth.NewSessionCookie(cookie.Value, false))
|
|
}
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
input := services.PatchUserInput{}
|
|
|
|
if r.PostForm.Has("username") {
|
|
v := r.PostFormValue("username")
|
|
if v == "" {
|
|
http.Error(w, "Username cannot be empty", http.StatusBadRequest)
|
|
return
|
|
}
|
|
input.Username = &v
|
|
}
|
|
|
|
if r.PostForm.Has("email") {
|
|
v := r.PostFormValue("email")
|
|
if v == "" {
|
|
http.Error(w, "Email cannot be empty", http.StatusBadRequest)
|
|
return
|
|
}
|
|
input.Email = &v
|
|
}
|
|
|
|
if r.PostForm.Has("current_password") {
|
|
v := r.PostFormValue("current_password")
|
|
input.CurrentPassword = &v
|
|
}
|
|
|
|
if r.PostForm.Has("new_password") {
|
|
v := r.PostFormValue("new_password")
|
|
if v == "" {
|
|
http.Error(w, "New password cannot be empty", http.StatusBadRequest)
|
|
return
|
|
}
|
|
input.NewPassword = &v
|
|
}
|
|
|
|
user, err := h.users.PatchUser(r.Context(), session.UserID, input)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, services.ErrCurrentPasswordRequired), errors.Is(err, services.ErrInvalidCurrentPassword):
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
default:
|
|
log.Printf("Error patching user: %v", err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
// The settings page targets the request's hx-target (a per-field error
|
|
// container) by default, so a plain success body would land there. Any
|
|
// field actually changed is instead pushed out-of-band to the element
|
|
// that displays its value, leaving the error container's swap empty.
|
|
data := patchUserSuccessData{PasswordChanged: input.NewPassword != nil}
|
|
if input.Username != nil {
|
|
data.Username = &user.Username
|
|
}
|
|
if input.Email != nil {
|
|
data.Email = &user.Email
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
patchUserSuccessTmpl.Execute(w, data)
|
|
}
|
|
|
|
type patchUserSuccessData struct {
|
|
Username *string
|
|
Email *string
|
|
PasswordChanged bool
|
|
}
|
|
|
|
var patchUserSuccessTmpl = template.Must(template.New("patchUserSuccess").Parse(`` +
|
|
`{{if .Username}}<p id="username-display" x-show="!editingUsername" hx-swap-oob="true" style="margin: 0"><strong>Username:</strong> {{.Username}}</p>{{end}}` +
|
|
`{{if .Email}}<p id="email-display" x-show="!editingEmail" hx-swap-oob="true" style="margin: 0"><strong>Email:</strong> {{.Email}}</p>{{end}}` +
|
|
`{{if .PasswordChanged}}<span style="color: green">Password updated.</span>{{end}}`,
|
|
))
|