Added auth to server hook.
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -24,3 +24,4 @@ vite.config.ts.timestamp-*
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Development
 | 
					# Development
 | 
				
			||||||
cert
 | 
					cert
 | 
				
			||||||
 | 
					.vscode
 | 
				
			||||||
							
								
								
									
										825
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										825
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -20,6 +20,8 @@
 | 
				
			|||||||
		"@sveltejs/adapter-auto": "^3.0.0",
 | 
							"@sveltejs/adapter-auto": "^3.0.0",
 | 
				
			||||||
		"@sveltejs/kit": "^2.0.0",
 | 
							"@sveltejs/kit": "^2.0.0",
 | 
				
			||||||
		"@sveltejs/vite-plugin-svelte": "^4.0.0",
 | 
							"@sveltejs/vite-plugin-svelte": "^4.0.0",
 | 
				
			||||||
 | 
							"@types/bcrypt": "^5.0.2",
 | 
				
			||||||
 | 
							"@types/jsonwebtoken": "^9.0.7",
 | 
				
			||||||
		"@types/node": "^22.10.7",
 | 
							"@types/node": "^22.10.7",
 | 
				
			||||||
		"eslint": "^9.7.0",
 | 
							"eslint": "^9.7.0",
 | 
				
			||||||
		"eslint-config-prettier": "^9.1.0",
 | 
							"eslint-config-prettier": "^9.1.0",
 | 
				
			||||||
@ -33,5 +35,10 @@
 | 
				
			|||||||
		"typescript-eslint": "^8.0.0",
 | 
							"typescript-eslint": "^8.0.0",
 | 
				
			||||||
		"vite": "^5.4.11",
 | 
							"vite": "^5.4.11",
 | 
				
			||||||
		"vitest": "^2.0.4"
 | 
							"vitest": "^2.0.4"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"dependencies": {
 | 
				
			||||||
 | 
							"bcrypt": "^5.1.1",
 | 
				
			||||||
 | 
							"jsonwebtoken": "^9.0.2",
 | 
				
			||||||
 | 
							"mongodb": "^6.12.0"
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								src/app.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								src/app.d.ts
									
									
									
									
										vendored
									
									
								
							@ -1,9 +1,14 @@
 | 
				
			|||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
 | 
					// See https://svelte.dev/docs/kit/types#app.d.ts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { LocalCredentials } from "$lib/server/requestTools";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// for information about these interfaces
 | 
					// for information about these interfaces
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
	namespace App {
 | 
						namespace App {
 | 
				
			||||||
		// interface Error {}
 | 
							// interface Error {}
 | 
				
			||||||
		// interface Locals {}
 | 
							interface Locals {
 | 
				
			||||||
 | 
								user: LocalCredentials;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		// interface PageData {}
 | 
							// interface PageData {}
 | 
				
			||||||
		// interface PageState {}
 | 
							// interface PageState {}
 | 
				
			||||||
		// interface Platform {}
 | 
							// interface Platform {}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,15 @@
 | 
				
			|||||||
 | 
					import { isAuthorized } from "$lib/server/requestTools";
 | 
				
			||||||
 | 
					import { unauthorizedResponse } from "$lib/server/responseBodies";
 | 
				
			||||||
import type { Handle } from "@sveltejs/kit";
 | 
					import type { Handle } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const handle: Handle = async ({ event, resolve }) => {
 | 
					export const handle: Handle = async ({ event, resolve }) => {
 | 
				
			||||||
	console.log("this got called", event.isSubRequest);
 | 
						const auth = await isAuthorized(event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!auth) {
 | 
				
			||||||
 | 
							return unauthorizedResponse();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						event.locals.user = auth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return await resolve(event);
 | 
						return await resolve(event);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { hasOnlyKeys, hasProperty } from "./validation";
 | 
					import { hasOnlyKeys, hasProperty } from "./validation";
 | 
				
			||||||
import type { Id } from "./server/Id";
 | 
					import type { Id } from "./Id";
 | 
				
			||||||
import type { State } from "./State";
 | 
					import type { State } from "./State";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface GameData {
 | 
					export interface GameData {
 | 
				
			||||||
 | 
				
			|||||||
@ -100,11 +100,7 @@ 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 and attempt to roll the highest die and go
 | 
				
			||||||
 * first.
 | 
					 * first.
 | 
				
			||||||
 *
 | 
					 *axpected to re-roll as a tie breaker. Re-rolling continues until someone
 | 
				
			||||||
 * Players can roll in any order. Each time a player rolls, the event checks to see if
 | 
					 | 
				
			||||||
 * everyone has rolled and if there is a winner, it sets that person as the first active
 | 
					 | 
				
			||||||
 * player and ends the rollling-for-first stage of the game. If there was a tie, those
 | 
					 | 
				
			||||||
 * players are expected to re-roll as a tie breaker. Re-rolling continues until someone
 | 
					 | 
				
			||||||
 * wins.
 | 
					 * wins.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export class RollForFirst implements GameEvent {
 | 
					export class RollForFirst implements GameEvent {
 | 
				
			||||||
@ -391,10 +387,7 @@ export class Score implements GameEvent {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function scorePips(count: number, pips: number) {
 | 
					function scorePips(count: number, pips: number) {
 | 
				
			||||||
	if (count < 3) {
 | 
						if (coa
 | 
				
			||||||
		// If not a three of a kind, return the raw dice value...
 | 
					 | 
				
			||||||
		return pipScore(pips) * count;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ...otherwise, this is a three or more of a kind.
 | 
						// ...otherwise, this is a three or more of a kind.
 | 
				
			||||||
	if (pips === 1) {
 | 
						if (pips === 1) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										11
									
								
								src/lib/Id.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/lib/Id.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import { ObjectId } from "mongodb";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Id = ObjectId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createId(): Id {
 | 
				
			||||||
 | 
						return new ObjectId();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isId(target: unknown): target is Id {
 | 
				
			||||||
 | 
						return target instanceof ObjectId;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,7 +1,8 @@
 | 
				
			|||||||
import { hasProperty } from "$lib/validation";
 | 
					import { hasProperty } from "$lib/validation";
 | 
				
			||||||
 | 
					import { isId, type Id } from "$lib/Id";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Listing<T> {
 | 
					export interface Listing<T = unknown> {
 | 
				
			||||||
	id: string;
 | 
						id: Id;
 | 
				
			||||||
	createdAt: string;
 | 
						createdAt: string;
 | 
				
			||||||
	modifiedAt: string | null;
 | 
						modifiedAt: string | null;
 | 
				
			||||||
	deleted: boolean;
 | 
						deleted: boolean;
 | 
				
			||||||
@ -12,7 +13,7 @@ export function isListing<T>(
 | 
				
			|||||||
	target: unknown,
 | 
						target: unknown,
 | 
				
			||||||
	dataGuard?: (target: unknown) => target is T
 | 
						dataGuard?: (target: unknown) => target is T
 | 
				
			||||||
): target is Listing<T> {
 | 
					): target is Listing<T> {
 | 
				
			||||||
	if (!hasProperty(target, "id", "string")) return false;
 | 
						if (!hasProperty(target, "id", isId)) return false;
 | 
				
			||||||
	if (!hasProperty(target, "createdAt", "string")) return false;
 | 
						if (!hasProperty(target, "createdAt", "string")) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!hasProperty(target, "modifiedAt", "null")) {
 | 
						if (!hasProperty(target, "modifiedAt", "null")) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										22
									
								
								src/lib/Login.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/lib/Login.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					import { hash } from "bcrypt";
 | 
				
			||||||
 | 
					import { hasProperty, hasOnlyKeys } from "./validation";
 | 
				
			||||||
 | 
					import { BCRYPT_SALT_ROUNDS } from "$env/static/private";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const saltRounds = parseInt(BCRYPT_SALT_ROUNDS);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface LoginData {
 | 
				
			||||||
 | 
						username: string;
 | 
				
			||||||
 | 
						password: string;
 | 
				
			||||||
 | 
						role: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isLoginData(target: unknown): target is LoginData {
 | 
				
			||||||
 | 
						if (!hasProperty(target, "username", "string")) return false;
 | 
				
			||||||
 | 
						if (!hasProperty(target, "password", "string")) return false;
 | 
				
			||||||
 | 
						if (!hasProperty(target, "role", "string")) return false;
 | 
				
			||||||
 | 
						return hasOnlyKeys(target, ["username", "password", "role"]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function hashPassword(password: string): Promise<string> {
 | 
				
			||||||
 | 
						return await hash(password, saltRounds);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import type { Id } from "./Id";
 | 
					import type { Id } from "../Id";
 | 
				
			||||||
import type { GameData } from "../GameData";
 | 
					import type { GameData } from "../GameData";
 | 
				
			||||||
import type { State } from "../State";
 | 
					import type { State } from "../State";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1 +0,0 @@
 | 
				
			|||||||
export type Id = string;
 | 
					 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import type { GameData } from "../GameData";
 | 
					import type { GameData } from "$lib/GameData";
 | 
				
			||||||
import type { Listing } from "./modifyListing";
 | 
					import type { Listing } from "$lib/Listing";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const games: Listing<GameData>[] = [];
 | 
					export const games: Listing<GameData>[] = [];
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
import { randomUUID } from "crypto";
 | 
					 | 
				
			||||||
import type { Listing } from "$lib/Listing";
 | 
					import type { Listing } from "$lib/Listing";
 | 
				
			||||||
 | 
					import { createId } from "$lib/Id";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function createNewListing<T>(data: T): Listing<T> {
 | 
					export function createNewListing<T>(data: T): Listing<T> {
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		id: randomUUID(),
 | 
							id: createId(),
 | 
				
			||||||
		createdAt: new Date().toISOString(),
 | 
							createdAt: new Date().toISOString(),
 | 
				
			||||||
		modifiedAt: null,
 | 
							modifiedAt: null,
 | 
				
			||||||
		deleted: false,
 | 
							deleted: false,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										80
									
								
								src/lib/server/mongo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/lib/server/mongo.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					import type { Listing } from "$lib/Listing";
 | 
				
			||||||
 | 
					import { MongoClient, ObjectId, ServerApiVersion, type Document, type WithId } from "mongodb";
 | 
				
			||||||
 | 
					import type { Id } from "../Id";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ListingFromMongo = Omit<Listing, "id"> & { _id: ObjectId };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const uri = `mongodb://127.0.0.1:27017`;
 | 
				
			||||||
 | 
					const DATABASE = "ten-thousand";
 | 
				
			||||||
 | 
					let client: MongoClient | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function init() {
 | 
				
			||||||
 | 
						const c = new MongoClient(uri, {
 | 
				
			||||||
 | 
							serverApi: {
 | 
				
			||||||
 | 
								version: ServerApiVersion.v1,
 | 
				
			||||||
 | 
								strict: true,
 | 
				
			||||||
 | 
								deprecationErrors: true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await c.connect();
 | 
				
			||||||
 | 
						return c;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function writeListing(col: string, listing: Listing) {
 | 
				
			||||||
 | 
						if (client === null) {
 | 
				
			||||||
 | 
							client = await init();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await client.db(DATABASE).collection(col).insertOne(fixListingForMongo(listing));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function readListingById<T>(
 | 
				
			||||||
 | 
						col: string,
 | 
				
			||||||
 | 
						id: Id,
 | 
				
			||||||
 | 
						dataGuard: (target: unknown) => target is T
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function readListingByQuery<T>(
 | 
				
			||||||
 | 
						col: string,
 | 
				
			||||||
 | 
						query: object,
 | 
				
			||||||
 | 
						dataGuard: (target: unknown) => target is Listing<T>
 | 
				
			||||||
 | 
					): Promise<Listing<T> | null> {
 | 
				
			||||||
 | 
						if (client === null) {
 | 
				
			||||||
 | 
							client = await init();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const res = await client.db(DATABASE).collection(col).findOne(query);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (res === null) {
 | 
				
			||||||
 | 
							return null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return fixListingFromMongo(res, dataGuard);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function fixListingForMongo(listing: Listing): ListingFromMongo {
 | 
				
			||||||
 | 
						// TODO: These two statements are tricky without any. Perhaps id could be optional,
 | 
				
			||||||
 | 
						//       but that's kind of stupid because it's not optional on the type I'm
 | 
				
			||||||
 | 
						//       constructing. Figure out something better here.
 | 
				
			||||||
 | 
						const adjusted: any = { _id: listing.id, ...listing };
 | 
				
			||||||
 | 
						delete adjusted.id;
 | 
				
			||||||
 | 
						return adjusted;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function fixListingFromMongo<T>(
 | 
				
			||||||
 | 
						target: WithId<Document>,
 | 
				
			||||||
 | 
						dataGuard: (target: unknown) => target is Listing<T>
 | 
				
			||||||
 | 
					): Listing<T> {
 | 
				
			||||||
 | 
						// TODO: These two statements are tricky without any. Perhaps id could be optional,
 | 
				
			||||||
 | 
						//       but that's kind of stupid because it's not optional on the type I'm
 | 
				
			||||||
 | 
						//       constructing. Figure out something better here.
 | 
				
			||||||
 | 
						const adjusted: any = { id: target._id, ...target };
 | 
				
			||||||
 | 
						delete adjusted._id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!dataGuard(adjusted)) {
 | 
				
			||||||
 | 
							throw new Error("the returned document does not conform to the provided type");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return adjusted;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										157
									
								
								src/lib/server/requestTools.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								src/lib/server/requestTools.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,157 @@
 | 
				
			|||||||
 | 
					import { JWT_SECRET } from "$env/static/private";
 | 
				
			||||||
 | 
					import type { RequestEvent } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					import jwt from "jsonwebtoken";
 | 
				
			||||||
 | 
					import { routeAuth, type Method, type RouteAuthRule } from "./routeAuth";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type LocalCredentials =
 | 
				
			||||||
 | 
						| { kind: "Basic"; payload: { username: string; password: string } }
 | 
				
			||||||
 | 
						| { kind: "Bearer"; payload: jwt.JwtPayload | string }
 | 
				
			||||||
 | 
						| { kind: "None" };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function isAuthorized(event: RequestEvent): Promise<LocalCredentials | null> {
 | 
				
			||||||
 | 
						let path = event.url.pathname;
 | 
				
			||||||
 | 
						let tokenKind: "Basic" | "Bearer" | "None";
 | 
				
			||||||
 | 
						let tokenRole: string;
 | 
				
			||||||
 | 
						let tokenDesc: LocalCredentials;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const parts = breakupPath(path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (parts[0] !== "api") {
 | 
				
			||||||
 | 
							// not concerned about requests made to the frontend server
 | 
				
			||||||
 | 
							return { kind: "None" };
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const authHeader = event.request.headers.get("authorization");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// get token kind and role from the header
 | 
				
			||||||
 | 
						if (!authHeader) {
 | 
				
			||||||
 | 
							// This is a stranger: they have no token and they will be assigned the default
 | 
				
			||||||
 | 
							// role.
 | 
				
			||||||
 | 
							tokenKind = "None";
 | 
				
			||||||
 | 
							tokenRole = "default";
 | 
				
			||||||
 | 
							tokenDesc = { kind: "None" };
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							const [kind, token] = authHeader.split(" ");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (kind === "Bearer") {
 | 
				
			||||||
 | 
								tokenKind = "Bearer";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// The role can be derived from the JWT token.
 | 
				
			||||||
 | 
								const payload = await jwt.verify(token, JWT_SECRET);
 | 
				
			||||||
 | 
								if (typeof payload === "string") {
 | 
				
			||||||
 | 
									// 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;
 | 
				
			||||||
 | 
									// user should have a bearer token
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								tokenRole = payload.role;
 | 
				
			||||||
 | 
								tokenDesc = { kind: "Bearer", payload };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (!tokenRole) {
 | 
				
			||||||
 | 
									// Something has gone wrong: I should not have issued a token without a
 | 
				
			||||||
 | 
									// role.
 | 
				
			||||||
 | 
									return null;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else if (kind === "Basic") {
 | 
				
			||||||
 | 
								const decoded = Buffer.from(token, "base64").toString("ascii");
 | 
				
			||||||
 | 
								const [username, password] = decoded.split(":");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								tokenKind = "Basic";
 | 
				
			||||||
 | 
								tokenRole = "default";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								tokenDesc = { kind: "Basic", payload: { username, password } };
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// Something the server doesn't recognize was passed as the token kind.
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Determine if the role has the ability to hit this endpoint with this method
 | 
				
			||||||
 | 
						// and the given token.
 | 
				
			||||||
 | 
						const rules = routeAuth[tokenRole];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let hasMatchingAllow = false;
 | 
				
			||||||
 | 
						for (const rule of rules) {
 | 
				
			||||||
 | 
							console.log("checking rule", rule);
 | 
				
			||||||
 | 
							if (matchesRequest(tokenKind, parts, event.request.method as Method, rule)) {
 | 
				
			||||||
 | 
								console.log("match!");
 | 
				
			||||||
 | 
								if (rule.action === "deny") {
 | 
				
			||||||
 | 
									// if a request matches any deny rule, then it is denied, regardless
 | 
				
			||||||
 | 
									// of whether or not it also matches an allow rule.
 | 
				
			||||||
 | 
									return null;
 | 
				
			||||||
 | 
								} else if (rule.action === "allow") {
 | 
				
			||||||
 | 
									hasMatchingAllow = true;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (hasMatchingAllow) {
 | 
				
			||||||
 | 
							return tokenDesc;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function breakupPath(path: string): string[] {
 | 
				
			||||||
 | 
						// remove a leading /
 | 
				
			||||||
 | 
						if (path[0] === "/") path = path.slice(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return path.split("/");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function matchesRequest(
 | 
				
			||||||
 | 
						requestTokenKind: "Basic" | "Bearer" | "None",
 | 
				
			||||||
 | 
						requestParts: string[],
 | 
				
			||||||
 | 
						method: Method,
 | 
				
			||||||
 | 
						{ endpoint, methods, tokenKind = "Bearer" }: RouteAuthRule
 | 
				
			||||||
 | 
					): boolean {
 | 
				
			||||||
 | 
						if (tokenKind !== requestTokenKind) {
 | 
				
			||||||
 | 
							console.log("token types didn't match", tokenKind, requestTokenKind);
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!methods.includes("*") && !methods.includes(method)) {
 | 
				
			||||||
 | 
							console.log("Bad method", method);
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const ruleParts = breakupPath(endpoint);
 | 
				
			||||||
 | 
						for (let i = 0; i < ruleParts.length; i++) {
 | 
				
			||||||
 | 
							const rulePart = ruleParts[i];
 | 
				
			||||||
 | 
							const reqPart = requestParts[i];
 | 
				
			||||||
 | 
							if (rulePart[0] === "[" && rulePart[rulePart.length - 1] === "]") {
 | 
				
			||||||
 | 
								// This part of the path represents an ID.
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (i == ruleParts.length - 1 && rulePart === "*") {
 | 
				
			||||||
 | 
								// Rule has a wildcard, anything after it automatically matches.
 | 
				
			||||||
 | 
								return true;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (rulePart !== reqPart) {
 | 
				
			||||||
 | 
								console.log("rule parts do not match", rulePart, reqPart);
 | 
				
			||||||
 | 
								return false;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -17,3 +17,11 @@ export function notFoundResponse() {
 | 
				
			|||||||
export function serverErrorResponse() {
 | 
					export function serverErrorResponse() {
 | 
				
			||||||
	return Response.json({ error: "Unexpected Server Error" }, { status: 500 });
 | 
						return Response.json({ error: "Unexpected Server Error" }, { status: 500 });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function unauthorizedResponse() {
 | 
				
			||||||
 | 
						return Response.json({ error: "Unauthorized" }, { status: 401 });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function forbiddenResponse() {
 | 
				
			||||||
 | 
						return Response.json({ error: "Forbidden" }, { status: 403 });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										25
									
								
								src/lib/server/routeAuth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/lib/server/routeAuth.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					export type Method = "POST" | "GET" | "PUT" | "DELETE" | "PATCH" | "*";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface RouteAuthRule {
 | 
				
			||||||
 | 
						action: "allow" | "deny";
 | 
				
			||||||
 | 
						methods: Method[];
 | 
				
			||||||
 | 
						endpoint: string;
 | 
				
			||||||
 | 
						tokenKind?: "None" | "Bearer" | "Basic"; // defaults to "Bearer"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const routeAuth: { [k: string]: RouteAuthRule[] } = {
 | 
				
			||||||
 | 
						// default is an unknown user. They are allowed to create a new user for themselves
 | 
				
			||||||
 | 
						// without any token, and they are allowed to access the token endpoint to login with
 | 
				
			||||||
 | 
						// a Basic token. Other than that, they cannot do anything!
 | 
				
			||||||
 | 
						default: [
 | 
				
			||||||
 | 
							{ action: "allow", methods: ["POST"], endpoint: "/api/users", tokenKind: "None" },
 | 
				
			||||||
 | 
							{ action: "allow", methods: ["POST"], endpoint: "/api/token", tokenKind: "Basic" }
 | 
				
			||||||
 | 
						],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// player is anyone else. They are authorized to hit any endpoint, using any method,
 | 
				
			||||||
 | 
						// with a Bearer token.
 | 
				
			||||||
 | 
						player: [
 | 
				
			||||||
 | 
							{ action: "allow", methods: ["*"], endpoint: "*" },
 | 
				
			||||||
 | 
							{ action: "deny", methods: ["POST"], endpoint: "/api/token" }
 | 
				
			||||||
 | 
						]
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -10,11 +10,11 @@
 | 
				
			|||||||
// hasProperty can also receive a specific array type which resembles defining arrays of
 | 
					// hasProperty can also receive a specific array type which resembles defining arrays of
 | 
				
			||||||
// items in TypeScript: e.g. string[] is an array of string, (string|number)[] is an array of
 | 
					// items in TypeScript: e.g. string[] is an array of string, (string|number)[] is an array of
 | 
				
			||||||
// strings or numbers. It does not recognize the Array<string> syntax.
 | 
					// strings or numbers. It does not recognize the Array<string> syntax.
 | 
				
			||||||
export function hasProperty<T extends typeof Object>(
 | 
					export function hasProperty(
 | 
				
			||||||
	target: unknown,
 | 
						target: unknown,
 | 
				
			||||||
	propertyName: string,
 | 
						propertyName: string,
 | 
				
			||||||
	propertyType: string | T,
 | 
						propertyType: string | ((target: unknown) => boolean),
 | 
				
			||||||
	isNullable: boolean = false,
 | 
						isNullable: boolean = false
 | 
				
			||||||
): boolean {
 | 
					): boolean {
 | 
				
			||||||
	if (target === null || target === undefined) return false;
 | 
						if (target === null || target === undefined) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -24,12 +24,12 @@ export function hasProperty<T extends typeof Object>(
 | 
				
			|||||||
		return false;
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (p === null) {
 | 
						if (typeof propertyType === "function") {
 | 
				
			||||||
		return propertyType === "null" || isNullable;
 | 
							return propertyType(p);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (typeof propertyType !== "string") {
 | 
						if (p === null) {
 | 
				
			||||||
		return p instanceof propertyType
 | 
							return propertyType === "null" || isNullable;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (propertyType === "array") {
 | 
						if (propertyType === "array") {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,13 +3,16 @@ 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 { games } from "$lib/server/cache";
 | 
					import { games } from "$lib/server/cache";
 | 
				
			||||||
 | 
					import { writeListing } from "$lib/server/mongo";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const GET: RequestHandler = (): Response => {
 | 
					export const GET: RequestHandler = (): Response => {
 | 
				
			||||||
	return listResponse(games);
 | 
						return listResponse(games);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const POST: RequestHandler = (): Response => {
 | 
					export const POST: RequestHandler = async (): Promise<Response> => {
 | 
				
			||||||
	const newListing = createNewListing(new Game());
 | 
						const newListing = createNewListing(new Game());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await writeListing("games", newListing);
 | 
				
			||||||
	games.push(newListing);
 | 
						games.push(newListing);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return singleResponse(newListing.id);
 | 
						return singleResponse(newListing.id);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,15 @@
 | 
				
			|||||||
import { games } from "$lib/server/cache";
 | 
					import { games } from "$lib/server/cache";
 | 
				
			||||||
import { notFoundResponse, singleResponse } from "$lib/server/responseBodies";
 | 
					import { badRequestResponse, notFoundResponse, singleResponse } from "$lib/server/responseBodies";
 | 
				
			||||||
import type { RequestHandler } from "@sveltejs/kit";
 | 
					import type { RequestHandler } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const GET: RequestHandler = ({ params }): Response => {
 | 
					export const GET: RequestHandler = ({ params }): Response => {
 | 
				
			||||||
	const id = params["gameid"];
 | 
						const id = params["gameid"];
 | 
				
			||||||
	const game = games.find(({ id: gid }) => id === gid);
 | 
					
 | 
				
			||||||
 | 
						if (!id) {
 | 
				
			||||||
 | 
							return badRequestResponse("missing gameid parameter");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const game = games.find(({ id: gid }) => id === gid.toString());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!game) {
 | 
						if (!game) {
 | 
				
			||||||
		return notFoundResponse();
 | 
							return notFoundResponse();
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										53
									
								
								src/routes/api/token/+server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/routes/api/token/+server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					import { isListing } from "$lib/Listing";
 | 
				
			||||||
 | 
					import { isLoginData } from "$lib/Login";
 | 
				
			||||||
 | 
					import { readListingByQuery } from "$lib/server/mongo";
 | 
				
			||||||
 | 
					import { getRequestBody } from "$lib/server/requestTools";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						badRequestResponse,
 | 
				
			||||||
 | 
						notFoundResponse,
 | 
				
			||||||
 | 
						singleResponse,
 | 
				
			||||||
 | 
						unauthorizedResponse
 | 
				
			||||||
 | 
					} from "$lib/server/responseBodies";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					import { JWT_SECRET } from "$env/static/private";
 | 
				
			||||||
 | 
					import { compare } from "bcrypt";
 | 
				
			||||||
 | 
					import jwt from "jsonwebtoken";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const POST: RequestHandler = async ({ locals }): Promise<Response> => {
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							const { user } = locals;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (user.kind !== "Basic") {
 | 
				
			||||||
 | 
								return unauthorizedResponse();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const { username, password } = user.payload;
 | 
				
			||||||
 | 
							const listing = await readListingByQuery(
 | 
				
			||||||
 | 
								"logins",
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									"data.username": username
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								(target) => isListing(target, isLoginData)
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!listing) {
 | 
				
			||||||
 | 
								return notFoundResponse();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (await compare(password, listing.data.password)) {
 | 
				
			||||||
 | 
								const token = await jwt.sign(
 | 
				
			||||||
 | 
									{ sub: listing.id, username, role: listing.data.role },
 | 
				
			||||||
 | 
									JWT_SECRET,
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										expiresIn: "1d"
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return singleResponse(token);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return badRequestResponse("wrong password");
 | 
				
			||||||
 | 
						} catch (err) {
 | 
				
			||||||
 | 
							return badRequestResponse("username and password are required");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										40
									
								
								src/routes/api/users/+server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/routes/api/users/+server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					import { hashPassword, isLoginData } from "$lib/Login";
 | 
				
			||||||
 | 
					import { createNewListing } from "$lib/server/modifyListing";
 | 
				
			||||||
 | 
					import { writeListing } from "$lib/server/mongo";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						badRequestResponse,
 | 
				
			||||||
 | 
						forbiddenResponse,
 | 
				
			||||||
 | 
						serverErrorResponse,
 | 
				
			||||||
 | 
						singleResponse
 | 
				
			||||||
 | 
					} from "$lib/server/responseBodies";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const POST: RequestHandler = async ({ request }): Promise<Response> => {
 | 
				
			||||||
 | 
						let body: unknown;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						console.log("here");
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							body = await request.json();
 | 
				
			||||||
 | 
						} catch (err) {
 | 
				
			||||||
 | 
							console.log(err);
 | 
				
			||||||
 | 
							return badRequestResponse("body is required");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!isLoginData(body)) {
 | 
				
			||||||
 | 
							return badRequestResponse("body should contain username and password");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (body.role !== "player") {
 | 
				
			||||||
 | 
							return forbiddenResponse();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						body.password = await hashPassword(body.password);
 | 
				
			||||||
 | 
						const listing = createNewListing(body);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							await writeListing("logins", listing);
 | 
				
			||||||
 | 
							return singleResponse(listing.id);
 | 
				
			||||||
 | 
						} catch (err) {
 | 
				
			||||||
 | 
							return serverErrorResponse();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -1,24 +1,28 @@
 | 
				
			|||||||
GET http://localhost:5173/api
 | 
					@token=token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GET https://localhost:5173/api
 | 
				
			||||||
Accept: application/json
 | 
					Accept: application/json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
###
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
GET http://localhost:5173/api/games
 | 
					GET https://localhost:5173/api/games
 | 
				
			||||||
 | 
					Accept: application/json
 | 
				
			||||||
 | 
					Authorization: Bearer {{token}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					POST https://localhost:5173/api/games
 | 
				
			||||||
 | 
					Accept: application/json
 | 
				
			||||||
 | 
					Authorization: Bearer {{token}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GET https://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3
 | 
				
			||||||
Accept: application/json
 | 
					Accept: application/json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
###
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
POST http://localhost:5173/api/games
 | 
					PUT https://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3
 | 
				
			||||||
Accept: application/json
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
###
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
GET http://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3
 | 
					 | 
				
			||||||
Accept: application/json
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
###
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
PUT http://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3
 | 
					 | 
				
			||||||
Accept: application/json
 | 
					Accept: application/json
 | 
				
			||||||
Content-Type: application/json
 | 
					Content-Type: application/json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -30,12 +34,32 @@ Content-Type: application/json
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
###
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
POST http://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3/turns
 | 
					POST https://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3/turns
 | 
				
			||||||
Accept: application/json
 | 
					Accept: application/json
 | 
				
			||||||
Content-Type: application/json
 | 
					Content-Type: application/json
 | 
				
			||||||
 | 
					Authorization: Bearer {{token}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    "kind": "Roll",
 | 
					    "kind": "Roll",
 | 
				
			||||||
    "player": 2,
 | 
					    "player": 2,
 | 
				
			||||||
    "value": 4
 | 
					    "value": 4
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					POST https://localhost:5173/api/users
 | 
				
			||||||
 | 
					Accept: application/json
 | 
				
			||||||
 | 
					Content-Type: application/json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    "username": "worf",
 | 
				
			||||||
 | 
					    "password": "klingon",
 | 
				
			||||||
 | 
					    "role": "player"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					POST https://localhost:5173/api/token
 | 
				
			||||||
 | 
					Accept: application/json
 | 
				
			||||||
 | 
					Content-Type: application/json
 | 
				
			||||||
 | 
					Authorization: Basic worf:klingon
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user