setup some race condition protection on update, host client now starts game when everyone is ready
This commit is contained in:
		
							
								
								
									
										13
									
								
								src/lib/ApiGameData.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/lib/ApiGameData.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					import { hasOnlyKeys, hasProperty } from "$lib/validation";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ApiGameData {
 | 
				
			||||||
 | 
						isStarted: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isApiGameData(target: unknown): target is ApiGameData {
 | 
				
			||||||
 | 
						if (!hasProperty(target, "isStarted", "boolean")) {
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return hasOnlyKeys(target, ["isStarted"]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -7,6 +7,7 @@ export interface Listing<T = unknown> {
 | 
				
			|||||||
	deleted: boolean;
 | 
						deleted: boolean;
 | 
				
			||||||
	modifiedAt: string | null;
 | 
						modifiedAt: string | null;
 | 
				
			||||||
	data: T;
 | 
						data: T;
 | 
				
			||||||
 | 
						version: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function isListing<T>(
 | 
					export function isListing<T>(
 | 
				
			||||||
@ -22,8 +23,11 @@ export function isListing<T>(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	if (!hasProperty(target, "deleted", "boolean")) return false;
 | 
						if (!hasProperty(target, "deleted", "boolean")) return false;
 | 
				
			||||||
	if (!hasProperty(target, "data", "object")) return false;
 | 
						if (!hasProperty(target, "data", "object")) return false;
 | 
				
			||||||
 | 
						if (!hasProperty(target, "version", "number")) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!hasOnlyKeys(target, ["id", "createdAt", "modifiedAt", "deleted", "data"]))
 | 
						if (
 | 
				
			||||||
 | 
							!hasOnlyKeys(target, ["id", "createdAt", "modifiedAt", "deleted", "data", "version"])
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return dataGuard?.((target as any)["data"]) ?? true;
 | 
						return dataGuard?.((target as any)["data"]) ?? true;
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,10 @@
 | 
				
			|||||||
	import type { GamePlayer } from "$lib/GamePlayer";
 | 
						import type { GamePlayer } from "$lib/GamePlayer";
 | 
				
			||||||
	import { getMeContext } from "$lib/meContext";
 | 
						import { getMeContext } from "$lib/meContext";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { players, withReadyStatus = false } = $props();
 | 
						const {
 | 
				
			||||||
 | 
							players,
 | 
				
			||||||
 | 
							withReadyStatus,
 | 
				
			||||||
 | 
						}: { players: GamePlayer[]; withReadyStatus: boolean } = $props();
 | 
				
			||||||
	const me = getMeContext();
 | 
						const me = getMeContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function getClasses(player: GamePlayer, withReadyStatus: boolean) {
 | 
						function getClasses(player: GamePlayer, withReadyStatus: boolean) {
 | 
				
			||||||
 | 
				
			|||||||
@ -43,11 +43,16 @@ export async function writeNewListing(col: ServerCollections, listing: Listing)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export async function writeUpdatedListing(col: ServerCollections, listing: Listing) {
 | 
					export async function writeUpdatedListing(col: ServerCollections, listing: Listing) {
 | 
				
			||||||
	const client = await getClient();
 | 
						const client = await getClient();
 | 
				
			||||||
 | 
						const previousVersion = listing.version;
 | 
				
			||||||
 | 
						listing.version++;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await client
 | 
						return await client
 | 
				
			||||||
		.db(DATABASE)
 | 
							.db(DATABASE)
 | 
				
			||||||
		.collection(col)
 | 
							.collection(col)
 | 
				
			||||||
		.replaceOne({ _id: new ObjectId(listing.id) }, fixListingForMongo(listing));
 | 
							.replaceOne(
 | 
				
			||||||
 | 
								{ _id: new ObjectId(listing.id), version: previousVersion },
 | 
				
			||||||
 | 
								fixListingForMongo(listing),
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function listCollection<T>(
 | 
					export async function listCollection<T>(
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										9
									
								
								src/lib/server/retry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/lib/server/retry.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					export async function retry(fn: () => Promise<[boolean, Response]>, retries = 3) {
 | 
				
			||||||
 | 
						let [retryable, response] = await fn(); // 1 try
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						while (retries-- && retryable) {
 | 
				
			||||||
 | 
							[retryable, response] = await fn(); // and 3 retries
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return response;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import { isGameData, type GameData } from "$lib/GameData";
 | 
					import { isApiGameData, type ApiGameData } from "$lib/ApiGameData";
 | 
				
			||||||
 | 
					import { type GameData } from "$lib/GameData";
 | 
				
			||||||
import { isListing } from "$lib/Listing";
 | 
					import { isListing } from "$lib/Listing";
 | 
				
			||||||
import { Game } from "$lib/server/Game";
 | 
					import { Game } from "$lib/server/Game";
 | 
				
			||||||
import { updateListing } from "$lib/server/modifyListing";
 | 
					import { updateListing } from "$lib/server/modifyListing";
 | 
				
			||||||
@ -7,13 +8,15 @@ import {
 | 
				
			|||||||
	ServerCollections,
 | 
						ServerCollections,
 | 
				
			||||||
	writeUpdatedListing,
 | 
						writeUpdatedListing,
 | 
				
			||||||
} from "$lib/server/mongo";
 | 
					} from "$lib/server/mongo";
 | 
				
			||||||
import { getBody, getParam, ResourceId } from "$lib/server/requestTools";
 | 
					import { getBody, getParam, getUser, ResourceId } from "$lib/server/requestTools";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
	badRequestResponse,
 | 
						badRequestResponse,
 | 
				
			||||||
	conflictResponse,
 | 
						conflictResponse,
 | 
				
			||||||
 | 
						forbiddenResponse,
 | 
				
			||||||
	notFoundResponse,
 | 
						notFoundResponse,
 | 
				
			||||||
	singleResponse,
 | 
						singleResponse,
 | 
				
			||||||
} from "$lib/server/responseBodies";
 | 
					} from "$lib/server/responseBodies";
 | 
				
			||||||
 | 
					import { retry } from "$lib/server/retry";
 | 
				
			||||||
import type { RequestHandler } from "@sveltejs/kit";
 | 
					import type { RequestHandler } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const GET: RequestHandler = async ({ params }): Promise<Response> => {
 | 
					export const GET: RequestHandler = async ({ params }): Promise<Response> => {
 | 
				
			||||||
@ -32,13 +35,18 @@ export const GET: RequestHandler = async ({ params }): Promise<Response> => {
 | 
				
			|||||||
	return singleResponse(game);
 | 
						return singleResponse(game);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const PUT: RequestHandler = async ({ request, params }): Promise<Response> => {
 | 
					export const PUT: RequestHandler = async ({
 | 
				
			||||||
 | 
						locals,
 | 
				
			||||||
 | 
						request,
 | 
				
			||||||
 | 
						params,
 | 
				
			||||||
 | 
					}): Promise<Response> => {
 | 
				
			||||||
	const id = getParam(params, ResourceId.Game);
 | 
						const id = getParam(params, ResourceId.Game);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!id) {
 | 
						if (!id) {
 | 
				
			||||||
		return badRequestResponse("missing gameid parameter");
 | 
							return badRequestResponse("missing gameid parameter");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const putItem = async (): Promise<[boolean, Response]> => {
 | 
				
			||||||
		const gameListing = await readListingById(
 | 
							const gameListing = await readListingById(
 | 
				
			||||||
			ServerCollections.Games,
 | 
								ServerCollections.Games,
 | 
				
			||||||
			id,
 | 
								id,
 | 
				
			||||||
@ -46,25 +54,50 @@ export const PUT: RequestHandler = async ({ request, params }): Promise<Response
 | 
				
			|||||||
		);
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!gameListing) {
 | 
							if (!gameListing) {
 | 
				
			||||||
		return notFoundResponse();
 | 
								return [false, notFoundResponse()];
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const game = Game.from(gameListing.data);
 | 
							const game = Game.from(gameListing.data);
 | 
				
			||||||
	const body = await getBody(request, isGameData);
 | 
					
 | 
				
			||||||
 | 
							let body: ApiGameData | null;
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								body = await getBody(request, isApiGameData);
 | 
				
			||||||
 | 
							} catch (err) {
 | 
				
			||||||
 | 
								return [false, badRequestResponse("malformed game data in body")];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!body) {
 | 
							if (!body) {
 | 
				
			||||||
		return badRequestResponse("missing game data in body");
 | 
								return [false, badRequestResponse("missing game data in body")];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const user = getUser(locals);
 | 
				
			||||||
 | 
							const userId = user.payload.sub;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (game.isStarted !== body.isStarted) {
 | 
				
			||||||
 | 
								if (game.isStarted) {
 | 
				
			||||||
 | 
									return [false, conflictResponse()];
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (game.players[0].id !== userId) {
 | 
				
			||||||
 | 
									return [false, forbiddenResponse()];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!game.isStarted) {
 | 
					 | 
				
			||||||
		if (body.isStarted) {
 | 
					 | 
				
			||||||
			game.start();
 | 
								game.start();
 | 
				
			||||||
		}
 | 
							} else {
 | 
				
			||||||
	} else if (game.isStarted !== body.isStarted) {
 | 
								return [false, singleResponse(gameListing)];
 | 
				
			||||||
		return conflictResponse();
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await writeUpdatedListing(ServerCollections.Games, updateListing(gameListing, game));
 | 
							const res = await writeUpdatedListing(
 | 
				
			||||||
 | 
								ServerCollections.Games,
 | 
				
			||||||
 | 
								updateListing(gameListing, game),
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return singleResponse(gameListing);
 | 
							if (!res.acknowledged || res.modifiedCount === 0) {
 | 
				
			||||||
 | 
								return [true, conflictResponse()];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return [false, singleResponse(gameListing)];
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return await retry(putItem, 200);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,7 @@ import {
 | 
				
			|||||||
	notFoundResponse,
 | 
						notFoundResponse,
 | 
				
			||||||
	singleResponse,
 | 
						singleResponse,
 | 
				
			||||||
} from "$lib/server/responseBodies";
 | 
					} from "$lib/server/responseBodies";
 | 
				
			||||||
 | 
					import { retry } from "$lib/server/retry";
 | 
				
			||||||
import type { RequestHandler } from "@sveltejs/kit";
 | 
					import type { RequestHandler } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const PUT: RequestHandler = async ({ params, request }): Promise<Response> => {
 | 
					export const PUT: RequestHandler = async ({ params, request }): Promise<Response> => {
 | 
				
			||||||
@ -35,19 +36,35 @@ export const PUT: RequestHandler = async ({ params, request }): Promise<Response
 | 
				
			|||||||
		return badRequestResponse("malformed request");
 | 
							return badRequestResponse("malformed request");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const listing = await readListingById(ServerCollections.Games, id, isListing<GameData>);
 | 
						const putItem = async (): Promise<[boolean, Response]> => {
 | 
				
			||||||
 | 
							const listing = await readListingById(
 | 
				
			||||||
 | 
								ServerCollections.Games,
 | 
				
			||||||
 | 
								id,
 | 
				
			||||||
 | 
								isListing<GameData>,
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!listing) {
 | 
							if (!listing) {
 | 
				
			||||||
		return notFoundResponse();
 | 
								return [false, notFoundResponse()];
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (listing.data.isStarted === true) {
 | 
							if (listing.data.isStarted === true) {
 | 
				
			||||||
		return conflictResponse();
 | 
								return [false, conflictResponse()];
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const game = Game.from(listing.data);
 | 
							const game = Game.from(listing.data);
 | 
				
			||||||
	if (game.setPlayerReady(player) === null) return notFoundResponse();
 | 
							if (game.setPlayerReady(player) === null) return [false, notFoundResponse()];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await writeUpdatedListing(ServerCollections.Games, updateListing(listing, game));
 | 
							const res = await writeUpdatedListing(
 | 
				
			||||||
 | 
								ServerCollections.Games,
 | 
				
			||||||
 | 
								updateListing(listing, game),
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return singleResponse(game);
 | 
							if (!res.acknowledged || res.modifiedCount === 0) {
 | 
				
			||||||
 | 
								return [true, conflictResponse()];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return [false, singleResponse(game)];
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return await retry(putItem);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -10,8 +10,6 @@ export const load: PageServerLoad = async ({ fetch, params, cookies, depends })
 | 
				
			|||||||
	let body: unknown;
 | 
						let body: unknown;
 | 
				
			||||||
	const token = cookies.get("access_token");
 | 
						const token = cookies.get("access_token");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	depends(url);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (!token) {
 | 
						if (!token) {
 | 
				
			||||||
		error(401);
 | 
							error(401);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,20 +1,23 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import { invalidate } from "$app/navigation";
 | 
					 | 
				
			||||||
	import PlayerList from "$lib/components/PlayerList.svelte";
 | 
						import PlayerList from "$lib/components/PlayerList.svelte";
 | 
				
			||||||
	import type { Me } from "$lib/me";
 | 
					 | 
				
			||||||
	import type { PageData } from "./$types";
 | 
						import type { PageData } from "./$types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let { data: page }: { data: PageData } = $props();
 | 
						let { data: page }: { data: PageData } = $props();
 | 
				
			||||||
	function getHandler(gameId: string, me: Me) {
 | 
						const players = $state(page.game.data.players);
 | 
				
			||||||
		return async () => {
 | 
						const allReady = $derived(players.every(({ isReady }) => isReady));
 | 
				
			||||||
			const playerIndex = page.game.data.players.findIndex(
 | 
						let isStarted = $state(page.game.data.isStarted);
 | 
				
			||||||
				({ id }) => id === page.me?.id,
 | 
						const meId = page.me?.id ?? "";
 | 
				
			||||||
			);
 | 
						const gameId = page.game.id;
 | 
				
			||||||
			const target = page.game.data.players[playerIndex];
 | 
						const token = page.token;
 | 
				
			||||||
			const update = { ...target, isReady: !target.isReady };
 | 
						const isHost = players.findIndex(({ id }) => id === meId) === 0;
 | 
				
			||||||
			let response = await fetch(`/api/games/${gameId}/players/${me.id}`, {
 | 
						const playerIndex = players.findIndex(({ id }) => id === meId);
 | 
				
			||||||
 | 
						const me = players[playerIndex];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function handleSetReady(isReady: boolean) {
 | 
				
			||||||
 | 
							const update = { ...me, isReady };
 | 
				
			||||||
 | 
							let response = await fetch(`/api/games/${gameId}/players/${meId}`, {
 | 
				
			||||||
			method: "PUT",
 | 
								method: "PUT",
 | 
				
			||||||
				headers: [["Authorization", page.token]],
 | 
								headers: [["Authorization", token]],
 | 
				
			||||||
			body: JSON.stringify(update),
 | 
								body: JSON.stringify(update),
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -22,25 +25,44 @@
 | 
				
			|||||||
			throw new Error("unable to set ready");
 | 
								throw new Error("unable to set ready");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			invalidate(`/api/games/${gameId}`);
 | 
							players[playerIndex] = update;
 | 
				
			||||||
		};
 | 
					
 | 
				
			||||||
 | 
							if (isHost) {
 | 
				
			||||||
 | 
								await startGameIfReady();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function startGameIfReady() {
 | 
				
			||||||
 | 
							if (allReady) {
 | 
				
			||||||
 | 
								const res = await fetch(`/api/games/${gameId}`, {
 | 
				
			||||||
 | 
									method: "PUT",
 | 
				
			||||||
 | 
									headers: [["Authorization", token]],
 | 
				
			||||||
 | 
									body: JSON.stringify({ isStarted: true }),
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (res.status !== 200) {
 | 
				
			||||||
 | 
									await handleSetReady(false);
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									isStarted = true;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#if page.game.data.isStarted}
 | 
					{#if isStarted}
 | 
				
			||||||
	<h1>This is game {page.game.id}</h1>
 | 
						<h1>This is game {page.game.id}</h1>
 | 
				
			||||||
{:else}
 | 
					{:else}
 | 
				
			||||||
	<h1>This is some lobby</h1>
 | 
						<h1>This is some lobby</h1>
 | 
				
			||||||
	<div id="lobby">
 | 
						<div id="lobby">
 | 
				
			||||||
		<div id="players">
 | 
							<div id="players">
 | 
				
			||||||
			<div id="player-list">
 | 
								<div id="player-list">
 | 
				
			||||||
				<PlayerList players={page.game.data.players} withReadyStatus />
 | 
									<PlayerList {players} withReadyStatus />
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<input
 | 
								<input
 | 
				
			||||||
				id="ready"
 | 
									id="ready"
 | 
				
			||||||
				type="button"
 | 
									type="button"
 | 
				
			||||||
				value="Set Ready"
 | 
									value="Set Ready"
 | 
				
			||||||
				onclick={getHandler(page.game.id, page.me!)}
 | 
									onclick={() => handleSetReady(!me.isReady)}
 | 
				
			||||||
			/>
 | 
								/>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div id="chat" style="background: palevioletred">
 | 
							<div id="chat" style="background: palevioletred">
 | 
				
			||||||
 | 
				
			|||||||
@ -11,30 +11,29 @@ Authorization: Bearer {{token}}
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
###
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PUT https://localhost:5173/api/games/67b39573a0fcb80dd13f2c8b
 | 
				
			||||||
 | 
					Accept: application/json
 | 
				
			||||||
 | 
					Content-Type: application/json
 | 
				
			||||||
 | 
					Authorization: Bearer {{token}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    "isStarted": true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
POST https://localhost:5173/api/games
 | 
					POST https://localhost:5173/api/games
 | 
				
			||||||
Accept: application/json
 | 
					Accept: application/json
 | 
				
			||||||
Authorization: Bearer {{token}}
 | 
					Authorization: Bearer {{token}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
###
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
GET https://localhost:5173/api/games/6790386c4a41c3599d47d986
 | 
					GET https://localhost:5173/api/games/67b39573a0fcb80dd13f2c8b
 | 
				
			||||||
Accept: application/json
 | 
					Accept: application/json
 | 
				
			||||||
Authorization: Bearer {{token}}
 | 
					Authorization: Bearer {{token}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
###
 | 
					###
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
PUT https://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3
 | 
					 | 
				
			||||||
Accept: application/json
 | 
					 | 
				
			||||||
Content-Type: application/json
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    "state": {},
 | 
					 | 
				
			||||||
    "isStarted": true,
 | 
					 | 
				
			||||||
    "players": ["2", "45", "10"]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
###
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
POST https://localhost:5173/api/games/67b39573a0fcb80dd13f2c8b/turns
 | 
					POST https://localhost:5173/api/games/67b39573a0fcb80dd13f2c8b/turns
 | 
				
			||||||
Accept: application/json
 | 
					Accept: application/json
 | 
				
			||||||
Content-Type: application/json
 | 
					Content-Type: application/json
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user