Skip to content

Commit

Permalink
feat: kick banned player from the queue (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
garrappachc authored Oct 23, 2024
1 parent 596259b commit c4e7756
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 38 deletions.
4 changes: 4 additions & 0 deletions src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export interface Events {
'player/ban:added': {
ban: PlayerBanModel
}
'player/ban:revoked': {
ban: PlayerBanModel
admin: SteamId64
}

'queue/mapPool:reset': {
maps: MapPoolEntry[]
Expand Down
21 changes: 20 additions & 1 deletion src/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ dialog::backdrop {
background-color: lighten(theme(colors.abru.DEFAULT), 4%);
}
}

&--darker {
background-color: theme(colors.abru.dark.25);
&:hover {
background-color: darken(theme(colors.abru.dark.25), 5%);
}
}
}

.banner {
Expand Down Expand Up @@ -438,6 +445,18 @@ dialog::backdrop {
}
}

.admin-panel-header {
display: grid;
grid-template-columns: 1fr auto;
margin-bottom: 24px;
align-items: center;

h1 {
font-size: 32px;
color: theme(colors.abru.light.75);
}
}

.input-group {
display: flex;
flex-direction: column;
Expand All @@ -458,7 +477,7 @@ dialog::backdrop {
font-weight: 500;
font-size: 18px;
border-radius: 4px;
padding: 6px 12px;
padding: 6px 12px;
position: relative;

&:hover {
Expand Down
25 changes: 25 additions & 0 deletions src/players/revoke-ban.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { ObjectId } from 'mongodb'
import { collections } from '../database/collections'
import type { SteamId64 } from '../shared/types/steam-id-64'
import { events } from '../events'

export async function revokeBan(banId: ObjectId, adminId: SteamId64) {
let ban = await collections.playerBans.findOne({ _id: banId })
if (!ban) {
throw new Error(`ban not found: ${banId}`)
}

if (ban.end < new Date()) {
throw new Error(`ban already expired: ${banId}`)
}

const after = (await collections.playerBans.findOneAndUpdate(
{ _id: banId },
{ $set: { end: new Date() } },
{
returnDocument: 'after',
},
))!
events.emit('player/ban:revoked', { ban: after, admin: adminId })
return after
}
22 changes: 22 additions & 0 deletions src/players/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ZodTypeProvider } from 'fastify-type-provider-zod'
import { z } from 'zod'
import { steamId64 } from '../shared/schemas/steam-id-64'
import {
BanDetails,
EditPlayerBansPage,
EditPlayerProfilePage,
EditPlayerSkillPage,
Expand All @@ -19,6 +20,8 @@ import { banExpiryFormSchema } from './schemas/ban-expiry-form.schema'
import { format } from 'date-fns'
import { getBanExpiryDate } from './get-ban-expiry-date'
import { addBan } from './add-ban'
import { ObjectId } from 'mongodb'
import { revokeBan } from './revoke-ban'

export default fp(
// eslint-disable-next-line @typescript-eslint/require-await
Expand Down Expand Up @@ -205,6 +208,25 @@ export default fp(
reply.status(200).html(await EditPlayerBansPage({ player, user: req.user! }))
},
)
.put(
'/players/:steamId/edit/bans/:banId/revoke',
{
config: {
authorize: [PlayerRole.admin],
},
schema: {
params: z.object({
steamId: steamId64,
banId: z.string().transform(value => new ObjectId(value)),
}),
},
},
async (request, reply) => {
const { banId } = request.params
const ban = await revokeBan(banId, request.user!.player.steamId)
reply.status(200).html(await BanDetails({ ban }))
},
)
.get(
'/players/ban-expiry',
{
Expand Down
101 changes: 67 additions & 34 deletions src/players/views/html/edit-player.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { Footer } from '../../../html/components/footer'
import {
AdminPanel,
AdminPanelBody,
AdminPanelHeader,
AdminPanelLink,
AdminPanelSidebar,
} from '../../../html/components/admin-panel'
Expand All @@ -18,6 +17,7 @@ import {
IconChartArrowsVertical,
IconPlus,
IconUserScan,
IconX,
} from '../../../html/components/icons'
import type { Children } from '@kitajs/html'
import { queue } from '../../../queue'
Expand Down Expand Up @@ -107,24 +107,30 @@ export async function EditPlayerBansPage(props: { player: WithId<PlayerModel>; u
.toArray()

return (
<EditPlayer player={props.player} user={props.user} activePage="/bans">
<EditPlayer
player={props.player}
user={props.user}
activePage="/bans"
action={
<a href={`/players/${props.player.steamId}/edit/bans/add`} class="button button--accent">
<IconPlus />
Add ban
</a>
}
>
<div class="admin-panel-content">
{bans.length === 0 ? (
<span class="text-abru-light-75 italic">No bans</span>
<span class="italic text-abru-light-75">No bans</span>
) : (
bans.map(ban => <BanDetails ban={ban} />)
<>
<div class="edit-player-ban-list">
{bans.map(ban => (
<BanDetails ban={ban} />
))}
</div>
</>
)}
</div>

<div class="flex">
<a
href={`/players/${props.player.steamId}/edit/bans/add`}
class="button button--accent mt-6"
>
<IconPlus />
Add ban
</a>
</div>
</EditPlayer>
)
}
Expand All @@ -134,6 +140,7 @@ function EditPlayer(props: {
user: User
children: Children
activePage: keyof typeof editPlayerPages
action?: Children
}) {
return (
<Layout
Expand Down Expand Up @@ -167,7 +174,10 @@ function EditPlayer(props: {
</AdminPanelLink>
</AdminPanelSidebar>
<AdminPanelBody>
<AdminPanelHeader>{editPlayerPages[props.activePage]}</AdminPanelHeader>
<div class="admin-panel-header">
<h1>{editPlayerPages[props.activePage]}</h1>
{props.action ? props.action : <></>}
</div>
{props.children}
</AdminPanelBody>
</AdminPanel>
Expand All @@ -177,30 +187,53 @@ function EditPlayer(props: {
)
}

async function BanDetails(props: { ban: PlayerBanModel }) {
export async function BanDetails(props: { ban: WithId<PlayerBanModel> }) {
const player = await collections.players.findOne({ _id: props.ban.player })
if (!player) {
throw new Error(`player ${props.ban.player.toString()} not found`)
}
const admin = await collections.players.findOne({ _id: props.ban.admin })

return (
<div class="group">
<div>
<span class="text-2xl font-bold me-2" safe>
{props.ban.reason}
</span>
<span class="text-sm">
by{' '}
<a href={`/players/${admin?.steamId}`} safe>
{admin?.name ?? 'unknown admin'}
</a>
</span>
</div>
<div class="ban-item group" id={`player-ban-${props.ban._id}`}>
<form class="contents">
<div class="col-span-3">
<span class="me-2 text-2xl font-bold" safe>
{props.ban.reason}
</span>
<span class="text-sm">
by{' '}
<a href={`/players/${admin?.steamId}`} safe>
{admin?.name ?? 'unknown admin'}
</a>
</span>
</div>

<div class="text-base" safe>
Starts {format(props.ban.start, 'MMMM dd, yyyy, HH:mm')}
</div>
<div class="row-span-2 flex items-center">
{props.ban.end > new Date() ? (
<button
class="button button--darker"
hx-put={`/players/${player.steamId}/edit/bans/${props.ban._id.toString()}/revoke`}
hx-trigger="click"
hx-target={`#player-ban-${props.ban._id}`}
hx-swap="outerHTML"
>
<IconX />
<span class="sr-only">Revoke ban</span>
</button>
) : (
<></>
)}
</div>

<div class="text-base" safe>
Ends {format(props.ban.end, 'MMMM dd, yyyy, HH:mm')}
</div>
<span class="text-base">
Starts: <strong safe>{format(props.ban.start, 'MMMM dd, yyyy, HH:mm')}</strong>
</span>

<span class="text-base">
Ends: <strong safe>{format(props.ban.end, 'MMMM dd, yyyy, HH:mm')}</strong>
</span>
</form>
</div>
)
}
14 changes: 14 additions & 0 deletions src/players/views/html/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,17 @@
width: 42px;
height: 42px;
}

.edit-player-ban-list {
display: grid;
grid-template-columns: auto auto 1fr auto;
gap: 8px;

.ban-item {
@apply col-span-4;
@apply row-span-2;
display: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
}
}
1 change: 1 addition & 0 deletions src/queue/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default fp(
await app.register((await import('./plugins/update-clients')).default)
await app.register((await import('./plugins/auto-reset')).default)
await app.register((await import('./plugins/kick-replacement-players')).default)
await app.register((await import('./plugins/kick-banned-players')).default)

app.get('/', async (req, reply) => {
await reply.status(200).html(QueuePage(req.user))
Expand Down
23 changes: 23 additions & 0 deletions src/queue/plugins/kick-banned-players.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import fp from 'fastify-plugin'
import { events } from '../../events'
import { safe } from '../../utils/safe'
import { kick } from '../kick'
import { collections } from '../../database/collections'

export default fp(
async () => {
events.on('player/ban:added', async ({ ban }) => {
await safe(async () => {
const player = await collections.players.findOne({ _id: ban.player })
if (!player) {
throw new Error(`player ${ban.player.toString()} not found`)
}

await kick(player.steamId)
})
})
},
{
name: 'kick banned players',
},
)
12 changes: 9 additions & 3 deletions src/queue/views/html/queue-slot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,17 @@ export async function QueueSlot(props: { slot: QueueSlotModel; actor?: SteamId64
)
} else if (props.actor) {
const actor = await collections.players.findOne({ steamId: props.actor })
if (!props.actor) {
if (!actor) {
throw new Error(`actor invalid: ${props.actor}`)
}

slotContent = <JoinButton slotId={props.slot.id} disabled={Boolean(actor?.activeGame)} />
const activeBans = await collections.playerBans.countDocuments({
player: actor._id,
end: { $gte: new Date() },
})
const disabled = Boolean(actor.activeGame) || activeBans > 0

slotContent = <JoinButton slotId={props.slot.id} disabled={disabled} />
}

return (
Expand Down Expand Up @@ -137,7 +143,7 @@ function PlayerInfo(props: {
class="h-[42px] w-[42px] rounded"
/>
<a
class="text-abru-dark-3 flex-1 overflow-hidden whitespace-nowrap text-center text-xl font-bold hover:underline"
class="flex-1 overflow-hidden whitespace-nowrap text-center text-xl font-bold text-abru-dark-3 hover:underline"
href={`/players/${props.player.steamId}`}
safe
>
Expand Down
23 changes: 23 additions & 0 deletions tests/10-queue/07-banned-players-get-kicked.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { users } from '../data'
import { authUsers, expect } from '../fixtures/auth-users'
import { QueuePage } from '../pages/queue.page'

authUsers.use({ steamIds: [users[0].steamId, users[1].steamId] })

authUsers('banned player gets kicked', async ({ steamIds, pages }) => {
const player = new QueuePage(pages.get(users[1].steamId)!)
await player.joinQueue(0)

const admin = users[0]
const adminPage = pages.get(admin.steamId)!
await adminPage.goto(`/players/${steamIds[1]}`)
await adminPage.getByRole('link', { name: 'Edit player' }).click()
await adminPage.getByRole('link', { name: 'Bans' }).click()
await adminPage.getByRole('link', { name: 'Add ban' }).click()
await adminPage.getByLabel('Reason').fill('Cheating')
await adminPage.getByRole('button', { name: 'Save' }).click()

await expect(player.slot(0).joinButton()).toBeDisabled()

await adminPage.getByRole('button', { name: 'Revoke ban' }).click()
})

0 comments on commit c4e7756

Please sign in to comment.