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
- `PATCH /api/me` lets the signed-in user edit two fields of their own profile: `display_name` (string) and `weekly_digest` (boolean, the weekly summary email). The body is a JSON object holding any subset of those keys; a key that is absent or `null` leaves its field unchanged, and any other key in the body is ignored. A `null` body counts as an empty object. The response is 200 with the updated user, in the same shape `GET /api/me` returns.
- A body larger than 16 KiB, one that is not valid JSON, holds more than one JSON value, or gives one of the two fields the wrong JSON type is a 400 `{"error": "invalid_body"}`. A resulting `display_name` that is blank after trimming whitespace or longer than 80 Unicode code points is a 400 `{"error": "invalid_display_name"}`. Nothing is saved on a 400. Existing rows already satisfy the name rule. Request bodies reach the service as valid UTF-8: the API gateway rejects anything else.
- Every signed-in user can call the endpoint for their own account. `is_admin` is granted and revoked only by admins in the existing admin console, and `email` changes only through the existing change-email flow, which verifies the new address first. Neither may change through this endpoint.
- A store failure is a 500 `{"error": "internal"}`, logged with the user id; no internals reach the client. The settings page saves one form at a time, and concurrent edits of one user row are out of scope (last write wins).
Files touched
- internal/account/profile.go
- internal/account/routes.go
--- 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))