Files
minstrel/internal/api/me_profile.go
T
bvandeusenandClaude Opus 5.5 08a2c06b85 docs: api comments say what the endpoints do, not how they came to be (#6277)
Rewrites the internal/api comment blocks to rule 32: task, milestone and
rule ids gone, the stories behind them restated as the constraint they
left, stale references corrected (the stream secret is wired from config,
the timezone handler already refreshes the scheduler, the Android app is
the native client, the design docs cited are not in the repo). Also
rewraps comment lines over 80 columns left by the earlier batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:23:58 -04:00

135 lines
3.7 KiB
Go

package api
import (
"encoding/json"
"errors"
"net/http"
"regexp"
"strings"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgx/v5/pgconn"
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)
// emailRe is a simple format check. We don't try to validate
// deliverability — the SMTP send itself is the real check.
var emailRe = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`)
type updateProfileReq struct {
// Pointer fields so the caller can omit-vs-clear distinctly.
// nil = leave unchanged; non-nil empty string = clear; non-nil
// non-empty = update to that value. Note: distinguishing
// omit-vs-clear at JSON-decode time requires custom unmarshaling
// or a sentinel; for v1 we use the simpler "empty string clears"
// semantics: any field present in the JSON body is applied.
DisplayName *string `json:"display_name"`
Email *string `json:"email"`
}
type meProfileResp struct {
ID string `json:"id"`
Username string `json:"username"`
DisplayName *string `json:"display_name"`
Email *string `json:"email"`
IsAdmin bool `json:"is_admin"`
// DebugModeEnabled is the account's diagnostics opt-in. The client
// reads it here to decide whether to run the on-device diagnostics
// reporter. Admin-set; the client also has a local OFF switch.
DebugModeEnabled bool `json:"debug_mode_enabled"`
}
func (h *handlers) handleUpdateMyProfile(w http.ResponseWriter, r *http.Request) {
user, ok := requireUser(w, r)
if !ok {
return
}
var req updateProfileReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, apierror.BadRequest("invalid_body", ""))
return
}
// Normalize: trim spaces; treat empty string as "clear" (NULL).
var displayName, email *string
if req.DisplayName != nil {
s := strings.TrimSpace(*req.DisplayName)
if s == "" {
displayName = nil
} else {
displayName = &s
}
}
if req.Email != nil {
s := strings.ToLower(strings.TrimSpace(*req.Email))
if s == "" {
email = nil
} else {
if !emailRe.MatchString(s) {
writeErr(w, apierror.BadRequest("email_invalid", ""))
return
}
email = &s
}
}
// If neither field was provided, no-op (just re-fetch and return).
q := dbq.New(h.pool)
if req.DisplayName == nil && req.Email == nil {
current, err := q.GetUserByID(r.Context(), user.ID)
if err != nil {
h.logger.Error("update profile: lookup failed", "err", err)
writeErr(w, apierror.Internal(err))
return
}
writeJSON(w, http.StatusOK, profileViewFromUser(current))
return
}
// To preserve existing values for fields not in the request, we need
// to fetch the current row and merge.
current, err := q.GetUserByID(r.Context(), user.ID)
if err != nil {
h.logger.Error("update profile: lookup failed", "err", err)
writeErr(w, apierror.Internal(err))
return
}
if req.DisplayName == nil {
displayName = current.DisplayName
}
if req.Email == nil {
email = current.Email
}
updated, err := q.UpdateUserProfile(r.Context(), dbq.UpdateUserProfileParams{
ID: user.ID,
DisplayName: displayName,
Email: email,
})
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UniqueViolation {
writeErr(w, apierror.Conflict("email_taken", ""))
return
}
h.logger.Error("update profile: update failed", "err", err)
writeErr(w, apierror.Internal(err))
return
}
writeJSON(w, http.StatusOK, profileViewFromUser(updated))
}
func profileViewFromUser(u dbq.User) meProfileResp {
return meProfileResp{
ID: uuidToString(u.ID),
Username: u.Username,
DisplayName: u.DisplayName,
Email: u.Email,
IsAdmin: u.IsAdmin,
DebugModeEnabled: u.DebugModeEnabled,
}
}