add turns and update token logic.
This commit is contained in:
		@ -4,7 +4,7 @@ import type { State } from "./State";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export interface GameData {
 | 
					export interface GameData {
 | 
				
			||||||
	isStarted: boolean;
 | 
						isStarted: boolean;
 | 
				
			||||||
	players: Id[];
 | 
						players: { id: Id; username: string }[];
 | 
				
			||||||
	state: State;
 | 
						state: State;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -17,7 +17,11 @@ export function isGameData(target: unknown): target is GameData {
 | 
				
			|||||||
		const { players } = target as any;
 | 
							const { players } = target as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for (const player of players) {
 | 
							for (const player of players) {
 | 
				
			||||||
			if (!isId(player)) {
 | 
								if (!isId(player.id)) {
 | 
				
			||||||
 | 
									return false;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (!hasProperty(player, "username", "string")) {
 | 
				
			||||||
				return false;
 | 
									return false;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -98,10 +98,8 @@ export class SeatPlayers implements GameEvent {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * RollForFirst takes a player index and a value which represents the pips on the die
 | 
					 * RollForFirst takes a player index and a value which represents the pips on the die
 | 
				
			||||||
 * that the player rolled. It represents and attempt to roll the highest die and go
 | 
					 * that the player rolled. It represents an attempt to roll the highest die and go
 | 
				
			||||||
 * first.
 | 
					 * first.
 | 
				
			||||||
 *axpected to re-roll as a tie breaker. Re-rolling continues until someone
 | 
					 | 
				
			||||||
 * wins.
 | 
					 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export class RollForFirst implements GameEvent {
 | 
					export class RollForFirst implements GameEvent {
 | 
				
			||||||
	kind: GameEventKind.RollForFirst;
 | 
						kind: GameEventKind.RollForFirst;
 | 
				
			||||||
@ -279,7 +277,7 @@ export class Hold implements GameEvent {
 | 
				
			|||||||
			// Detect two threes of a kind: if the number of held values was six, and the
 | 
								// Detect two threes of a kind: if the number of held values was six, and the
 | 
				
			||||||
			// set of unique values is two, then there MUST have been two threes of a
 | 
								// set of unique values is two, then there MUST have been two threes of a
 | 
				
			||||||
			// kind.
 | 
								// kind.
 | 
				
			||||||
			total = 1_500;
 | 
								total = 1_600;
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			// A player can use a "push" if they are using every one of their rolled
 | 
								// A player can use a "push" if they are using every one of their rolled
 | 
				
			||||||
			// dice.
 | 
								// dice.
 | 
				
			||||||
 | 
				
			|||||||
@ -1,19 +1,11 @@
 | 
				
			|||||||
import { ObjectId } from "mongodb";
 | 
					import { ObjectId } from "mongodb";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Id = ObjectId;
 | 
					export type Id = string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function createId(): Id {
 | 
					export function createId(): Id {
 | 
				
			||||||
	return new ObjectId();
 | 
						return new ObjectId().toString();
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function idFromString(str: string) {
 | 
					 | 
				
			||||||
	return new ObjectId(str);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function stringFromId(id: Id) {
 | 
					 | 
				
			||||||
	return id.toString();
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function isId(target: unknown): target is Id {
 | 
					export function isId(target: unknown): target is Id {
 | 
				
			||||||
	return target instanceof ObjectId;
 | 
						return typeof target === "string";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ import { hasProperty } from "$lib/validation";
 | 
				
			|||||||
export type ServerResponse =
 | 
					export type ServerResponse =
 | 
				
			||||||
	| { item: Listing<unknown> }
 | 
						| { item: Listing<unknown> }
 | 
				
			||||||
	| { items: Listing<unknown>[] }
 | 
						| { items: Listing<unknown>[] }
 | 
				
			||||||
 | 
						| { access_token: string }
 | 
				
			||||||
	| { error: string };
 | 
						| { error: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function isServerResponse(target: unknown): target is ServerResponse {
 | 
					export function isServerResponse(target: unknown): target is ServerResponse {
 | 
				
			||||||
@ -14,5 +15,6 @@ export function isServerResponse(target: unknown): target is ServerResponse {
 | 
				
			|||||||
	if (hasProperty(target, "item", "object")) return true;
 | 
						if (hasProperty(target, "item", "object")) return true;
 | 
				
			||||||
	if (hasProperty(target, "items", "object[]")) return true;
 | 
						if (hasProperty(target, "items", "object[]")) return true;
 | 
				
			||||||
	if (hasProperty(target, "error", "string")) return true;
 | 
						if (hasProperty(target, "error", "string")) return true;
 | 
				
			||||||
 | 
						if (hasProperty(target, "access_token", "string")) return true;
 | 
				
			||||||
	return false;
 | 
						return false;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										32
									
								
								src/lib/components/PlayerList.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/lib/components/PlayerList.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					<script>
 | 
				
			||||||
 | 
						import { getMeContext } from "$lib/meContext";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { players } = $props();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const me = getMeContext();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="list">
 | 
				
			||||||
 | 
						<ol>
 | 
				
			||||||
 | 
							{#each players as player}
 | 
				
			||||||
 | 
								{#if me !== null && player.id === me.id}
 | 
				
			||||||
 | 
									<li class="you">you</li>
 | 
				
			||||||
 | 
								{:else}
 | 
				
			||||||
 | 
									<li>{player.username}</li>
 | 
				
			||||||
 | 
								{/if}
 | 
				
			||||||
 | 
							{/each}
 | 
				
			||||||
 | 
						</ol>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						.you {
 | 
				
			||||||
 | 
							font-weight: bold;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.list {
 | 
				
			||||||
 | 
							border: 1pt gray solid;
 | 
				
			||||||
 | 
							background: white;
 | 
				
			||||||
 | 
							height: 100%;
 | 
				
			||||||
 | 
							width: 100%;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										14
									
								
								src/lib/me.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/lib/me.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					import { hasProperty } from "./validation";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface Me {
 | 
				
			||||||
 | 
						id: string;
 | 
				
			||||||
 | 
						role: string;
 | 
				
			||||||
 | 
						username: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isMe(target: unknown): target is Me {
 | 
				
			||||||
 | 
						if (!hasProperty(target, "id", "string")) return false;
 | 
				
			||||||
 | 
						if (!hasProperty(target, "role", "string")) return false;
 | 
				
			||||||
 | 
						if (!hasProperty(target, "username", "string")) return false;
 | 
				
			||||||
 | 
						return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								src/lib/meContext.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/lib/meContext.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					import { getContext, hasContext, setContext } from "svelte";
 | 
				
			||||||
 | 
					import type { Me } from "./me";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function setMeContext(me: Me) {
 | 
				
			||||||
 | 
						setContext("me", me);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getMeContext(): Me | null {
 | 
				
			||||||
 | 
						if (hasContext("me")) {
 | 
				
			||||||
 | 
							return getContext("me");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -3,7 +3,7 @@ import type { GameData } from "../GameData";
 | 
				
			|||||||
import type { State } from "../State";
 | 
					import type { State } from "../State";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class Game implements GameData {
 | 
					export class Game implements GameData {
 | 
				
			||||||
	players: Id[];
 | 
						players: { id: Id; username: string }[];
 | 
				
			||||||
	isStarted: boolean;
 | 
						isStarted: boolean;
 | 
				
			||||||
	state: State;
 | 
						state: State;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -13,8 +13,17 @@ export class Game implements GameData {
 | 
				
			|||||||
		this.state = {};
 | 
							this.state = {};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addPlayer(id: Id) {
 | 
						static from(data: GameData) {
 | 
				
			||||||
		this.players.push(id);
 | 
							const game = new Game();
 | 
				
			||||||
 | 
							game.players = data.players;
 | 
				
			||||||
 | 
							game.isStarted = data.isStarted;
 | 
				
			||||||
 | 
							game.state = data.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return game;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						addPlayer(id: Id, username: string) {
 | 
				
			||||||
 | 
							this.players.push({ id, username });
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	start() {
 | 
						start() {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										15
									
								
								src/lib/server/ServerJwtPayload.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/lib/server/ServerJwtPayload.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					import * as jwt from "jsonwebtoken";
 | 
				
			||||||
 | 
					import { hasProperty } from "../validation";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ServerJwtPayload = jwt.JwtPayload & {
 | 
				
			||||||
 | 
						username: string;
 | 
				
			||||||
 | 
						role: string;
 | 
				
			||||||
 | 
						sub: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isServerJwtPayload(target: unknown): target is ServerJwtPayload {
 | 
				
			||||||
 | 
						if (!hasProperty(target, "username", "string")) return false;
 | 
				
			||||||
 | 
						if (!hasProperty(target, "role", "string")) return false;
 | 
				
			||||||
 | 
						if (!hasProperty(target, "sub", "string")) return false;
 | 
				
			||||||
 | 
						return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -3,10 +3,15 @@ import jwt from "jsonwebtoken";
 | 
				
			|||||||
import { type Method, type RouteAuthRule } from "./routeAuth";
 | 
					import { type Method, type RouteAuthRule } from "./routeAuth";
 | 
				
			||||||
import type { Listing } from "$lib/Listing";
 | 
					import type { Listing } from "$lib/Listing";
 | 
				
			||||||
import type { LoginData } from "$lib/Login";
 | 
					import type { LoginData } from "$lib/Login";
 | 
				
			||||||
 | 
					import { isServerJwtPayload, type ServerJwtPayload } from "./ServerJwtPayload";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type LocalCredentials = (
 | 
					export type LocalCredentials = (
 | 
				
			||||||
	| { kind: "Basic"; payload: { username: string; password: string } }
 | 
						| { kind: "Basic"; payload: { username: string; password: string } }
 | 
				
			||||||
	| { kind: "Bearer"; payload: jwt.JwtPayload | string }
 | 
						| {
 | 
				
			||||||
 | 
								kind: "Bearer";
 | 
				
			||||||
 | 
								payload: ServerJwtPayload;
 | 
				
			||||||
 | 
								role: string;
 | 
				
			||||||
 | 
						  }
 | 
				
			||||||
	| { kind: "None" }
 | 
						| { kind: "None" }
 | 
				
			||||||
) & { role: string };
 | 
					) & { role: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -17,13 +22,15 @@ export enum AuthorizationResult {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function createToken(listing: Listing<LoginData>, secret: string) {
 | 
					export async function createToken(listing: Listing<LoginData>, secret: string) {
 | 
				
			||||||
	return await jwt.sign(
 | 
						const serverPayload: ServerJwtPayload = {
 | 
				
			||||||
		{ sub: listing.id, username: listing.data.username, role: listing.data.role },
 | 
							sub: listing.id,
 | 
				
			||||||
		secret,
 | 
							username: listing.data.username,
 | 
				
			||||||
		{
 | 
							role: listing.data.role,
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return await jwt.sign(serverPayload, secret, {
 | 
				
			||||||
		expiresIn: "1d",
 | 
							expiresIn: "1d",
 | 
				
			||||||
		},
 | 
						});
 | 
				
			||||||
	);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function authenticate(
 | 
					export async function authenticate(
 | 
				
			||||||
@ -31,7 +38,6 @@ export async function authenticate(
 | 
				
			|||||||
	jwtSecret: string,
 | 
						jwtSecret: string,
 | 
				
			||||||
): Promise<LocalCredentials | null> {
 | 
					): Promise<LocalCredentials | null> {
 | 
				
			||||||
	const authHeader = event.request.headers.get("authorization");
 | 
						const authHeader = event.request.headers.get("authorization");
 | 
				
			||||||
	let tokenKind: "Basic" | "Bearer" | "None";
 | 
					 | 
				
			||||||
	let tokenRole: string;
 | 
						let tokenRole: string;
 | 
				
			||||||
	let tokenDesc: LocalCredentials;
 | 
						let tokenDesc: LocalCredentials;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -39,27 +45,24 @@ export async function authenticate(
 | 
				
			|||||||
	if (!authHeader) {
 | 
						if (!authHeader) {
 | 
				
			||||||
		// This is a stranger: they have no token and they will be assigned the default
 | 
							// This is a stranger: they have no token and they will be assigned the default
 | 
				
			||||||
		// role.
 | 
							// role.
 | 
				
			||||||
		tokenKind = "None";
 | 
					 | 
				
			||||||
		tokenRole = "default";
 | 
							tokenRole = "default";
 | 
				
			||||||
		tokenDesc = { kind: "None", role: tokenRole };
 | 
							tokenDesc = { kind: "None", role: tokenRole };
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		const [kind, token] = authHeader.split(" ");
 | 
							const [kind, token] = authHeader.split(" ");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (kind === "Bearer") {
 | 
							if (kind === "Bearer") {
 | 
				
			||||||
			tokenKind = "Bearer";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// The role can be derived from the JWT token.
 | 
								// The role can be derived from the JWT token.
 | 
				
			||||||
			const payload = await jwt.verify(token, jwtSecret);
 | 
								const payload = await jwt.verify(token, jwtSecret);
 | 
				
			||||||
			if (typeof payload === "string") {
 | 
								if (!isServerJwtPayload(payload)) {
 | 
				
			||||||
				// I do not assign, and don't know what to do with, these kinds of
 | 
					 | 
				
			||||||
				// tokens. Perhaps an error should be logged here, since this is a
 | 
					 | 
				
			||||||
				// weird thing to have stumbled on.
 | 
					 | 
				
			||||||
				return null;
 | 
									return null;
 | 
				
			||||||
				// user should have a bearer token
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			tokenRole = payload.role;
 | 
								tokenRole = payload.role;
 | 
				
			||||||
			tokenDesc = { kind: "Bearer", payload, role: tokenRole };
 | 
								tokenDesc = {
 | 
				
			||||||
 | 
									kind: "Bearer",
 | 
				
			||||||
 | 
									payload,
 | 
				
			||||||
 | 
									role: tokenRole,
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!tokenRole) {
 | 
								if (!tokenRole) {
 | 
				
			||||||
				// Something has gone wrong: I should not have issued a token without a
 | 
									// Something has gone wrong: I should not have issued a token without a
 | 
				
			||||||
@ -70,7 +73,6 @@ export async function authenticate(
 | 
				
			|||||||
			const decoded = Buffer.from(token, "base64").toString("ascii");
 | 
								const decoded = Buffer.from(token, "base64").toString("ascii");
 | 
				
			||||||
			const [username, password] = decoded.split(":");
 | 
								const [username, password] = decoded.split(":");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			tokenKind = "Basic";
 | 
					 | 
				
			||||||
			tokenRole = "default";
 | 
								tokenRole = "default";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			tokenDesc = { kind: "Basic", payload: { username, password }, role: tokenRole };
 | 
								tokenDesc = { kind: "Basic", payload: { username, password }, role: tokenRole };
 | 
				
			||||||
@ -95,6 +97,13 @@ export function isAuthorized(
 | 
				
			|||||||
	const { role: tokenRole, kind: tokenKind } = creds;
 | 
						const { role: tokenRole, kind: tokenKind } = creds;
 | 
				
			||||||
	const rules = roleRules[tokenRole];
 | 
						const rules = roleRules[tokenRole];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (parts[0] !== "api") {
 | 
				
			||||||
 | 
							// All the routes for the backed server are prefixed with api, and the backed
 | 
				
			||||||
 | 
							// server is where all the authorization is going to happen so we will ignore
 | 
				
			||||||
 | 
							// calls to the frontend.
 | 
				
			||||||
 | 
							return AuthorizationResult.Allowed;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let hasMatchingAllow = false;
 | 
						let hasMatchingAllow = false;
 | 
				
			||||||
	for (const rule of rules) {
 | 
						for (const rule of rules) {
 | 
				
			||||||
		if (matchesRequest(parts, method, rule)) {
 | 
							if (matchesRequest(parts, method, rule)) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,16 +0,0 @@
 | 
				
			|||||||
export async function getRequestBody<T = unknown>(
 | 
					 | 
				
			||||||
	req: Request,
 | 
					 | 
				
			||||||
	validation?: (target: unknown) => target is T,
 | 
					 | 
				
			||||||
): Promise<T> {
 | 
					 | 
				
			||||||
	if (req.body === null) {
 | 
					 | 
				
			||||||
		throw new Error("no body is present on the request");
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const body = await req.json();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (validation && !validation(body)) {
 | 
					 | 
				
			||||||
		throw new Error("body validation failed");
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return body;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import type { Id } from "$lib/Id";
 | 
				
			||||||
import type { Listing } from "$lib/Listing";
 | 
					import type { Listing } from "$lib/Listing";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
	MongoClient,
 | 
						MongoClient,
 | 
				
			||||||
@ -6,10 +7,15 @@ import {
 | 
				
			|||||||
	type Document,
 | 
						type Document,
 | 
				
			||||||
	type WithId,
 | 
						type WithId,
 | 
				
			||||||
} from "mongodb";
 | 
					} from "mongodb";
 | 
				
			||||||
import type { Id } from "../Id";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ListingFromMongo = Omit<Listing, "id"> & { _id: ObjectId };
 | 
					type ListingFromMongo = Omit<Listing, "id"> & { _id: ObjectId };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum ServerCollections {
 | 
				
			||||||
 | 
						Games = "games",
 | 
				
			||||||
 | 
						Logins = "logins",
 | 
				
			||||||
 | 
						Turns = "turns",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const uri = `mongodb://127.0.0.1:27017`;
 | 
					const uri = `mongodb://127.0.0.1:27017`;
 | 
				
			||||||
const DATABASE = "ten-thousand";
 | 
					const DATABASE = "ten-thousand";
 | 
				
			||||||
let cachedClient: MongoClient | null = null;
 | 
					let cachedClient: MongoClient | null = null;
 | 
				
			||||||
@ -29,14 +35,23 @@ async function getClient() {
 | 
				
			|||||||
	return c;
 | 
						return c;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function writeListing(col: string, listing: Listing) {
 | 
					export async function writeNewListing(col: ServerCollections, listing: Listing) {
 | 
				
			||||||
	const client = await getClient();
 | 
						const client = await getClient();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await client.db(DATABASE).collection(col).insertOne(fixListingForMongo(listing));
 | 
						await client.db(DATABASE).collection(col).insertOne(fixListingForMongo(listing));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function writeUpdatedListing(col: ServerCollections, listing: Listing) {
 | 
				
			||||||
 | 
						const client = await getClient();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await client
 | 
				
			||||||
 | 
							.db(DATABASE)
 | 
				
			||||||
 | 
							.collection(col)
 | 
				
			||||||
 | 
							.replaceOne({ _id: new ObjectId(listing.id) }, fixListingForMongo(listing));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function listCollection<T>(
 | 
					export async function listCollection<T>(
 | 
				
			||||||
	col: string,
 | 
						col: ServerCollections,
 | 
				
			||||||
	dataGuard: (target: unknown) => target is Listing<T>,
 | 
						dataGuard: (target: unknown) => target is Listing<T>,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
	const client = await getClient();
 | 
						const client = await getClient();
 | 
				
			||||||
@ -46,19 +61,30 @@ export async function listCollection<T>(
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function readListingById<T>(
 | 
					export async function readListingById<T>(
 | 
				
			||||||
	col: string,
 | 
						col: ServerCollections,
 | 
				
			||||||
	id: Id,
 | 
						id: Id,
 | 
				
			||||||
	dataGuard: (target: unknown) => target is Listing<T>,
 | 
						dataGuard: (target: unknown) => target is Listing<T>,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
	const client = await getClient();
 | 
						const client = await getClient();
 | 
				
			||||||
	const res = await client.db(DATABASE).collection(col).findOne({ _id: id });
 | 
						const res = await client
 | 
				
			||||||
 | 
							.db(DATABASE)
 | 
				
			||||||
 | 
							.collection(col)
 | 
				
			||||||
 | 
							.findOne({ _id: idFromString(id) });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (res === null) return null;
 | 
						if (res === null) return null;
 | 
				
			||||||
	return fixListingFromMongo(res, dataGuard);
 | 
						return fixListingFromMongo(res, dataGuard);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function idFromString(str: string) {
 | 
				
			||||||
 | 
						return new ObjectId(str);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function stringFromId(id: ObjectId) {
 | 
				
			||||||
 | 
						return id.toString();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function readListingByQuery<T>(
 | 
					export async function readListingByQuery<T>(
 | 
				
			||||||
	col: string,
 | 
						col: ServerCollections,
 | 
				
			||||||
	query: object,
 | 
						query: object,
 | 
				
			||||||
	dataGuard: (target: unknown) => target is Listing<T>,
 | 
						dataGuard: (target: unknown) => target is Listing<T>,
 | 
				
			||||||
): Promise<Listing<T> | null> {
 | 
					): Promise<Listing<T> | null> {
 | 
				
			||||||
@ -73,7 +99,7 @@ export async function readListingByQuery<T>(
 | 
				
			|||||||
function fixListingForMongo(listing: Listing): ListingFromMongo {
 | 
					function fixListingForMongo(listing: Listing): ListingFromMongo {
 | 
				
			||||||
	const { id, ...rest } = listing;
 | 
						const { id, ...rest } = listing;
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		_id: id,
 | 
							_id: idFromString(id),
 | 
				
			||||||
		...rest,
 | 
							...rest,
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -83,7 +109,7 @@ function fixListingFromMongo<T>(
 | 
				
			|||||||
	dataGuard: (target: unknown) => target is Listing<T>,
 | 
						dataGuard: (target: unknown) => target is Listing<T>,
 | 
				
			||||||
): Listing<T> {
 | 
					): Listing<T> {
 | 
				
			||||||
	const { _id, ...rest } = target;
 | 
						const { _id, ...rest } = target;
 | 
				
			||||||
	const adjusted = { id: _id, ...rest };
 | 
						const adjusted = { id: stringFromId(_id), ...rest };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!dataGuard(adjusted)) {
 | 
						if (!dataGuard(adjusted)) {
 | 
				
			||||||
		throw new Error("the returned document does not conform to the provided type");
 | 
							throw new Error("the returned document does not conform to the provided type");
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										46
									
								
								src/lib/server/requestTools.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/lib/server/requestTools.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					import type { LocalCredentials } from "./auth";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum ResourceId {
 | 
				
			||||||
 | 
						Game = "gameid",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getUser(locals: { user: LocalCredentials }) {
 | 
				
			||||||
 | 
						// SvelteKit screws up this type somehow, and it's important to explicitely set it
 | 
				
			||||||
 | 
						// here.
 | 
				
			||||||
 | 
						const user: LocalCredentials = locals.user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (user.kind !== "Bearer") {
 | 
				
			||||||
 | 
							throw new Error("bad user information");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return user;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getParam(params: Partial<Record<string, string>>, resource: ResourceId) {
 | 
				
			||||||
 | 
						const id = params[resource];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!id) {
 | 
				
			||||||
 | 
							return null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return id;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getBody<T>(
 | 
				
			||||||
 | 
						request: Request,
 | 
				
			||||||
 | 
						dataGuard: (body: unknown) => body is T,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						let body: unknown;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							body = await request.json();
 | 
				
			||||||
 | 
						} catch (err) {
 | 
				
			||||||
 | 
							return null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!dataGuard(body)) {
 | 
				
			||||||
 | 
							throw new Error("data guard failed");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return body;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -6,6 +6,14 @@ export function singleResponse(item: unknown) {
 | 
				
			|||||||
	return Response.json({ item });
 | 
						return Response.json({ item });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createdResponse(id: string) {
 | 
				
			||||||
 | 
						return Response.json({ item: id }, { status: 201 });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function tokenResponse(token: string) {
 | 
				
			||||||
 | 
						return Response.json({ access_token: token });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function badRequestResponse(error: string = "Bad Request") {
 | 
					export function badRequestResponse(error: string = "Bad Request") {
 | 
				
			||||||
	return Response.json({ error }, { status: 400 });
 | 
						return Response.json({ error }, { status: 400 });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -420,6 +420,20 @@ describe("auth", () => {
 | 
				
			|||||||
					result: AuthorizationResult.Unauthenticated,
 | 
										result: AuthorizationResult.Unauthenticated,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									heading: "allows a user to hit any endpoint that doesn't start with api",
 | 
				
			||||||
 | 
									conditions: {
 | 
				
			||||||
 | 
										token: {
 | 
				
			||||||
 | 
											kind: "None",
 | 
				
			||||||
 | 
											role: "default",
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										method: "GET",
 | 
				
			||||||
 | 
										path: "/resource",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									expectations: {
 | 
				
			||||||
 | 
										result: AuthorizationResult.Allowed,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				heading: "correctly matches a route with a trailing /",
 | 
									heading: "correctly matches a route with a trailing /",
 | 
				
			||||||
				conditions: {
 | 
									conditions: {
 | 
				
			||||||
 | 
				
			|||||||
@ -39,9 +39,14 @@ describe("ServerResponse", () => {
 | 
				
			|||||||
				error: "something is wrong",
 | 
									error: "something is wrong",
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const accessTokenResponse = {
 | 
				
			||||||
 | 
									access_token: "Bearer somekindoftokenfromtheserver",
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			expect(isServerResponse(singleServerResponse)).to.be.true;
 | 
								expect(isServerResponse(singleServerResponse)).to.be.true;
 | 
				
			||||||
			expect(isServerResponse(listServerResponse)).to.be.true;
 | 
								expect(isServerResponse(listServerResponse)).to.be.true;
 | 
				
			||||||
			expect(isServerResponse(errorServerResponse)).to.be.true;
 | 
								expect(isServerResponse(errorServerResponse)).to.be.true;
 | 
				
			||||||
 | 
								expect(isServerResponse(accessTokenResponse)).to.be.true;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										49
									
								
								src/routes/+layout.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/routes/+layout.server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					import { isServerResponse } from "$lib/ServerResponse";
 | 
				
			||||||
 | 
					import { redirect, type ServerLoad } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					import { isMe, type Me } from "$lib/me";
 | 
				
			||||||
 | 
					import { setMeContext } from "$lib/meContext";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const load: ServerLoad = async ({ request, cookies, fetch }) => {
 | 
				
			||||||
 | 
						const url = new URL(request.url);
 | 
				
			||||||
 | 
						const token = cookies.get("access_token");
 | 
				
			||||||
 | 
						let me: Me | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (token) {
 | 
				
			||||||
 | 
							const res = await fetch("/api/me", {
 | 
				
			||||||
 | 
								headers: [["authorization", token]],
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const body = await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!isServerResponse(body)) {
 | 
				
			||||||
 | 
								throw new Error("missing or malformed body");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!("item" in body)) {
 | 
				
			||||||
 | 
								throw new Error("expected to receive an item");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const item = body.item;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!isMe(item)) {
 | 
				
			||||||
 | 
								throw new Error("expected to receive users 'me' object");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							me = item;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!token && url.pathname !== "/login" && url.pathname !== "/login/__data.json") {
 | 
				
			||||||
 | 
							console.log("REDIRECTING", url.pathname);
 | 
				
			||||||
 | 
							const baseUrl = `${url.protocol}${url.host}`;
 | 
				
			||||||
 | 
							const loginUrl = new URL("login", baseUrl);
 | 
				
			||||||
 | 
							loginUrl.searchParams.set("to", url.pathname);
 | 
				
			||||||
 | 
							redirect(302, loginUrl);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							console.log("no need to redirect");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							token,
 | 
				
			||||||
 | 
							me,
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/routes/+layout.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/routes/+layout.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { setMeContext } from "$lib/meContext.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let { children, data } = $props();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const me = data.me;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (me) {
 | 
				
			||||||
 | 
							setMeContext(me);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<main>
 | 
				
			||||||
 | 
						{@render children()}
 | 
				
			||||||
 | 
					</main>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						main {
 | 
				
			||||||
 | 
							max-width: 60rem;
 | 
				
			||||||
 | 
							margin: auto;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@ -1,2 +1,73 @@
 | 
				
			|||||||
<h1>Welcome to SvelteKit</h1>
 | 
					<h1>Play Ten Thousand!</h1>
 | 
				
			||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
 | 
					<a href="/games">Find a game</a>
 | 
				
			||||||
 | 
					<h2>What is Ten Thousand?</h2>
 | 
				
			||||||
 | 
					<p>
 | 
				
			||||||
 | 
						Ten Thousand is a dice game where players take turns rolling dice and scoring points to
 | 
				
			||||||
 | 
						try to reach 10,000 points.
 | 
				
			||||||
 | 
					</p>
 | 
				
			||||||
 | 
					<h2>How do I play?</h2>
 | 
				
			||||||
 | 
					<p>
 | 
				
			||||||
 | 
						The game starts with each player rolling to see who goes first. Each player then takes
 | 
				
			||||||
 | 
						turns rolling up to six dice, putting aside any scoring dice. Each time a player scores,
 | 
				
			||||||
 | 
						they can roll again to try to score more. Be careful, however, because if a player rolls
 | 
				
			||||||
 | 
						and doesn't score any points, they lose all the points they have scored during their
 | 
				
			||||||
 | 
						turn and their turn ends.
 | 
				
			||||||
 | 
					</p>
 | 
				
			||||||
 | 
					<p>
 | 
				
			||||||
 | 
						There are many variations to this game, this server uses the variation that I was taught
 | 
				
			||||||
 | 
						and have played many times, often obnoxiously loudly, at bars.
 | 
				
			||||||
 | 
					</p>
 | 
				
			||||||
 | 
					<h3>Basic Rules</h3>
 | 
				
			||||||
 | 
					<p>
 | 
				
			||||||
 | 
						Players start with six dice. Each die or dice they chose to score with, they put aside
 | 
				
			||||||
 | 
						and then roll with all the remaining dice. If all the dice have been rolled and set
 | 
				
			||||||
 | 
						aside for scoring, the player gets to re-roll all the dice, keeping whatever score they
 | 
				
			||||||
 | 
						already rolled up to that point. The player does not have to set aside scoring dice, and
 | 
				
			||||||
 | 
						can opt instead to re-roll them, hoping for a better score.
 | 
				
			||||||
 | 
					</p>
 | 
				
			||||||
 | 
					<p>
 | 
				
			||||||
 | 
						Once a player decides to hold, all the points they scored during their turn officially
 | 
				
			||||||
 | 
						get added to their score. A player can hold with as many or as few points as they like
 | 
				
			||||||
 | 
						with two exceptions: if a player <em>can</em> roll all six dice, they <em>must</em> roll
 | 
				
			||||||
 | 
						them; and the first time a player holds for points, their score <em>must</em> be worth at
 | 
				
			||||||
 | 
						least 1,000 points.
 | 
				
			||||||
 | 
					</p>
 | 
				
			||||||
 | 
					<p>
 | 
				
			||||||
 | 
						The final special scenario is called a "push." If a player has exactly two non-scoring
 | 
				
			||||||
 | 
						dice remaining, and those dice make a two of a kind (⚃ ⚃) then the player
 | 
				
			||||||
 | 
						takes those as if they were scoring dice worth zero points. Pushes can occur if a player
 | 
				
			||||||
 | 
						has decided to score all other dice which they have just rolled, and the dice they have
 | 
				
			||||||
 | 
						remaining are a push, or if the player has rolled two dice, and that roll has resulted
 | 
				
			||||||
 | 
						in a two of a kind. If a player has a push, they <em>must</em> take it, and since a push
 | 
				
			||||||
 | 
						can only result in six dice being available to roll again, a player who has rolled a push
 | 
				
			||||||
 | 
						must also roll again.
 | 
				
			||||||
 | 
					</p>
 | 
				
			||||||
 | 
					<p></p>
 | 
				
			||||||
 | 
					<p>
 | 
				
			||||||
 | 
						Once a player has hit 10,000, each other player gets one more turn. At the end, the
 | 
				
			||||||
 | 
						player with the most points (not necessarily the player who hit 10,000 first) wins.
 | 
				
			||||||
 | 
					</p>
 | 
				
			||||||
 | 
					<h3>Scoring</h3>
 | 
				
			||||||
 | 
					<ol>
 | 
				
			||||||
 | 
						<li>Ones (⚀) can be scored on their own for 100 points</li>
 | 
				
			||||||
 | 
						<li>Fives (⚄) can be scored on their own for 50 points</li>
 | 
				
			||||||
 | 
						<li>
 | 
				
			||||||
 | 
							Three of a kind (⚁ ⚁ ⚁) can be held for their face value, x100,
 | 
				
			||||||
 | 
							e.g., three twos are worth 200 points. The only exception is ones, three ones are
 | 
				
			||||||
 | 
							worth 1,000 points.
 | 
				
			||||||
 | 
						</li>
 | 
				
			||||||
 | 
						<li>
 | 
				
			||||||
 | 
							Four of a kind (⚂ ⚂ ⚂ ⚂) and five of a kind (⚂ ⚂
 | 
				
			||||||
 | 
							⚂ ⚂ ⚂) and six of a kind (⚂ ⚂ ⚂ ⚂ ⚂
 | 
				
			||||||
 | 
							⚂) are treated like a three of a kind where each additional matching die is
 | 
				
			||||||
 | 
							worth x2. So four threes is 600 (300 x 2), five threes is 1,200 (300 x 2 x 2), and six
 | 
				
			||||||
 | 
							threes are worth 2,400 (300 x 2 x 2 x 2).
 | 
				
			||||||
 | 
						</li>
 | 
				
			||||||
 | 
						<li>
 | 
				
			||||||
 | 
							Two threes of a kind (⚅ ⚅ ⚅ ⚃ ⚃ ⚃) is worth 1,600
 | 
				
			||||||
 | 
							points.
 | 
				
			||||||
 | 
						</li>
 | 
				
			||||||
 | 
						<li>
 | 
				
			||||||
 | 
							A run of 6 (⚀ ⚁ ⚂ ⚃ ⚄ ⚅) is worth 2,000 points.
 | 
				
			||||||
 | 
						</li>
 | 
				
			||||||
 | 
					</ol>
 | 
				
			||||||
 | 
				
			|||||||
@ -2,19 +2,26 @@ import type { RequestHandler } from "@sveltejs/kit";
 | 
				
			|||||||
import { listResponse, singleResponse } from "$lib/server/responseBodies";
 | 
					import { listResponse, singleResponse } from "$lib/server/responseBodies";
 | 
				
			||||||
import { createNewListing } from "$lib/server/modifyListing";
 | 
					import { createNewListing } from "$lib/server/modifyListing";
 | 
				
			||||||
import { Game } from "$lib/server/Game";
 | 
					import { Game } from "$lib/server/Game";
 | 
				
			||||||
import { listCollection, writeListing } from "$lib/server/mongo";
 | 
					import { listCollection, ServerCollections, writeNewListing } from "$lib/server/mongo";
 | 
				
			||||||
import { isListing } from "$lib/Listing";
 | 
					import { isListing } from "$lib/Listing";
 | 
				
			||||||
import type { GameData } from "$lib/GameData";
 | 
					import type { GameData } from "$lib/GameData";
 | 
				
			||||||
 | 
					import { getUser } from "$lib/server/requestTools";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const GET: RequestHandler = async (): Promise<Response> => {
 | 
					export const GET: RequestHandler = async (): Promise<Response> => {
 | 
				
			||||||
	const games = await listCollection("games", isListing<GameData>);
 | 
						const games = await listCollection(ServerCollections.Games, isListing<GameData>);
 | 
				
			||||||
	return listResponse(games);
 | 
						return listResponse(games);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const POST: RequestHandler = async (): Promise<Response> => {
 | 
					export const POST: RequestHandler = async ({ locals }): Promise<Response> => {
 | 
				
			||||||
	const newListing = createNewListing(new Game());
 | 
						const user = getUser(locals);
 | 
				
			||||||
 | 
						const { username, sub } = user.payload;
 | 
				
			||||||
 | 
						const game = new Game();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await writeListing("games", newListing);
 | 
						game.addPlayer(sub, username);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const newListing = createNewListing(game);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await writeNewListing(ServerCollections.Games, newListing);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return singleResponse(newListing.id);
 | 
						return singleResponse(newListing.id);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import type { GameData } from "$lib/GameData";
 | 
					import type { GameData } from "$lib/GameData";
 | 
				
			||||||
import { idFromString, type Id } from "$lib/Id";
 | 
					 | 
				
			||||||
import { isListing } from "$lib/Listing";
 | 
					import { isListing } from "$lib/Listing";
 | 
				
			||||||
import { readListingById } from "$lib/server/mongo";
 | 
					import { readListingById, ServerCollections } from "$lib/server/mongo";
 | 
				
			||||||
 | 
					import { getParam, ResourceId } from "$lib/server/requestTools";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
	badRequestResponse,
 | 
						badRequestResponse,
 | 
				
			||||||
	notFoundResponse,
 | 
						notFoundResponse,
 | 
				
			||||||
@ -10,20 +10,13 @@ import {
 | 
				
			|||||||
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> => {
 | 
				
			||||||
	const idStr = params["gameid"];
 | 
						const id = getParam(params, ResourceId.Game);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!idStr) {
 | 
						if (!id) {
 | 
				
			||||||
		return badRequestResponse("missing gameid parameter");
 | 
							return badRequestResponse("missing gameid parameter");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let id: Id;
 | 
						const game = await readListingById(ServerCollections.Games, id, isListing<GameData>);
 | 
				
			||||||
	try {
 | 
					 | 
				
			||||||
		id = idFromString(idStr);
 | 
					 | 
				
			||||||
	} catch (err) {
 | 
					 | 
				
			||||||
		return notFoundResponse();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const game = await readListingById("games", id, isListing<GameData>);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!game) {
 | 
						if (!game) {
 | 
				
			||||||
		return notFoundResponse();
 | 
							return notFoundResponse();
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										59
									
								
								src/routes/api/games/[gameid]/[turns]/+server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/routes/api/games/[gameid]/[turns]/+server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					import type { GameData } from "$lib/GameData";
 | 
				
			||||||
 | 
					import { getGameEvent, isGameEventData } from "$lib/GameEvent";
 | 
				
			||||||
 | 
					import { isListing } from "$lib/Listing";
 | 
				
			||||||
 | 
					import { updateListing } from "$lib/server/modifyListing";
 | 
				
			||||||
 | 
					import { createNewListing } from "$lib/server/modifyListing";
 | 
				
			||||||
 | 
					import { writeNewListing } from "$lib/server/mongo";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						readListingById,
 | 
				
			||||||
 | 
						ServerCollections,
 | 
				
			||||||
 | 
						writeUpdatedListing,
 | 
				
			||||||
 | 
					} from "$lib/server/mongo";
 | 
				
			||||||
 | 
					import { getBody, getParam, getUser, ResourceId } from "$lib/server/requestTools";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						badRequestResponse,
 | 
				
			||||||
 | 
						createdResponse,
 | 
				
			||||||
 | 
						notFoundResponse,
 | 
				
			||||||
 | 
						singleResponse,
 | 
				
			||||||
 | 
					} from "$lib/server/responseBodies";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const POST: RequestHandler = async ({
 | 
				
			||||||
 | 
						locals,
 | 
				
			||||||
 | 
						params,
 | 
				
			||||||
 | 
						request,
 | 
				
			||||||
 | 
					}): Promise<Response> => {
 | 
				
			||||||
 | 
						const id = getParam(params, ResourceId.Game);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (id === null) {
 | 
				
			||||||
 | 
							return badRequestResponse("missing gameid parameter");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const body = await getBody(request, isGameEventData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!body) {
 | 
				
			||||||
 | 
							return badRequestResponse("missing game event in body");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const user = getUser(locals);
 | 
				
			||||||
 | 
						const game = await readListingById(ServerCollections.Games, id, isListing<GameData>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!game) {
 | 
				
			||||||
 | 
							return notFoundResponse();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { state } = game.data;
 | 
				
			||||||
 | 
						const event = getGameEvent(game.data, body);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							event.run(state);
 | 
				
			||||||
 | 
							game.data.isStarted = true;
 | 
				
			||||||
 | 
						} catch (err) {
 | 
				
			||||||
 | 
							return badRequestResponse(`illegal turn: ${err}`);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await writeNewListing(ServerCollections.Turns, createNewListing(body));
 | 
				
			||||||
 | 
						await writeUpdatedListing(ServerCollections.Games, updateListing(game, game.data));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return singleResponse(game);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										16
									
								
								src/routes/api/me/+server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/routes/api/me/+server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import type { LocalCredentials } from "$lib/server/auth";
 | 
				
			||||||
 | 
					import { singleResponse } from "$lib/server/responseBodies";
 | 
				
			||||||
 | 
					import { error, type RequestHandler } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GET: RequestHandler = async ({ locals }): Promise<Response> => {
 | 
				
			||||||
 | 
						const user: LocalCredentials = locals.user;
 | 
				
			||||||
 | 
						if (user.kind !== "Bearer") {
 | 
				
			||||||
 | 
							error(401, "user not logged in");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return singleResponse({
 | 
				
			||||||
 | 
							id: user.payload.sub,
 | 
				
			||||||
 | 
							username: user.payload.username,
 | 
				
			||||||
 | 
							role: user.role,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -1,10 +1,9 @@
 | 
				
			|||||||
import { isListing } from "$lib/Listing";
 | 
					import { isListing } from "$lib/Listing";
 | 
				
			||||||
import { isLoginData } from "$lib/Login";
 | 
					import { isLoginData } from "$lib/Login";
 | 
				
			||||||
import { readListingByQuery } from "$lib/server/mongo";
 | 
					import { readListingByQuery, ServerCollections } from "$lib/server/mongo";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
	badRequestResponse,
 | 
						badRequestResponse,
 | 
				
			||||||
	notFoundResponse,
 | 
						tokenResponse,
 | 
				
			||||||
	singleResponse,
 | 
					 | 
				
			||||||
	unauthorizedResponse,
 | 
						unauthorizedResponse,
 | 
				
			||||||
} from "$lib/server/responseBodies";
 | 
					} from "$lib/server/responseBodies";
 | 
				
			||||||
import type { RequestHandler } from "@sveltejs/kit";
 | 
					import type { RequestHandler } from "@sveltejs/kit";
 | 
				
			||||||
@ -22,7 +21,7 @@ export const POST: RequestHandler = async ({ locals }): Promise<Response> => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		const { username, password } = user.payload;
 | 
							const { username, password } = user.payload;
 | 
				
			||||||
		const listing = await readListingByQuery(
 | 
							const listing = await readListingByQuery(
 | 
				
			||||||
			"logins",
 | 
								ServerCollections.Logins,
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				"data.username": username,
 | 
									"data.username": username,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
@ -30,12 +29,12 @@ export const POST: RequestHandler = async ({ locals }): Promise<Response> => {
 | 
				
			|||||||
		);
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!listing) {
 | 
							if (!listing) {
 | 
				
			||||||
			return notFoundResponse();
 | 
								return unauthorizedResponse();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (await compare(password, listing.data.password)) {
 | 
							if (await compare(password, listing.data.password)) {
 | 
				
			||||||
			const token = await createToken(listing, JWT_SECRET);
 | 
								const token = await createToken(listing, JWT_SECRET);
 | 
				
			||||||
			return singleResponse(token);
 | 
								return tokenResponse(token);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return badRequestResponse("wrong password");
 | 
							return badRequestResponse("wrong password");
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { hashPassword, isLoginData } from "$lib/Login";
 | 
					import { hashPassword, isLoginData } from "$lib/Login";
 | 
				
			||||||
import { createNewListing } from "$lib/server/modifyListing";
 | 
					import { createNewListing } from "$lib/server/modifyListing";
 | 
				
			||||||
import { writeListing } from "$lib/server/mongo";
 | 
					import { ServerCollections, writeNewListing } from "$lib/server/mongo";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
	badRequestResponse,
 | 
						badRequestResponse,
 | 
				
			||||||
	forbiddenResponse,
 | 
						forbiddenResponse,
 | 
				
			||||||
@ -30,7 +30,7 @@ export const POST: RequestHandler = async ({ request }): Promise<Response> => {
 | 
				
			|||||||
	const listing = createNewListing(body);
 | 
						const listing = createNewListing(body);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		await writeListing("logins", listing);
 | 
							await writeNewListing(ServerCollections.Logins, listing);
 | 
				
			||||||
		return singleResponse(listing.id);
 | 
							return singleResponse(listing.id);
 | 
				
			||||||
	} catch (err) {
 | 
						} catch (err) {
 | 
				
			||||||
		return serverErrorResponse();
 | 
							return serverErrorResponse();
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										13
									
								
								src/routes/games/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/routes/games/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					import { error } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					import type { PageServerLoad } from "./$types";
 | 
				
			||||||
 | 
					import { getPageData } from "./getPageData";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const load: PageServerLoad = async ({ fetch, cookies }) => {
 | 
				
			||||||
 | 
						const token = cookies.get("access_token");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!token) {
 | 
				
			||||||
 | 
							error(401);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return getPageData(fetch, token);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -1,52 +1,59 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
    type GameData = {id: number, name: string, players: string[]}
 | 
						import PlayerList from "$lib/components/PlayerList.svelte";
 | 
				
			||||||
 | 
						import type { GameData } from "$lib/GameData.js";
 | 
				
			||||||
 | 
						import type { Listing } from "$lib/Listing";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const games: GameData[] = [
 | 
						const { data } = $props();
 | 
				
			||||||
        {id: 1, name: "The Worst", players: ["Bob", "Ted", "George"]},
 | 
						const games = data.games;
 | 
				
			||||||
        {id: 2, name: "The Best", players: ["Shelly", "William", "Abby"]},
 | 
						const prettyDate = (date: Date) => {
 | 
				
			||||||
        {id: 3, name: "The One with the Treasure Chest", players: ["Jack"]},
 | 
							return `${date.toLocaleString()}`;
 | 
				
			||||||
    ];
 | 
						};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<main>
 | 
					<h1>Let’s Play Ten Thousand</h1>
 | 
				
			||||||
    <h1>Let’s Play Ten Thousand</h1>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <table>
 | 
					<h2>Games</h2>
 | 
				
			||||||
        <thead>
 | 
					<div class="game-list">
 | 
				
			||||||
            <tr>
 | 
					 | 
				
			||||||
                <td>No.</td>
 | 
					 | 
				
			||||||
                <td>Name</td>
 | 
					 | 
				
			||||||
                <td>Players</td>
 | 
					 | 
				
			||||||
            </tr>
 | 
					 | 
				
			||||||
        </thead>
 | 
					 | 
				
			||||||
        <tbody>
 | 
					 | 
				
			||||||
	{#each games as game}
 | 
						{#each games as game}
 | 
				
			||||||
		{@render GameRow(game)}
 | 
							{@render GameRow(game)}
 | 
				
			||||||
	{/each}
 | 
						{/each}
 | 
				
			||||||
        </tbody>
 | 
					</div>
 | 
				
			||||||
    </table>
 | 
					 | 
				
			||||||
</main>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#snippet GameRow (game: GameData)}
 | 
					{#snippet GameRow(game: Listing<GameData>)}
 | 
				
			||||||
    <tr>
 | 
						<div class="game-listing">
 | 
				
			||||||
        <td>{game.id}</td>    
 | 
							<div>{prettyDate(new Date(game.createdAt))}</div>
 | 
				
			||||||
        <td>{game.name}</td>
 | 
							<div>
 | 
				
			||||||
        <td>{game.players.length}</td>
 | 
								{#each games as game}
 | 
				
			||||||
    </tr>
 | 
									<PlayerList players={game.data.players} />
 | 
				
			||||||
 | 
								{/each}
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<form method="GET" action={`/games/${game.id}`}>
 | 
				
			||||||
 | 
								<input type="submit" value="JOIN" />
 | 
				
			||||||
 | 
							</form>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
{/snippet}
 | 
					{/snippet}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
    main {
 | 
						.game-list {
 | 
				
			||||||
        width: 60rem;
 | 
							width: 100%;
 | 
				
			||||||
        margin: auto;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    td {
 | 
						.game-listing {
 | 
				
			||||||
        border: solid black 1px;
 | 
							display: flex;
 | 
				
			||||||
        padding: 1rem;
 | 
							gap: 3rem;
 | 
				
			||||||
 | 
							padding: 0.5rem;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    table {
 | 
						.game-listing > div {
 | 
				
			||||||
 | 
							flex: 1 1 auto;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						form {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex: 1 1 auto;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						form input {
 | 
				
			||||||
		width: 100%;
 | 
							width: 100%;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 | 
				
			|||||||
@ -2,15 +2,21 @@ import { isGameData, type GameData } from "$lib/GameData";
 | 
				
			|||||||
import { isListing } from "$lib/Listing";
 | 
					import { isListing } from "$lib/Listing";
 | 
				
			||||||
import { isServerResponse } from "$lib/ServerResponse";
 | 
					import { isServerResponse } from "$lib/ServerResponse";
 | 
				
			||||||
import { error } from "@sveltejs/kit";
 | 
					import { error } from "@sveltejs/kit";
 | 
				
			||||||
import type { PageLoad } from "./$types";
 | 
					import type { PageServerLoad } from "./$types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const load: PageLoad = async ({ fetch, params }) => {
 | 
					export const load: PageServerLoad = async ({ fetch, params, cookies }) => {
 | 
				
			||||||
	const url = `/api/games/${params.gameid}`;
 | 
						const url = `/api/games/${params.gameid}`;
 | 
				
			||||||
	let res: Response;
 | 
						let res: Response;
 | 
				
			||||||
	let body: unknown;
 | 
						let body: unknown;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const token = cookies.get("access_token");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!token) {
 | 
				
			||||||
 | 
							error(401);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		res = await fetch(url);
 | 
							res = await fetch(url, { headers: [["Authorization", token]] });
 | 
				
			||||||
		body = await res.json();
 | 
							body = await res.json();
 | 
				
			||||||
	} catch (err) {
 | 
						} catch (err) {
 | 
				
			||||||
		error(500, "unable to call API");
 | 
							error(500, "unable to call API");
 | 
				
			||||||
@ -25,8 +31,8 @@ export const load: PageLoad = async ({ fetch, params }) => {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if ("item" in body && isListing<GameData>(body.item, isGameData)) {
 | 
						if ("item" in body && isListing<GameData>(body.item, isGameData)) {
 | 
				
			||||||
		return body.item;
 | 
							return { game: body.item };
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		error(500, "expected response body to contain game data");
 | 
							error(res.status, "unable to fetch game data");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -1,15 +1,43 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
    import type { PageData } from "./$types"
 | 
						import PlayerList from "$lib/components/PlayerList.svelte";
 | 
				
			||||||
    let { data }: { data: PageData } = $props();
 | 
						import type { PageData } from "./$types";
 | 
				
			||||||
 | 
						let { data: page }: { data: PageData } = $props();
 | 
				
			||||||
 | 
						const handleSubmit = () => {};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<main>
 | 
					{#if page.game.data.isStarted}
 | 
				
			||||||
    <h1>This is game {data.id}</h1>
 | 
						<h1>This is game {page.game.id}</h1>
 | 
				
			||||||
</main>
 | 
					{:else}
 | 
				
			||||||
 | 
						<h1>This is some lobby</h1>
 | 
				
			||||||
 | 
						<div id="lobby">
 | 
				
			||||||
 | 
							<div id="players">
 | 
				
			||||||
 | 
								<PlayerList players={page.game.data.players} />
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div id="chat" style="background: pink"></div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div id="controls">
 | 
				
			||||||
 | 
							<input type="button" value="Start Game" onsubmit={handleSubmit} />
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
    main {
 | 
						#lobby {
 | 
				
			||||||
        width: 60rem;
 | 
							display: flex;
 | 
				
			||||||
        margin: auto;
 | 
							width: 100%;
 | 
				
			||||||
 | 
							gap: 1rem;
 | 
				
			||||||
 | 
							min-height: 30rem;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						#players {
 | 
				
			||||||
 | 
							flex: 1;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						#chat {
 | 
				
			||||||
 | 
							flex: 2 2;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						#controls {
 | 
				
			||||||
 | 
							margin: 1rem 0;
 | 
				
			||||||
 | 
							text-align: right;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										50
									
								
								src/routes/games/getPageData.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/routes/games/getPageData.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					import { isGameData, type GameData } from "$lib/GameData";
 | 
				
			||||||
 | 
					import { isListing, type Listing } from "$lib/Listing";
 | 
				
			||||||
 | 
					import { isServerResponse } from "$lib/ServerResponse";
 | 
				
			||||||
 | 
					import { error } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getPageData(
 | 
				
			||||||
 | 
						svelteKitFetch: typeof fetch,
 | 
				
			||||||
 | 
						token: string,
 | 
				
			||||||
 | 
					): Promise<{ games: Listing<GameData>[] }> {
 | 
				
			||||||
 | 
						const url = "/api/games";
 | 
				
			||||||
 | 
						let res: Response;
 | 
				
			||||||
 | 
						let body: unknown;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							res = await svelteKitFetch(url, {
 | 
				
			||||||
 | 
								headers: [["Authorization", token]],
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							body = await res.json();
 | 
				
			||||||
 | 
						} catch (err) {
 | 
				
			||||||
 | 
							console.log(err);
 | 
				
			||||||
 | 
							error(500, "unable to call API");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (res.status === 404) {
 | 
				
			||||||
 | 
							error(404, `Not Found`);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!isServerResponse(body)) {
 | 
				
			||||||
 | 
							console.log("wasn't server response");
 | 
				
			||||||
 | 
							error(500, "expected to receive a properly formatted server response body");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ("items" in body) {
 | 
				
			||||||
 | 
							if (
 | 
				
			||||||
 | 
								body.items.reduce((result, item) => {
 | 
				
			||||||
 | 
									if (result) {
 | 
				
			||||||
 | 
										return isListing<GameData>(item, isGameData);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return false;
 | 
				
			||||||
 | 
								}, true)
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								return { games: body.items as Listing<GameData>[] };
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								error(500, "malformed API response");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							error(res.status, "unable to fetch game data");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										54
									
								
								src/routes/login/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/routes/login/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { isMe } from "$lib/me";
 | 
				
			||||||
 | 
						import { setMeContext } from "$lib/meContext";
 | 
				
			||||||
 | 
						import { isServerResponse } from "$lib/ServerResponse";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let username: string = $state("");
 | 
				
			||||||
 | 
						let password: string = $state("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const handleSubmit = async (e: SubmitEvent) => {
 | 
				
			||||||
 | 
							e.preventDefault();
 | 
				
			||||||
 | 
							let res = await fetch("/api/token", {
 | 
				
			||||||
 | 
								headers: [["authorization", `Basic ${btoa(`${username}:${password}`)}`]],
 | 
				
			||||||
 | 
								method: "POST",
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const params = new URLSearchParams(window.location.search);
 | 
				
			||||||
 | 
							const toPage = params.get("to") ?? "/";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							let body: unknown;
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								body = await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (!isServerResponse(body)) {
 | 
				
			||||||
 | 
									throw new Error("missing or malformed body");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (!("access_token" in body)) {
 | 
				
			||||||
 | 
									throw new Error("expected to receive an access token");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								document.cookie = `access_token=Bearer ${body.access_token} ; SameSite=Strict`;
 | 
				
			||||||
 | 
								window.location.replace(toPage);
 | 
				
			||||||
 | 
							} catch (e) {
 | 
				
			||||||
 | 
								console.error(e);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<main>
 | 
				
			||||||
 | 
						<h1>Login</h1>
 | 
				
			||||||
 | 
						<form onsubmit={handleSubmit}>
 | 
				
			||||||
 | 
							<div>
 | 
				
			||||||
 | 
								<label for="username">Username</label>
 | 
				
			||||||
 | 
								<input name="username" id="username" type="text" bind:value={username} />
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div>
 | 
				
			||||||
 | 
								<label for="password">Password</label>
 | 
				
			||||||
 | 
								<input name="password" id="password" type="password" bind:value={password} />
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div>
 | 
				
			||||||
 | 
								<input name="submit" id="submit" type="submit" value="Submit" />
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</form>
 | 
				
			||||||
 | 
					</main>
 | 
				
			||||||
							
								
								
									
										0
									
								
								src/routes/register/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/routes/register/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@ -35,15 +35,15 @@ Content-Type: application/json
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
###
 | 
					###
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
POST https://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3/turns
 | 
					POST https://localhost:5173/api/games/67b39573a0fcb80dd13f2c8b/turns
 | 
				
			||||||
Accept: application/json
 | 
					Accept: application/json
 | 
				
			||||||
Content-Type: application/json
 | 
					Content-Type: application/json
 | 
				
			||||||
Authorization: Bearer {{token}}
 | 
					Authorization: Bearer {{token}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    "kind": "Roll",
 | 
					    "kind": "SeatPlayers",
 | 
				
			||||||
    "player": 2,
 | 
					    "player": 1,
 | 
				
			||||||
    "value": 4
 | 
					    "value": 2
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
###
 | 
					###
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { configDefaults, coverageConfigDefaults, defineConfig } from "vitest/config";
 | 
					import { coverageConfigDefaults, defineConfig } from "vitest/config";
 | 
				
			||||||
import { sveltekit } from "@sveltejs/kit/vite";
 | 
					import { sveltekit } from "@sveltejs/kit/vite";
 | 
				
			||||||
import { readFileSync } from "fs";
 | 
					import { readFileSync } from "fs";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -18,5 +18,6 @@ export default defineConfig({
 | 
				
			|||||||
		coverage: {
 | 
							coverage: {
 | 
				
			||||||
			exclude: [...coverageConfigDefaults.exclude, "svelte.config.js"],
 | 
								exclude: [...coverageConfigDefaults.exclude, "svelte.config.js"],
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							environment: "jsdom",
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user