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>
135 lines
3.7 KiB
Go
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,
|
|
}
|
|
}
|