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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -22,8 +22,9 @@ type ID string
|
||||
type GlobalRole string
|
||||
|
||||
const (
|
||||
RolePlayer GlobalRole = "player"
|
||||
RoleAdmin GlobalRole = "admin"
|
||||
RolePlayer GlobalRole = "player"
|
||||
RoleModerator GlobalRole = "moderator"
|
||||
RoleAdmin GlobalRole = "admin"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
@@ -36,6 +37,7 @@ type Account struct {
|
||||
}
|
||||
|
||||
func (a Account) IsAdmin() bool { return a.Role == RoleAdmin }
|
||||
func (a Account) IsStaff() bool { return a.Role == RoleAdmin || a.Role == RoleModerator }
|
||||
|
||||
type Ratings struct {
|
||||
Tank int `json:"tank"`
|
||||
|
||||
@@ -17,6 +17,16 @@ func TestEventWorkflowRejectsStaleVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestModeratorIsStaffButNotAdministrator(t *testing.T) {
|
||||
account := Account{Role: RoleModerator}
|
||||
if !account.IsStaff() {
|
||||
t.Fatal("moderator should have operational staff permissions")
|
||||
}
|
||||
if account.IsAdmin() {
|
||||
t.Fatal("moderator must not have administrator role-management permission")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRosterSwapAndReserveKeepRoleShape(t *testing.T) {
|
||||
roster := testRoster()
|
||||
if err := roster.Swap("a", "a-d1", "b", "b-s1"); !errors.Is(err, ErrInvalid) {
|
||||
@@ -36,6 +46,35 @@ func TestRosterSwapAndReserveKeepRoleShape(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRosterDragOperationsPreserveEmptyRoleSlots(t *testing.T) {
|
||||
roster := testRoster()
|
||||
roster.Teams[0].CaptainPlayerID = "a-t"
|
||||
if err := roster.MoveToReserve("a", "a-t"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if roster.Teams[0].Slots[0].PlayerID != "" || roster.Teams[0].Slots[0].Role != Tank {
|
||||
t.Fatal("removed tank did not leave an identifiable empty tank slot")
|
||||
}
|
||||
if roster.Teams[0].CaptainPlayerID != "" {
|
||||
t.Fatal("removed captain was retained")
|
||||
}
|
||||
if err := roster.Validate(true); !errors.Is(err, ErrInvalid) {
|
||||
t.Fatalf("incomplete roster was confirmable: %v", err)
|
||||
}
|
||||
if err := roster.PlaceReserve("a", Tank, "reserve", 31); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := roster.MoveToReserve("b", "b-t"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := roster.MoveToEmpty("a", "reserve", "b", Tank); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if roster.Teams[1].Slots[0].PlayerID != "reserve" {
|
||||
t.Fatal("dragging into an empty role slot did not move the player")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeriesFSMRunsCoinBansAndResult(t *testing.T) {
|
||||
rules := testRules()
|
||||
series, err := NewSeries("series", "event", "", [2]string{"a", "b"}, rules)
|
||||
|
||||
Reference in New Issue
Block a user