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:
@@ -51,6 +51,7 @@ describe('backend DTO adapters', () => {
|
||||
expect(adaptEvent({ ...event, state: 'Cancelled' }).status).toBe('cancelled')
|
||||
expect(adaptEvent({ ...event, state: 'Completed' }).status).toBe('completed')
|
||||
expect(adaptEvent({ ...event, state: 'Live' }).status).toBe('live')
|
||||
expect(adaptEvent({ ...event, state: 'RegistrationClosed', endsAt: '2020-01-01T00:00:00Z' }).status).toBe('balancing')
|
||||
})
|
||||
|
||||
it('keeps corrected results out of the authoritative score', () => {
|
||||
@@ -106,4 +107,17 @@ describe('actual backend routes', () => {
|
||||
const rsvpCall = fetchMock.mock.calls.find(([url]) => url === '/api/events/event-1/rsvps/player-1')
|
||||
expect(rsvpCall?.[1]?.body).toBe(JSON.stringify({ status: 'Going' }))
|
||||
})
|
||||
|
||||
it('assigns moderators through the administrator-only route', async () => {
|
||||
const account = { id: 'account-2', discordId: 'discord-2', username: 'Mod', avatarUrl: '', role: 'moderator', createdAt: '2026-07-01T00:00:00Z' }
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify(account), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
await api.setModerator(account.id, true)
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/accounts/account-2/moderator', expect.objectContaining({
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ moderator: true }),
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { toRankOrdinal, type RankOrdinal } from '../ranks'
|
||||
|
||||
export type AccountRole = 'admin' | 'player'
|
||||
export type AccountRole = 'admin' | 'moderator' | 'player'
|
||||
export type RsvpStatus = 'going' | 'maybe' | 'not_going'
|
||||
export type PlayerRole = 'tank' | 'damage' | 'support'
|
||||
export type TeamSide = 'alpha' | 'beta'
|
||||
@@ -216,7 +216,6 @@ export function adaptSession(raw: RawSession): Session {
|
||||
}
|
||||
export function adaptEvent(raw: RawEvent, rsvps: RawRSVP[] = [], currentPlayerId?: string): MixEvent {
|
||||
const now = Date.now()
|
||||
const ends = new Date(raw.endsAt).getTime()
|
||||
const deadline = new Date(raw.registrationDeadline).getTime()
|
||||
const counts = { going: 0, maybe: 0, not_going: 0 }
|
||||
rsvps.forEach((rsvp) => { counts[statusFromRaw(rsvp.status)]++ })
|
||||
@@ -229,7 +228,7 @@ export function adaptEvent(raw: RawEvent, rsvps: RawRSVP[] = [], currentPlayerId
|
||||
endsAt: raw.endsAt,
|
||||
registrationDeadline: raw.registrationDeadline,
|
||||
status: raw.state === 'Cancelled' ? 'cancelled'
|
||||
: raw.state === 'Completed' || now > ends ? 'completed'
|
||||
: raw.state === 'Completed' ? 'completed'
|
||||
: raw.state === 'Live' ? 'live'
|
||||
: raw.state === 'RegistrationOpen' && now <= deadline ? 'registration' : 'balancing',
|
||||
rulesetName: 'Server ruleset',
|
||||
@@ -474,6 +473,9 @@ export const api = {
|
||||
}),
|
||||
})),
|
||||
players: async () => (await raw.players()).map(adaptPlayer),
|
||||
accounts: () => request<RawAccount[]>('/accounts'),
|
||||
setModerator: (accountId: string, moderator: boolean) =>
|
||||
request<RawAccount>(`/accounts/${accountId}/moderator`, { method: 'PATCH', body: JSON.stringify({ moderator }) }),
|
||||
events: async () => {
|
||||
const [events, session] = await Promise.all([raw.events(), raw.session()])
|
||||
return Promise.all(events.map(async (event) => {
|
||||
@@ -535,6 +537,11 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ rulesetId, expectedVersion }),
|
||||
})),
|
||||
revertWorkflowStage: async (eventId: string, expectedVersion: number) =>
|
||||
adaptEvent(await request<RawEvent>(`/events/${eventId}/workflow/back`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ expectedVersion }),
|
||||
})),
|
||||
generateWorkflowBalance: async (eventId: string, expectedVersion: number) => {
|
||||
const [result, players] = await Promise.all([
|
||||
request<{ event: RawEvent; candidates: RawBalanceCandidate[] }>(`/events/${eventId}/balance/generate`, {
|
||||
@@ -557,6 +564,12 @@ export const api = {
|
||||
}),
|
||||
swapRoster: (eventId: string, input: { teamAId: string; playerAId: string; teamBId: string; playerBId: string; expectedVersion: number }) =>
|
||||
request<RawRosterDraft>(`/events/${eventId}/roster/swap`, { method: 'POST', body: JSON.stringify(input) }),
|
||||
moveRosterPlayer: (eventId: string, input: { fromTeamId: string; playerId: string; toTeamId: string; role: RawSlot['role']; expectedVersion: number }) =>
|
||||
request<RawRosterDraft>(`/events/${eventId}/roster/move`, { method: 'POST', body: JSON.stringify(input) }),
|
||||
placeReservePlayer: (eventId: string, input: { teamId: string; reservePlayerId: string; role: RawSlot['role']; expectedVersion: number }) =>
|
||||
request<RawRosterDraft>(`/events/${eventId}/roster/place-reserve`, { method: 'POST', body: JSON.stringify(input) }),
|
||||
removeRosterPlayer: (eventId: string, input: { teamId: string; playerId: string; expectedVersion: number }) =>
|
||||
request<RawRosterDraft>(`/events/${eventId}/roster/remove`, { method: 'POST', body: JSON.stringify(input) }),
|
||||
substituteRoster: (eventId: string, input: { teamId: string; outgoingPlayerId: string; reservePlayerId: string; expectedVersion: number }, emergency = false) =>
|
||||
request<RawRosterDraft>(`/events/${eventId}/roster/${emergency ? 'emergency-substitute' : 'substitute'}`, { method: 'POST', body: JSON.stringify(input) }),
|
||||
setRosterCaptain: (eventId: string, teamId: string, playerId: string, expectedVersion: number) =>
|
||||
@@ -569,11 +582,13 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ expectedVersion, expectedRosterVersion }),
|
||||
}),
|
||||
startScrim: (eventId: string, expectedVersion: number) =>
|
||||
request<{ event: RawEvent; series?: RawSeries; tournament?: RawTournament }>(`/events/${eventId}/start`, {
|
||||
startScrim: async (eventId: string, expectedVersion: number) => {
|
||||
const result = await request<{ event: RawEvent; series?: RawSeries; tournament?: RawTournament }>(`/events/${eventId}/start`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ expectedVersion }),
|
||||
}),
|
||||
})
|
||||
return { ...result, series: result.series ? await adaptSeriesWithTeams(result.series) : undefined }
|
||||
},
|
||||
selectBalance: (eventId: string, candidate: BalanceCandidate) =>
|
||||
request<{ status: string }>(`/events/${eventId}/teams`, {
|
||||
method: 'PUT',
|
||||
|
||||
Reference in New Issue
Block a user