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