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

@@ -19,7 +19,7 @@ type ScrimStart struct {
}
func (s *Service) CancelEvent(ctx context.Context, actor domain.Account, eventID string, expectedVersion int) (domain.Event, error) {
if !actor.IsAdmin() {
if !actor.IsStaff() {
return domain.Event{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
@@ -44,7 +44,7 @@ func (s *Service) CancelEvent(ctx context.Context, actor domain.Account, eventID
}
func (s *Service) DeleteEvent(ctx context.Context, actor domain.Account, eventID string) error {
if !actor.IsAdmin() {
if !actor.IsStaff() {
return domain.ErrForbidden
}
if err := s.Store.DeleteEvent(ctx, eventID); err != nil {
@@ -57,7 +57,7 @@ func (s *Service) DeleteEvent(ctx context.Context, actor domain.Account, eventID
}
func (s *Service) CloseRegistration(ctx context.Context, actor domain.Account, eventID, rulesetID string, expectedVersion int) (domain.Event, error) {
if !actor.IsAdmin() {
if !actor.IsStaff() {
return domain.Event{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
@@ -81,7 +81,7 @@ func (s *Service) CloseRegistration(ctx context.Context, actor domain.Account, e
}
func (s *Service) GenerateBalance(ctx context.Context, actor domain.Account, eventID string, expectedVersion int) (BalanceWorkflow, error) {
if !actor.IsAdmin() {
if !actor.IsStaff() {
return BalanceWorkflow{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
@@ -106,7 +106,7 @@ func (s *Service) GenerateBalance(ctx context.Context, actor domain.Account, eve
}
func (s *Service) SelectWorkflowBalance(ctx context.Context, actor domain.Account, eventID string, candidate domain.BalanceCandidate, expectedVersion int) (domain.RosterDraft, error) {
if !actor.IsAdmin() {
if !actor.IsStaff() {
return domain.RosterDraft{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
@@ -137,7 +137,7 @@ func (s *Service) SelectWorkflowBalance(ctx context.Context, actor domain.Accoun
}
func (s *Service) SwapRoster(ctx context.Context, actor domain.Account, eventID, teamA, playerA, teamB, playerB string, expectedVersion int) (domain.RosterDraft, error) {
if !actor.IsAdmin() {
if !actor.IsStaff() {
return domain.RosterDraft{}, domain.ErrForbidden
}
roster, err := s.Store.GetRoster(ctx, eventID)
@@ -159,8 +159,83 @@ func (s *Service) SwapRoster(ctx context.Context, actor domain.Account, eventID,
return roster, err
}
func (s *Service) MoveRosterPlayer(ctx context.Context, actor domain.Account, eventID, fromTeamID, playerID, toTeamID string, role domain.Role, expectedVersion int) (domain.RosterDraft, error) {
if !actor.IsStaff() {
return domain.RosterDraft{}, domain.ErrForbidden
}
roster, err := s.Store.GetRoster(ctx, eventID)
if err != nil {
return roster, err
}
if roster.Version != expectedVersion {
return roster, fmt.Errorf("%w: stale roster version", domain.ErrConflict)
}
if err = roster.MoveToEmpty(fromTeamID, playerID, toTeamID, role); err != nil {
return roster, err
}
return s.saveRosterChange(ctx, actor, roster, expectedVersion, "roster.player_moved")
}
func (s *Service) PlaceReservePlayer(ctx context.Context, actor domain.Account, eventID, teamID, reserveID string, role domain.Role, expectedVersion int) (domain.RosterDraft, error) {
if !actor.IsStaff() {
return domain.RosterDraft{}, domain.ErrForbidden
}
roster, err := s.Store.GetRoster(ctx, eventID)
if err != nil {
return roster, err
}
if roster.Version != expectedVersion {
return roster, fmt.Errorf("%w: stale roster version", domain.ErrConflict)
}
players, err := s.Store.ListPlayers(ctx)
if err != nil {
return roster, err
}
rating := 0
for _, player := range players {
if player.ID == reserveID {
rating = ratingForRole(player, role)
break
}
}
if rating == 0 {
return roster, domain.ErrNotFound
}
if err = roster.PlaceReserve(teamID, role, reserveID, rating); err != nil {
return roster, err
}
return s.saveRosterChange(ctx, actor, roster, expectedVersion, "roster.reserve_placed")
}
func (s *Service) RemoveRosterPlayer(ctx context.Context, actor domain.Account, eventID, teamID, playerID string, expectedVersion int) (domain.RosterDraft, error) {
if !actor.IsStaff() {
return domain.RosterDraft{}, domain.ErrForbidden
}
roster, err := s.Store.GetRoster(ctx, eventID)
if err != nil {
return roster, err
}
if roster.Version != expectedVersion {
return roster, fmt.Errorf("%w: stale roster version", domain.ErrConflict)
}
if err = roster.MoveToReserve(teamID, playerID); err != nil {
return roster, err
}
return s.saveRosterChange(ctx, actor, roster, expectedVersion, "roster.player_removed")
}
func (s *Service) saveRosterChange(ctx context.Context, actor domain.Account, roster domain.RosterDraft, expectedVersion int, action string) (domain.RosterDraft, error) {
out, err := s.Store.SaveRoster(ctx, roster, expectedVersion)
if err == nil {
_ = s.Store.SaveTeams(ctx, roster.EventID, out.Teams)
_ = s.Store.AppendAudit(ctx, actor.ID, action, roster.EventID, out)
s.Bus.Publish("event:"+roster.EventID, out)
}
return out, err
}
func (s *Service) SubstituteRoster(ctx context.Context, actor domain.Account, eventID, teamID, outgoingID, reserveID string, expectedVersion int, emergency bool) (domain.RosterDraft, error) {
if !actor.IsAdmin() {
if !actor.IsStaff() {
return domain.RosterDraft{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
@@ -220,7 +295,7 @@ func (s *Service) SubstituteRoster(ctx context.Context, actor domain.Account, ev
}
func (s *Service) SetRosterCaptain(ctx context.Context, actor domain.Account, eventID, teamID, playerID string, expectedVersion int) (domain.RosterDraft, error) {
if !actor.IsAdmin() {
if !actor.IsStaff() {
return domain.RosterDraft{}, domain.ErrForbidden
}
roster, err := s.Store.GetRoster(ctx, eventID)
@@ -253,7 +328,7 @@ func (s *Service) SetRosterCaptain(ctx context.Context, actor domain.Account, ev
}
func (s *Service) ConfirmRosters(ctx context.Context, actor domain.Account, eventID string, expectedEventVersion, expectedRosterVersion int) (domain.Event, error) {
if !actor.IsAdmin() {
if !actor.IsStaff() {
return domain.Event{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
@@ -287,8 +362,59 @@ func (s *Service) ConfirmRosters(ctx context.Context, actor domain.Account, even
return event, err
}
func (s *Service) RevertWorkflowStage(ctx context.Context, actor domain.Account, eventID string, expectedVersion int) (domain.Event, error) {
if !actor.IsStaff() {
return domain.Event{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
if err != nil {
return event, err
}
oldVersion := event.Version
var previous domain.EventState
switch event.State {
case domain.RegistrationClosed:
previous = domain.RegistrationOpen
case domain.Balancing:
previous = domain.RegistrationClosed
case domain.RostersDraft:
previous = domain.Balancing
case domain.RostersConfirmed:
previous = domain.RostersDraft
default:
return event, fmt.Errorf("%w: this workflow stage cannot be reverted", domain.ErrConflict)
}
if err = event.Transition([]domain.EventState{event.State}, previous, expectedVersion); err != nil {
return event, err
}
if previous == domain.Balancing {
if err = s.Store.ResetRoster(ctx, eventID); err != nil {
return event, err
}
}
if previous == domain.RostersDraft {
roster, rosterErr := s.Store.GetRoster(ctx, eventID)
if rosterErr != nil {
return event, rosterErr
}
rosterVersion := roster.Version
roster.Confirmed = false
roster.Version++
if _, rosterErr = s.Store.SaveRoster(ctx, roster, rosterVersion); rosterErr != nil {
return event, rosterErr
}
}
event.UpdatedAt = s.Now()
event, err = s.Store.SaveEventWorkflow(ctx, event, oldVersion)
if err == nil {
_ = s.Store.AppendAudit(ctx, actor.ID, "workflow.reverted", eventID, map[string]domain.EventState{"state": previous})
s.Bus.Publish("event:"+eventID, event)
}
return event, err
}
func (s *Service) StartScrim(ctx context.Context, actor domain.Account, eventID string, expectedVersion int) (ScrimStart, error) {
if !actor.IsAdmin() {
if !actor.IsStaff() {
return ScrimStart{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)