Add player profiles and community settings
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / compose (push) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-19 21:43:48 +03:00
parent 5e0825f43f
commit fa4edc0df5
23 changed files with 1280 additions and 57 deletions

View File

@@ -65,6 +65,9 @@ func New(service *application.Service, store application.Store, hub *realtime.Hu
api.Get("/api/me", s.me)
api.Patch("/api/me/player", s.updateProfile)
api.Get("/api/players", s.players)
api.Get("/api/players/{playerID}", s.playerProfile)
api.Get("/api/community/settings", s.communitySettings)
api.Put("/api/community/settings", s.updateCommunitySettings)
api.Get("/api/accounts", s.accounts)
api.Patch("/api/accounts/{accountID}/moderator", s.setModerator)
api.Get("/api/events", s.events)
@@ -246,7 +249,8 @@ func (s *Server) me(w http.ResponseWriter, r *http.Request) {
func (s *Server) updateProfile(w http.ResponseWriter, r *http.Request) {
var in struct {
DisplayName string `json:"displayName"`
DisplayName *string `json:"displayName"`
BattleTag *string `json:"battleTag"`
Ratings domain.Ratings `json:"ratings"`
PreferredRoles []domain.Role `json:"preferredRoles"`
PreferredPlayerIDs []string `json:"preferredPlayerIds"`
@@ -256,7 +260,10 @@ func (s *Server) updateProfile(w http.ResponseWriter, r *http.Request) {
return
}
id := who(r)
out, err := s.service.UpdateOwnProfile(r.Context(), id.account, id.player, in.DisplayName, in.Ratings, in.PreferredRoles, in.PreferredPlayerIDs, in.AvoidedPlayerIDs)
out, err := s.service.UpdateOwnProfile(r.Context(), id.account, id.player, application.UpdateOwnProfileInput{
DisplayName: in.DisplayName, BattleTag: in.BattleTag, Ratings: in.Ratings,
PreferredRoles: in.PreferredRoles, PreferredPlayerIDs: in.PreferredPlayerIDs, AvoidedPlayerIDs: in.AvoidedPlayerIDs,
})
respond(w, out, err, http.StatusOK)
}
@@ -265,6 +272,27 @@ func (s *Server) players(w http.ResponseWriter, r *http.Request) {
respond(w, out, err, 200)
}
func (s *Server) playerProfile(w http.ResponseWriter, r *http.Request) {
out, err := s.service.GetPlayerProfile(r.Context(), chi.URLParam(r, "playerID"))
respond(w, out, err, http.StatusOK)
}
func (s *Server) communitySettings(w http.ResponseWriter, r *http.Request) {
out, err := s.service.GetCommunitySettings(r.Context())
respond(w, out, err, http.StatusOK)
}
func (s *Server) updateCommunitySettings(w http.ResponseWriter, r *http.Request) {
var input struct {
DiscordInviteURL string `json:"discordInviteUrl"`
}
if !decode(w, r, &input) {
return
}
out, err := s.service.UpdateCommunitySettings(r.Context(), who(r).account, input.DiscordInviteURL)
respond(w, out, err, http.StatusOK)
}
func (s *Server) accounts(w http.ResponseWriter, r *http.Request) {
out, err := s.service.ListAccounts(r.Context(), who(r).account)
respond(w, out, err, http.StatusOK)

View File

@@ -1,6 +1,18 @@
package httpapi
import "testing"
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"mixmaker/backend/internal/application"
"mixmaker/backend/internal/domain"
"mixmaker/backend/internal/realtime"
)
func TestCreateEventRequestDefaultsEveryonePing(t *testing.T) {
if !((createEventRequest{}).options().PingEveryone) {
@@ -14,3 +26,67 @@ func TestCreateEventRequestCanDisableEveryonePing(t *testing.T) {
t.Fatal("explicit false pingEveryone must disable the ping")
}
}
type profileHTTPStore struct {
application.Store
account domain.Account
player domain.Player
}
func (s *profileHTTPStore) AccountBySession(context.Context, string) (domain.Account, domain.Player, error) {
return s.account, s.player, nil
}
func (s *profileHTTPStore) GetPlayer(context.Context, string) (domain.Player, error) {
return s.player, nil
}
func (s *profileHTTPStore) ListEvents(context.Context, time.Time) ([]domain.Event, error) {
return []domain.Event{}, nil
}
func TestPlayerProfileHTTPResponseIsPublicSafe(t *testing.T) {
store := &profileHTTPStore{
account: domain.Account{ID: "account", Role: domain.RolePlayer},
player: domain.Player{
ID: "player", AccountID: "account", DisplayName: "Ana", BattleTag: "Ana#1234",
Ratings: domain.Ratings{Tank: 10, Damage: 20, Support: 30},
PreferredPlayerIDs: []string{"secret-preference"}, AvoidedPlayerIDs: []string{"secret-avoid"},
},
}
hub := realtime.New()
handler := New(application.New(store, hub, nil), store, hub, Config{CookieName: "session"})
request := httptest.NewRequest(http.MethodGet, "/api/players/player", nil)
request.AddCookie(&http.Cookie{Name: "session", Value: "valid"})
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("unexpected status %d: %s", response.Code, response.Body.String())
}
var body map[string]any
if err := json.Unmarshal(response.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
encoded := response.Body.String()
if strings.Contains(encoded, "preferred") || strings.Contains(encoded, "avoid") || strings.Contains(encoded, "accountId") {
t.Fatalf("profile leaked private fields: %s", encoded)
}
}
func TestModeratorCannotUpdateCommunitySettings(t *testing.T) {
store := &profileHTTPStore{
account: domain.Account{ID: "moderator", Role: domain.RoleModerator},
player: domain.Player{ID: "player", AccountID: "moderator"},
}
hub := realtime.New()
handler := New(application.New(store, hub, nil), store, hub, Config{CookieName: "session"})
request := httptest.NewRequest(http.MethodPut, "/api/community/settings", strings.NewReader(`{"discordInviteUrl":""}`))
request.AddCookie(&http.Cookie{Name: "session", Value: "valid"})
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusForbidden {
t.Fatalf("moderator update returned %d: %s", response.Code, response.Body.String())
}
}