Edit your profile from account settings

Go · Go · intermediate · modification

Adds profile editing to the account settings page: the display name and the weekly digest toggle. `PATCH /api/me` updates the stored profile from the fields the page sends, keys the page leaves out keep their stored values, and the response is the same `User` JSON that `GET /api/me` already returns. The body is capped at 16 KiB and trailing data is rejected; the resulting name is validated before it is saved through the existing `UserStore.Save`.

Workspace API on Go 1.22 and the standard library (`net/http` method-pattern `ServeMux`, `encoding/json`, `log/slog`). `main` mounts the mux behind the auth middleware, which requires a session for every `/api/` request and checks the CSRF token on unsafe methods; `auth.UserID(ctx)` returns the signed-in user's id and is always set inside `/api/` handlers. Package `account` already has `User` (`ID`, `Email`, `DisplayName`, `WeeklyDigest`, `IsAdmin`, `CreatedAt`, with the json tags `id`, `email`, `display_name`, `weekly_digest`, `is_admin`, `created_at`; `GET /api/me` returns it so the web app knows whether to show the admin link), the `Handler` with `Users` and `Log`, the `Me` handler, and `writeJSON` / `writeError(w, status, code)`. `UserStore.Get(ctx, id) (User, error)` loads a user by id and returns it by value; `UserStore.Save(ctx, u)` updates the `email`, `display_name`, `weekly_digest` and `is_admin` columns of the row whose id is `u.ID`, and is also what the admin console calls after setting `IsAdmin`. The admin console and its API authorise a user by the `is_admin` column, read on every request. Request keys are matched as `encoding/json` matches struct tags (case-insensitively); the settings page sends them in lowercase.

Requirements

Files touched

--- internal/account/profile.go
+package account
+
+import (
+	"encoding/json"
+	"io"
+	"net/http"
+	"strings"
+	"unicode/utf8"
+
+	"example.com/workspace/internal/auth"
+)
+
+const (
+	maxProfileBody     = 16 << 10
+	maxDisplayNameRune = 80
+)
+
+// UpdateMe handles PATCH /api/me. The body is decoded onto the stored user,
+// so any key the client leaves out keeps its current value.
+func (h *Handler) UpdateMe(w http.ResponseWriter, r *http.Request) {
+	userID := auth.UserID(r.Context())
+	u, err := h.Users.Get(r.Context(), userID)
+	if err != nil {
+		h.Log.Error("load user for profile update", "user_id", userID, "err", err)
+		writeError(w, http.StatusInternalServerError, "internal")
+		return
+	}
+
+	dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxProfileBody))

Review this PR

Go practice