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:
@@ -65,6 +65,8 @@ 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/accounts", s.accounts)
|
||||
api.Patch("/api/accounts/{accountID}/moderator", s.setModerator)
|
||||
api.Get("/api/events", s.events)
|
||||
api.Post("/api/events", s.createEvent)
|
||||
api.Get("/api/events/{eventID}", s.getEvent)
|
||||
@@ -84,10 +86,14 @@ func New(service *application.Service, store application.Store, hub *realtime.Hu
|
||||
api.Get("/api/events/{eventID}/roster", s.getRoster)
|
||||
api.Put("/api/events/{eventID}/roster", s.selectWorkflowBalance)
|
||||
api.Post("/api/events/{eventID}/roster/swap", s.swapRoster)
|
||||
api.Post("/api/events/{eventID}/roster/move", s.moveRosterPlayer)
|
||||
api.Post("/api/events/{eventID}/roster/place-reserve", s.placeReservePlayer)
|
||||
api.Post("/api/events/{eventID}/roster/remove", s.removeRosterPlayer)
|
||||
api.Post("/api/events/{eventID}/roster/substitute", s.substituteRoster)
|
||||
api.Post("/api/events/{eventID}/roster/emergency-substitute", s.emergencySubstitute)
|
||||
api.Put("/api/events/{eventID}/roster/captain", s.setRosterCaptain)
|
||||
api.Post("/api/events/{eventID}/roster/confirm", s.confirmRosters)
|
||||
api.Post("/api/events/{eventID}/workflow/back", s.revertWorkflowStage)
|
||||
api.Post("/api/events/{eventID}/start", s.startScrim)
|
||||
api.Put("/api/teams/{teamID}/captain", s.assignCaptain)
|
||||
api.Post("/api/rulesets", s.saveRuleset)
|
||||
@@ -220,8 +226,8 @@ func (s *Server) authenticate(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
func who(r *http.Request) identity { return r.Context().Value(identityKey{}).(identity) }
|
||||
func requireAdmin(r *http.Request) error {
|
||||
if !who(r).account.IsAdmin() {
|
||||
func requireStaff(r *http.Request) error {
|
||||
if !who(r).account.IsStaff() {
|
||||
return domain.ErrForbidden
|
||||
}
|
||||
return nil
|
||||
@@ -253,6 +259,22 @@ func (s *Server) players(w http.ResponseWriter, r *http.Request) {
|
||||
respond(w, out, err, 200)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (s *Server) setModerator(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
Moderator bool `json:"moderator"`
|
||||
}
|
||||
if !decode(w, r, &in) {
|
||||
return
|
||||
}
|
||||
out, err := s.service.SetModerator(r.Context(), who(r).account, chi.URLParam(r, "accountID"), in.Moderator)
|
||||
respond(w, out, err, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *Server) events(w http.ResponseWriter, r *http.Request) {
|
||||
from := time.Unix(0, 0).UTC()
|
||||
if raw := r.URL.Query().Get("from"); raw != "" {
|
||||
@@ -358,7 +380,7 @@ func (s *Server) assignCaptain(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) saveRuleset(w http.ResponseWriter, r *http.Request) {
|
||||
if err := requireAdmin(r); err != nil {
|
||||
if err := requireStaff(r); err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
@@ -410,7 +432,7 @@ func (s *Server) coinToss(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) createMapDraft(w http.ResponseWriter, r *http.Request) {
|
||||
if err := requireAdmin(r); err != nil {
|
||||
if err := requireStaff(r); err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
@@ -478,7 +500,7 @@ func (s *Server) mapBan(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) createHeroDraft(w http.ResponseWriter, r *http.Request) {
|
||||
if err := requireAdmin(r); err != nil {
|
||||
if err := requireStaff(r); err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
@@ -532,7 +554,7 @@ func (s *Server) heroBan(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Server) authorizeTeam(r *http.Request, eventID, teamID string) error {
|
||||
id := who(r)
|
||||
if id.account.IsAdmin() {
|
||||
if id.account.IsStaff() {
|
||||
return nil
|
||||
}
|
||||
teams, err := s.store.ListTeams(r.Context(), eventID)
|
||||
@@ -548,7 +570,7 @@ func (s *Server) authorizeTeam(r *http.Request, eventID, teamID string) error {
|
||||
}
|
||||
|
||||
func (s *Server) createTournament(w http.ResponseWriter, r *http.Request) {
|
||||
if err := requireAdmin(r); err != nil {
|
||||
if err := requireStaff(r); err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -79,6 +79,48 @@ func (s *Server) swapRoster(w http.ResponseWriter, r *http.Request) {
|
||||
respond(w, out, err, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *Server) moveRosterPlayer(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
FromTeamID string `json:"fromTeamId"`
|
||||
PlayerID string `json:"playerId"`
|
||||
ToTeamID string `json:"toTeamId"`
|
||||
Role domain.Role `json:"role"`
|
||||
ExpectedVersion int `json:"expectedVersion"`
|
||||
}
|
||||
if !decode(w, r, &in) {
|
||||
return
|
||||
}
|
||||
out, err := s.service.MoveRosterPlayer(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.FromTeamID, in.PlayerID, in.ToTeamID, in.Role, in.ExpectedVersion)
|
||||
respond(w, out, err, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *Server) placeReservePlayer(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
TeamID string `json:"teamId"`
|
||||
ReservePlayerID string `json:"reservePlayerId"`
|
||||
Role domain.Role `json:"role"`
|
||||
ExpectedVersion int `json:"expectedVersion"`
|
||||
}
|
||||
if !decode(w, r, &in) {
|
||||
return
|
||||
}
|
||||
out, err := s.service.PlaceReservePlayer(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.TeamID, in.ReservePlayerID, in.Role, in.ExpectedVersion)
|
||||
respond(w, out, err, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *Server) removeRosterPlayer(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
TeamID string `json:"teamId"`
|
||||
PlayerID string `json:"playerId"`
|
||||
ExpectedVersion int `json:"expectedVersion"`
|
||||
}
|
||||
if !decode(w, r, &in) {
|
||||
return
|
||||
}
|
||||
out, err := s.service.RemoveRosterPlayer(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.TeamID, in.PlayerID, in.ExpectedVersion)
|
||||
respond(w, out, err, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *Server) substituteRoster(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleSubstitute(w, r, false)
|
||||
}
|
||||
@@ -126,6 +168,17 @@ func (s *Server) confirmRosters(w http.ResponseWriter, r *http.Request) {
|
||||
respond(w, out, err, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *Server) revertWorkflowStage(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
ExpectedVersion int `json:"expectedVersion"`
|
||||
}
|
||||
if !decode(w, r, &in) {
|
||||
return
|
||||
}
|
||||
out, err := s.service.RevertWorkflowStage(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.ExpectedVersion)
|
||||
respond(w, out, err, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *Server) startScrim(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
ExpectedVersion int `json:"expectedVersion"`
|
||||
|
||||
@@ -55,7 +55,7 @@ func (s *Store) UpsertDiscordAccount(ctx context.Context, account domain.Account
|
||||
err = tx.QueryRow(ctx, `INSERT INTO accounts(id,discord_id,username,avatar_url,role,created_at)
|
||||
VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(discord_id) DO UPDATE
|
||||
SET username=excluded.username,avatar_url=excluded.avatar_url,
|
||||
role=CASE WHEN accounts.role='admin' THEN accounts.role ELSE excluded.role END
|
||||
role=CASE WHEN accounts.role IN ('admin','moderator') THEN accounts.role ELSE excluded.role END
|
||||
RETURNING id,discord_id,username,avatar_url,role,created_at`,
|
||||
account.ID, account.DiscordID, account.Username, account.AvatarURL, role, account.CreatedAt).
|
||||
Scan(&account.ID, &account.DiscordID, &account.Username, &account.AvatarURL, &account.Role, &account.CreatedAt)
|
||||
@@ -84,6 +84,31 @@ func (s *Store) UpsertDiscordAccount(ctx context.Context, account domain.Account
|
||||
return account, p, tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) ListAccounts(ctx context.Context) ([]domain.Account, error) {
|
||||
rows, err := s.pool.Query(ctx, `SELECT id,discord_id,username,avatar_url,role,created_at FROM accounts ORDER BY lower(username),id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
accounts := make([]domain.Account, 0)
|
||||
for rows.Next() {
|
||||
var account domain.Account
|
||||
if err := rows.Scan(&account.ID, &account.DiscordID, &account.Username, &account.AvatarURL, &account.Role, &account.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accounts = append(accounts, account)
|
||||
}
|
||||
return accounts, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) UpdateAccountRole(ctx context.Context, accountID string, role domain.GlobalRole) (domain.Account, error) {
|
||||
var account domain.Account
|
||||
err := s.pool.QueryRow(ctx, `UPDATE accounts SET role=$2 WHERE id=$1 AND role<>'admin'
|
||||
RETURNING id,discord_id,username,avatar_url,role,created_at`, accountID, role).
|
||||
Scan(&account.ID, &account.DiscordID, &account.Username, &account.AvatarURL, &account.Role, &account.CreatedAt)
|
||||
return account, mapError(err)
|
||||
}
|
||||
|
||||
func (s *Store) CreateSession(ctx context.Context, token, accountID string, expires time.Time) error {
|
||||
_, err := s.pool.Exec(ctx, `INSERT INTO sessions(token_hash,account_id,expires_at) VALUES($1,$2,$3)`, HashToken(token), accountID, expires)
|
||||
return err
|
||||
@@ -331,6 +356,21 @@ func (s *Store) GetRoster(ctx context.Context, eventID string) (domain.RosterDra
|
||||
return roster, mapError(err)
|
||||
}
|
||||
|
||||
func (s *Store) ResetRoster(ctx context.Context, eventID string) error {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err = tx.Exec(ctx, `DELETE FROM event_rosters WHERE event_id=$1`, eventID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `DELETE FROM teams WHERE event_id=$1`, eventID); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) StartScrim(ctx context.Context, event domain.Event, expectedVersion int, series []domain.Series, tournament *domain.Tournament) error {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user