From 872a79663b72436a888eb1b4e16e51e6d254ace8 Mon Sep 17 00:00:00 2001 From: jd Date: Sun, 22 Feb 2026 12:02:05 +0000 Subject: [PATCH] Finished implementing match endpoints --- API Tests/BGApp/Matches/Delete.yml | 6 +-- API Tests/BGApp/Matches/Get.yml | 2 +- API Tests/BGApp/Matches/Leave.yml | 23 +++++++++ API Tests/BGApp/Players/Get.yml | 2 +- src/endpoints/matches.ts | 13 ++++- src/orm/matches.ts | 76 ++++++++++++++++++++---------- src/routes/matches.ts | 3 ++ 7 files changed, 94 insertions(+), 31 deletions(-) create mode 100644 API Tests/BGApp/Matches/Leave.yml diff --git a/API Tests/BGApp/Matches/Delete.yml b/API Tests/BGApp/Matches/Delete.yml index 2de044e..367265c 100644 --- a/API Tests/BGApp/Matches/Delete.yml +++ b/API Tests/BGApp/Matches/Delete.yml @@ -5,13 +5,13 @@ info: http: method: DELETE - url: "{{BASE_URL}}/{{SECTOR}}/{{UserID}}" + url: "{{BASE_URL}}/{{SECTOR}}/{{MatchID}}" auth: inherit runtime: variables: - - name: UserID - value: "" + - name: MatchID + value: 846M1L settings: encodeUrl: true diff --git a/API Tests/BGApp/Matches/Get.yml b/API Tests/BGApp/Matches/Get.yml index 4644842..1de308a 100644 --- a/API Tests/BGApp/Matches/Get.yml +++ b/API Tests/BGApp/Matches/Get.yml @@ -11,7 +11,7 @@ http: runtime: variables: - name: MatchID - value: 9YQ84M + value: 848O12 settings: encodeUrl: true diff --git a/API Tests/BGApp/Matches/Leave.yml b/API Tests/BGApp/Matches/Leave.yml new file mode 100644 index 0000000..bde88d0 --- /dev/null +++ b/API Tests/BGApp/Matches/Leave.yml @@ -0,0 +1,23 @@ +info: + name: Leave + type: http + seq: 4 + +http: + method: POST + url: "{{BASE_URL}}/{{SECTOR}}/{{MatchID}}/leave" + body: + type: json + data: "" + auth: inherit + +runtime: + variables: + - name: MatchID + value: 848O12 + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/API Tests/BGApp/Players/Get.yml b/API Tests/BGApp/Players/Get.yml index 4178f04..7b55ea3 100644 --- a/API Tests/BGApp/Players/Get.yml +++ b/API Tests/BGApp/Players/Get.yml @@ -11,7 +11,7 @@ http: runtime: variables: - name: PlayerID - value: "" + value: 539DPX settings: encodeUrl: true diff --git a/src/endpoints/matches.ts b/src/endpoints/matches.ts index f679762..4dd27ce 100644 --- a/src/endpoints/matches.ts +++ b/src/endpoints/matches.ts @@ -30,7 +30,17 @@ async function get(request: UnwrappedRequest): Promise { async function drop(request: UnwrappedRequest): Promise { try { - return new OkResponse(await orm.matches.drop(UserId.fromHash(request.params.id), request.claims)); + return new OkResponse(await orm.matches.drop(MatchId.fromHash(request.params.id), request.claims)); + } catch (error: any) { + return new ErrorResponse(error as Error); + } +} + +async function leave(request: UnwrappedRequest): Promise { + try { + return new OkResponse( + await orm.matches.removePlayer(MatchId.fromHash(request.params.id), request.claims.userId as UserId), + ); } catch (error: any) { return new ErrorResponse(error as Error); } @@ -40,4 +50,5 @@ export default { create, get, drop, + leave, }; diff --git a/src/orm/matches.ts b/src/orm/matches.ts index 3ce33b5..8792dee 100644 --- a/src/orm/matches.ts +++ b/src/orm/matches.ts @@ -4,6 +4,7 @@ import { first, orderBy } from 'lodash'; import { BadRequestError, NotFoundError, UnauthorizedError } from '../utilities/errors'; import { GameId, MatchId, PlayerId, UserId } from '../utilities/secureIds'; import { calculateElos } from '../utilities/elo'; +import { orm } from './orm'; export class MatchParticipant { matchId?: MatchId; @@ -125,13 +126,14 @@ export class MatchOrm { throw new UnauthorizedError(); } - if (!dbResult) { - throw new NotFoundError('No matching user exists'); + const matchData = dbResult?.find((x: any) => x.match_id); + if (!matchData?.match_id) { + throw new NotFoundError('No matching match exists'); } return new Match( - MatchId.fromID(dbResult?.[0]?.match_id), - GameId.fromID(dbResult?.[0]?.game_id), + MatchId.fromID(matchData?.match_id), + GameId.fromID(matchData?.game_id), orderBy( dbResult .filter((x: any) => x.player_id) @@ -152,28 +154,52 @@ export class MatchOrm { ); } - async drop(id: UserId, claims?: Claims): Promise { - // if ( - // !( - // Claims.test(Claims.ADMIN, claims) || - // Claims.test(Claims.USERS.OTHER.DELETE, claims) || - // (Claims.test(Claims.USERS.SELF.DELETE, claims) && id === claims?.userId) - // ) - // ) { - // throw new UnauthorizedError(); - // } - // - // // Ensure user exists before attempting to delete - // await this.get(id); - // await sql.transaction(async (tx) => { - // await tx`DELETE - // FROM user_claims - // WHERE user_id = ${id.raw}`; - // await tx`DELETE - // FROM users - // WHERE id = ${id.raw}`; - // }); + async drop(id: MatchId, claims?: Claims): Promise { + const match = await this.get(id); + if ( + !( + Claims.test(Claims.ADMIN, claims) || + (Claims.test(Claims.MATCHES.OWNED.DELETE, claims) && match.owner === claims?.userId) + ) + ) { + throw new UnauthorizedError(); + } + + await sql.transaction(async (tx) => { + await tx`DELETE + FROM match_players + WHERE match_id = ${id.raw}`; + await tx`DELETE + FROM matches + WHERE id = ${id.raw}`; + }); return; } + + async removePlayer(matchId: MatchId, participantId: UserId): Promise; + async removePlayer(matchId: MatchId, participantId: PlayerId): Promise { + let playerId: PlayerId = participantId; + if (participantId instanceof UserId) { + playerId = (await orm.users.get(participantId))?.playerId; + if (!playerId) { + throw new BadRequestError('User is not a participant'); + } + } + + const player = await orm.players.get(playerId); + + sql.transaction(async (tx) => { + const eloRefund = parseInt( + ( + await tx`SELECT elo_change FROM public.match_players WHERE match_id=${matchId.raw} AND player_id = ${playerId.raw}` + )?.[0]?.elo_change ?? 0, + ); + await tx`DELETE FROM match_players WHERE match_id=${matchId.raw} AND player_id=${playerId.raw}`; + if (!player.isRatingLocked) { + await tx`UPDATE players SET elo=${player.elo - eloRefund} WHERE id=${playerId.raw}`; + } + }); + return; + } } diff --git a/src/routes/matches.ts b/src/routes/matches.ts index 01517c5..bee4b92 100644 --- a/src/routes/matches.ts +++ b/src/routes/matches.ts @@ -7,5 +7,8 @@ export default { ':id': { GET: guard(matches.get, [Claims.ADMIN, Claims.MATCHES.OWNED.READ, Claims.MATCHES.PARTICIPANT.READ]), DELETE: guard(matches.drop, [Claims.ADMIN, Claims.MATCHES.OWNED.DELETE, Claims.USERS.SELF.UPDATE]), + leave: { + POST: guard(matches.leave, [Claims.MATCHES.PARTICIPANT.LEAVE]), + }, }, };