Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/cmd/map/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type HandlersMap struct {
ServiceName string
DB *gorm.DB
RedisCache *cache.RedisManager
feeds *respCache
Teams *teams.TeamManager
Users *users.UserManager
Challenges *challenges.ChallengeManager
Expand Down
140 changes: 67 additions & 73 deletions backend/cmd/map/handlers/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,20 @@ func (h *HandlersMap) JSONActivityHandler(w http.ResponseWriter, r *http.Request
if !ok {
return
}
// Get all activity logs for the given UUID
activityLogs, err := h.Logs.AllActivity(uuid)
if err != nil {
log.Err(err).Msg(h.T(r.Context())("feed.error_activity"))
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: h.T(r.Context())("feed.error_activity")})
return
}
visibleActivityLogs := make([]logs.ActivityLog, 0, len(activityLogs))
for _, activityLog := range activityLogs {
if activityLog.Visible {
visibleActivityLogs = append(visibleActivityLogs, activityLog)
h.serveCachedJSON(w, feedKey("activity", uuid), func() (int, []byte) {
activityLogs, err := h.Logs.AllActivity(uuid)
if err != nil {
log.Err(err).Msg(h.T(r.Context())("feed.error_activity"))
return http.StatusInternalServerError, marshalJSON(MapErrorResponse{Error: h.T(r.Context())("feed.error_activity")})
}
}
// Send response
HTTPResponse(w, JSONApplicationUTF8, http.StatusOK, visibleActivityLogs)
visibleActivityLogs := make([]logs.ActivityLog, 0, len(activityLogs))
for _, activityLog := range activityLogs {
if activityLog.Visible {
visibleActivityLogs = append(visibleActivityLogs, activityLog)
}
}
return http.StatusOK, marshalJSON(visibleActivityLogs)
})
}

// JSONGameClockHandler returns server-authoritative game clock state.
Expand Down Expand Up @@ -184,52 +183,49 @@ func (h *HandlersMap) JSONTeamsHandler(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
// Get all teams for the given UUID
allTeams, err := h.Teams.GetAll(uuid)
if err != nil {
log.Err(err).Msg(h.T(r.Context())("feed.error_teams"))
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: h.T(r.Context())("feed.error_teams")})
return
}

showTeamMembers, err := h.Settings.GetGameboardShowTeamMembers(uuid)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
log.Err(err).Msg("error retrieving gameboard_show_team_members setting")
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: h.T(r.Context())("feed.error_team_settings")})
return
}

membersByTeamID := make(map[uint][]string)
if showTeamMembers {
allUsers, err := h.Users.GetAll(uuid)
h.serveCachedJSON(w, feedKey("teams", uuid), func() (int, []byte) {
allTeams, err := h.Teams.GetAll(uuid)
if err != nil {
log.Err(err).Msg("error retrieving users for teams JSON")
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: h.T(r.Context())("feed.error_team_members")})
return
log.Err(err).Msg(h.T(r.Context())("feed.error_teams"))
return http.StatusInternalServerError, marshalJSON(MapErrorResponse{Error: h.T(r.Context())("feed.error_teams")})
}
for _, user := range allUsers {
if user.TeamID == 0 || !user.Active || user.Service {
continue

showTeamMembers, err := h.Settings.GetGameboardShowTeamMembers(uuid)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
log.Err(err).Msg("error retrieving gameboard_show_team_members setting")
return http.StatusInternalServerError, marshalJSON(MapErrorResponse{Error: h.T(r.Context())("feed.error_team_settings")})
}

membersByTeamID := make(map[uint][]string)
if showTeamMembers {
allUsers, err := h.Users.GetAll(uuid)
if err != nil {
log.Err(err).Msg("error retrieving users for teams JSON")
return http.StatusInternalServerError, marshalJSON(MapErrorResponse{Error: h.T(r.Context())("feed.error_team_members")})
}
for _, user := range allUsers {
if user.TeamID == 0 || !user.Active || user.Service {
continue
}
membersByTeamID[user.TeamID] = append(membersByTeamID[user.TeamID], user.Username)
}
membersByTeamID[user.TeamID] = append(membersByTeamID[user.TeamID], user.Username)
}
}

filteredTeams := make([]JSONTeamResponse, 0, len(allTeams))
for _, team := range allTeams {
if !team.Active || !team.Visible {
continue
filteredTeams := make([]JSONTeamResponse, 0, len(allTeams))
for _, team := range allTeams {
if !team.Active || !team.Visible {
continue
}
filteredTeams = append(filteredTeams, JSONTeamResponse{
Name: team.Name,
Logo: team.Logo,
Points: team.Points,
LastScore: teamLastScorePtr(team.LastScore),
TeamMembers: membersByTeamID[team.ID],
})
}
filteredTeams = append(filteredTeams, JSONTeamResponse{
Name: team.Name,
Logo: team.Logo,
Points: team.Points,
LastScore: teamLastScorePtr(team.LastScore),
TeamMembers: membersByTeamID[team.ID],
})
}
// Send response
HTTPResponse(w, JSONApplicationUTF8, http.StatusOK, filteredTeams)
return http.StatusOK, marshalJSON(filteredTeams)
})
}

func teamLastScorePtr(lastScore time.Time) *time.Time {
Expand All @@ -249,15 +245,14 @@ func (h *HandlersMap) JSONChallengesHandler(w http.ResponseWriter, r *http.Reque
if !ok {
return
}
// Get all active challenges for the given UUID
challenges, err := h.Challenges.GetActive(uuid)
if err != nil {
log.Err(err).Msg(h.T(r.Context())("feed.error_challenges"))
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: h.T(r.Context())("feed.error_challenges")})
return
}
// Send response
HTTPResponse(w, JSONApplicationUTF8, http.StatusOK, challenges)
h.serveCachedJSON(w, feedKey("challenges", uuid), func() (int, []byte) {
challenges, err := h.Challenges.GetActive(uuid)
if err != nil {
log.Err(err).Msg(h.T(r.Context())("feed.error_challenges"))
return http.StatusInternalServerError, marshalJSON(MapErrorResponse{Error: h.T(r.Context())("feed.error_challenges")})
}
return http.StatusOK, marshalJSON(challenges)
})
}

// JSONCountriesHandler returns live gameboard country data for all countries,
Expand Down Expand Up @@ -545,17 +540,16 @@ func (h *HandlersMap) JSONChatHandler(w http.ResponseWriter, r *http.Request) {
if h.Config.DebugHTTP.Enabled {
DebugHTTPDump(h.DebugHTTP, r, h.Config.DebugHTTP.ShowBody)
}
_, ok := h.validatedJSONUUID(w, r)
uuid, ok := h.validatedJSONUUID(w, r)
if !ok {
return
}
// Get all chat entries for the given UUID
chatEntries, err := h.Chat.GetVisible()
if err != nil {
log.Err(err).Msg(h.T(r.Context())("feed.error_chat"))
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: h.T(r.Context())("feed.error_chat")})
return
}
// Send response
HTTPResponse(w, JSONApplicationUTF8, http.StatusOK, chatEntries)
h.serveCachedJSON(w, feedKey("chat", uuid), func() (int, []byte) {
chatEntries, err := h.Chat.GetVisible()
if err != nil {
log.Err(err).Msg(h.T(r.Context())("feed.error_chat"))
return http.StatusInternalServerError, marshalJSON(MapErrorResponse{Error: h.T(r.Context())("feed.error_chat")})
}
return http.StatusOK, marshalJSON(chatEntries)
})
}
45 changes: 45 additions & 0 deletions backend/cmd/map/handlers/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -846,3 +846,48 @@ func TestJSONWorldDominationHandlerReturnsCurrentTeamMetrics(t *testing.T) {
require.Equal(t, 67, resp.WinRatePct)
require.Equal(t, 33, resp.LoseRatePct)
}

func TestJSONChallengesFeedCacheServesL1AndInvalidates(t *testing.T) {
db := newJSONTestDB(t)
challengeManager, err := challenges.CreateChallengeManager(db)
require.NoError(t, err)
require.NoError(t, challengeManager.Create(challenges.Challenge{
Title: "Cached challenge",
Country: "ES",
Active: true,
Points: 100,
UUID: jsonTestUUID,
}))

handler := CreateHandlersMap(
WithConfig(config.MapCTFConfiguration{Map: config.ConfigurationMap{UUID: jsonTestUUID}}),
WithChallenges(challengeManager),
)
// L1-only cache (no Redis) keeps the test network-free.
handler.feeds = &respCache{}

fetch := func() []challenges.Challenge {
req := newRequestWithUUID(http.MethodGet, "/json/challenges", jsonTestUUID)
rec := httptest.NewRecorder()
handler.JSONChallengesHandler(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var out []challenges.Challenge
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &out))
return out
}

// First fetch misses and populates the cache.
require.Len(t, fetch(), 1)

// Deactivate the challenge directly in the DB, bypassing the handler/cache.
require.NoError(t, challengeManager.DB.Model(&challenges.Challenge{}).
Where("uuid = ?", jsonTestUUID).Update("active", false).Error)

// Cached read still returns the stale challenge, proving L1 served it.
cached := fetch()
require.Len(t, cached, 1, "cached feed should not reflect a bypassing DB write")

// After invalidation the next fetch reflects the DB state.
handler.invalidateFeed("challenges", jsonTestUUID)
require.Empty(t, fetch(), "invalidated feed should reflect the DB state")
}
5 changes: 5 additions & 0 deletions backend/cmd/map/handlers/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ func (h *HandlersMap) ChatPOSTHandler(w http.ResponseWriter, r *http.Request) {
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: h.T(r.Context())("chat.failed_create")})
return
}
h.invalidateFeed("chat", uuid)
HTTPResponse(w, JSONApplicationUTF8, http.StatusOK, MapChatResponse{
Success: true,
})
Expand Down Expand Up @@ -318,6 +319,8 @@ func (h *HandlersMap) ScorePOSTHandler(w http.ResponseWriter, r *http.Request) {
return
}

h.invalidateFeed("teams", uuid)
h.invalidateFeed("activity", uuid)
HTTPResponse(w, JSONApplicationUTF8, http.StatusOK, MapScoreResponse{
Success: true,
Message: h.T(r.Context())("score.completed"),
Expand Down Expand Up @@ -523,6 +526,8 @@ func (h *HandlersMap) HintPOSTHandler(w http.ResponseWriter, r *http.Request) {
return
}

h.invalidateFeed("teams", uuid)
h.invalidateFeed("activity", uuid)
HTTPResponse(w, JSONApplicationUTF8, http.StatusOK, MapHintResponse{
Success: true,
Message: h.T(r.Context())("hint.unlocked"),
Expand Down
Loading
Loading