Enhance user profile management and Gravatar integration

- Added email field to user profile API and frontend components for better user identification.
- Implemented PATCH /map/api/me endpoint to update user email, enhancing user experience.
- Introduced useGravatarUrl composable for generating Gravatar URLs based on user email.
- Updated profile and layout components to display user avatars using Gravatar, improving visual consistency.
- Enhanced development documentation to guide testing of navbar and profile features.
This commit is contained in:
2026-03-01 16:48:56 +03:00
parent db0b48774a
commit 6a6977ddff
14 changed files with 311 additions and 28 deletions

View File

@@ -33,7 +33,14 @@ func (h *Handlers) APIRouter(rw http.ResponseWriter, req *http.Request) {
h.APILogout(rw, req)
return
case "me":
h.APIMe(rw, req)
switch req.Method {
case http.MethodGet:
h.APIMe(rw, req)
case http.MethodPatch:
h.APIMeUpdate(rw, req)
default:
JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
}
return
case "me/tokens":
h.APIMeTokens(rw, req)

View File

@@ -18,12 +18,17 @@ type meResponse struct {
Auths []string `json:"auths"`
Tokens []string `json:"tokens,omitempty"`
Prefix string `json:"prefix,omitempty"`
Email string `json:"email,omitempty"`
}
type passwordRequest struct {
Pass string `json:"pass"`
}
type meUpdateRequest struct {
Email string `json:"email"`
}
// APILogin handles POST /map/api/login.
func (h *Handlers) APILogin(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
@@ -105,9 +110,36 @@ func (h *Handlers) APIMe(rw http.ResponseWriter, req *http.Request) {
}
out := meResponse{Username: s.Username, Auths: s.Auths}
out.Tokens, out.Prefix = h.Auth.GetUserTokensAndPrefix(ctx, s.Username)
if u := h.Auth.GetUserByUsername(ctx, s.Username); u != nil {
out.Email = u.Email
}
JSON(rw, http.StatusOK, out)
}
// APIMeUpdate handles PATCH /map/api/me (update current user email).
func (h *Handlers) APIMeUpdate(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPatch {
JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
return
}
ctx := req.Context()
s := h.Auth.GetSession(ctx, req)
if s == nil {
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
return
}
var body meUpdateRequest
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST")
return
}
if err := h.Auth.SetUserEmail(ctx, s.Username, body.Email); err != nil {
JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR")
return
}
rw.WriteHeader(http.StatusOK)
}
// APIMeTokens handles POST /map/api/me/tokens.
func (h *Handlers) APIMeTokens(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {

View File

@@ -287,6 +287,51 @@ func TestAPIMePassword(t *testing.T) {
}
}
func TestAPIMeUpdate_UpdatesEmail(t *testing.T) {
env := newTestEnv(t)
env.createUser(t, "alice", "pass", app.Auths{app.AUTH_MAP})
sid := env.loginSession(t, "alice")
patchBody := `{"email":"test@example.com"}`
req := withSession(httptest.NewRequest(http.MethodPatch, "/map/api/me", strings.NewReader(patchBody)), sid)
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
env.h.APIMeUpdate(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
}
req2 := withSession(httptest.NewRequest(http.MethodGet, "/map/api/me", nil), sid)
rr2 := httptest.NewRecorder()
env.h.APIMe(rr2, req2)
if rr2.Code != http.StatusOK {
t.Fatalf("expected 200 on GET /me, got %d", rr2.Code)
}
var meResp struct {
Username string `json:"username"`
Email string `json:"email"`
}
if err := json.NewDecoder(rr2.Body).Decode(&meResp); err != nil {
t.Fatal(err)
}
if meResp.Email != "test@example.com" {
t.Fatalf("expected email test@example.com, got %q", meResp.Email)
}
}
func TestAPIRouter_Me_MethodNotAllowed(t *testing.T) {
env := newTestEnv(t)
env.createUser(t, "alice", "pass", app.Auths{app.AUTH_MAP})
sid := env.loginSession(t, "alice")
req := withSession(httptest.NewRequest(http.MethodPost, "/map/api/me", nil), sid)
rr := httptest.NewRecorder()
env.h.APIRouter(rr, req)
if rr.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected 405 for POST /me, got %d", rr.Code)
}
}
func TestAdminUsers_RequiresAdmin(t *testing.T) {
env := newTestEnv(t)
env.createUser(t, "alice", "pass", app.Auths{app.AUTH_MAP})