Implement account management features in API, including endpoints for listing accounts and updating account roles. Introduce moderator role with associated permissions, and refactor access control checks to accommodate staff roles. Update database schema to support new role constraints and enhance frontend navigation for staff access.
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / compose (push) Has been cancelled

This commit is contained in:
2026-07-19 02:32:15 +03:00
parent 4b889fb8a0
commit 1a5a39baf0
18 changed files with 726 additions and 74 deletions

View File

@@ -49,11 +49,17 @@ func (r RosterDraft) Validate(requireCaptains bool) error {
}
counts := map[Role]int{}
for _, slot := range team.Slots {
if slot.PlayerID == "" || seen[slot.PlayerID] {
counts[slot.Role]++
if slot.PlayerID == "" {
if requireCaptains {
return fmt.Errorf("%w: every roster slot must be filled", ErrInvalid)
}
continue
}
if seen[slot.PlayerID] {
return fmt.Errorf("%w: roster players must be unique", ErrInvalid)
}
seen[slot.PlayerID] = true
counts[slot.Role]++
}
if counts[Tank] != 1 || counts[Damage] != 2 || counts[Support] != 2 {
return fmt.Errorf("%w: every team must use 1/2/2", ErrInvalid)
@@ -127,6 +133,88 @@ func (r *RosterDraft) Substitute(teamID, outgoingID, reserveID string, rating in
return ErrNotFound
}
func (r *RosterDraft) PlaceReserve(teamID string, role Role, reserveID string, rating int) error {
if r.Confirmed {
return fmt.Errorf("%w: rosters are locked", ErrConflict)
}
reserveIndex := slices.Index(r.Reserve, reserveID)
if reserveIndex < 0 {
return fmt.Errorf("%w: player is not in reserve", ErrInvalid)
}
for teamIndex := range r.Teams {
if r.Teams[teamIndex].ID != teamID {
continue
}
for slotIndex := range r.Teams[teamIndex].Slots {
slot := &r.Teams[teamIndex].Slots[slotIndex]
if slot.Role == role && slot.PlayerID == "" {
slot.PlayerID, slot.Rating = reserveID, rating
r.Reserve = slices.Delete(r.Reserve, reserveIndex, reserveIndex+1)
r.Version++
return r.Validate(false)
}
}
}
return fmt.Errorf("%w: empty role slot not found", ErrNotFound)
}
func (r *RosterDraft) MoveToEmpty(fromTeamID, playerID, toTeamID string, role Role) error {
if r.Confirmed {
return fmt.Errorf("%w: rosters are locked", ErrConflict)
}
var source, target *Slot
var sourceTeam *Team
for teamIndex := range r.Teams {
team := &r.Teams[teamIndex]
for slotIndex := range team.Slots {
slot := &team.Slots[slotIndex]
if team.ID == fromTeamID && slot.PlayerID == playerID {
source, sourceTeam = slot, team
}
if team.ID == toTeamID && slot.Role == role && slot.PlayerID == "" {
target = slot
}
}
}
if source == nil || target == nil {
return ErrNotFound
}
if source.Role != target.Role {
return fmt.Errorf("%w: only equal roles can be moved", ErrInvalid)
}
target.PlayerID, target.Rating = source.PlayerID, source.Rating
source.PlayerID, source.Rating = "", 0
if sourceTeam.CaptainPlayerID == playerID {
sourceTeam.CaptainPlayerID = ""
}
r.Version++
return r.Validate(false)
}
func (r *RosterDraft) MoveToReserve(teamID, playerID string) error {
if r.Confirmed {
return fmt.Errorf("%w: rosters are locked", ErrConflict)
}
for teamIndex := range r.Teams {
if r.Teams[teamIndex].ID != teamID {
continue
}
for slotIndex := range r.Teams[teamIndex].Slots {
slot := &r.Teams[teamIndex].Slots[slotIndex]
if slot.PlayerID == playerID {
slot.PlayerID, slot.Rating = "", 0
r.Reserve = append(r.Reserve, playerID)
if r.Teams[teamIndex].CaptainPlayerID == playerID {
r.Teams[teamIndex].CaptainPlayerID = ""
}
r.Version++
return r.Validate(false)
}
}
}
return ErrNotFound
}
func (r *RosterDraft) EmergencySubstitute(teamID, outgoingID, reserveID string, rating int) error {
confirmed := r.Confirmed
r.Confirmed = false