diff --git a/src/lib/cookie.ts b/src/lib/cookie.ts index c26031cfcd620ea034d251333a813860f5c79db3..80b9420e4cf0da9b3c3391fec60b2175ca7add45 100644 --- a/src/lib/cookie.ts +++ b/src/lib/cookie.ts @@ -6,26 +6,30 @@ import { encode, decode } from 'msgpackr'; const cryptr = new Cryptr(env.COOKIE_SECRET); -export function secureCookie<T>(cookies: Cookies, schema: Schema<T>, name: string) { - function read(): T | null { - const cookie = cookies.get(name); +export class SecureCookie<T> { + constructor( + protected cookies: Cookies, + protected schema: Schema<T>, + readonly name: string + ) {} + + read(): T | null { + const cookie = this.cookies.get(this.name); if (!cookie) return null; try { const decryptedBuffer = cryptr.decrypt(cookie); const json = decode(decryptedBuffer); - const data = schema.parse(json); + const data = this.schema.parse(json); return data; } catch { return null; } } - function write(data: T) { + write(data: T) { const compactJson = encode(data); const encryptedBuffer = cryptr.encrypt(compactJson); - cookies.set(name, encryptedBuffer, { path: '/' }); + this.cookies.set(this.name, encryptedBuffer, { path: '/' }); } - - return { read, write }; } diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index 46c60716941a3e59b7435010bf9460ab4536d748..44b85a8d4d5135fc811fe3b3354eac0b705a1128 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -4,43 +4,37 @@ import crypto from 'crypto'; export default class Cryptr { algorithm = 'aes-128-gcm' as const; encoding = 'base64' as const; - pbkdf2Iterations = 100000; - saltLength = 64; ivLength = 16; tagLength = 16; - tagPosition = this.saltLength + this.ivLength; - encryptedPosition = this.tagPosition + this.tagLength; + encryptedPosition = this.ivLength + this.tagLength; constructor(public secret: string) {} - getKey(salt: crypto.BinaryLike) { - // return crypto.pbkdf2Sync(this.secret, salt, this.pbkdf2Iterations, 16, 'sha512'); - return crypto.randomBytes(16); + getKey() { + return crypto.createHash('sha256').update(this.secret).digest().subarray(0, 16); } - encrypt(value: crypto.BinaryLike) { + encrypt(value: crypto.BinaryLike): string { const iv = crypto.randomBytes(this.ivLength); - const salt = crypto.randomBytes(this.saltLength); - const key = this.getKey(salt); + const key = this.getKey(); const cipher = crypto.createCipheriv(this.algorithm, key, iv); const encrypted = Buffer.concat([cipher.update(value), cipher.final()]); const tag = cipher.getAuthTag(); - return Buffer.concat([salt, iv, tag, encrypted]).toString(this.encoding); + return Buffer.concat([iv, tag, encrypted]).toString(this.encoding); } - decrypt(value: string) { + decrypt(value: string): Buffer { const stringValue = Buffer.from(String(value), this.encoding); - const salt = stringValue.subarray(0, this.saltLength); - const iv = stringValue.subarray(this.saltLength, this.tagPosition); - const tag = stringValue.subarray(this.tagPosition, this.encryptedPosition); + const iv = stringValue.subarray(0, this.ivLength); + const tag = stringValue.subarray(this.ivLength, this.encryptedPosition); const encrypted = stringValue.subarray(this.encryptedPosition); - const key = this.getKey(salt); + const key = this.getKey(); const decipher = crypto.createDecipheriv(this.algorithm, key, iv); diff --git a/src/lib/game.ts b/src/lib/game.ts index b403af40902f152a26d431d234bbdb1f1ae7a603..5ad81ad880d41462c91bf09c702b0f15d5e8fb7d 100644 --- a/src/lib/game.ts +++ b/src/lib/game.ts @@ -1,5 +1,5 @@ import type { Cookies } from '@sveltejs/kit'; -import { secureCookie } from './cookie'; +import { SecureCookie } from './cookie'; import { z } from 'zod'; export enum GameStage { @@ -20,35 +20,35 @@ export const gameStateSchema = z.object({ export type GameState = z.infer<typeof gameStateSchema>; -const defaultGameState: GameState = { - stage: GameStage.NEXT, - history: [], - step: 0, - score: 0, - options: [], - solution: '' -}; +export class Game { + state: GameState; + protected cookie_name = 'qui-est-ce'; + protected cookie: SecureCookie<GameState>; -export function loadGameState(cookies: Cookies) { - const COOKIE_NAME = 'qui-est-ce'; - const cookie = secureCookie(cookies, gameStateSchema, COOKIE_NAME); + constructor(protected cookies: Cookies) { + this.cookie = new SecureCookie(cookies, gameStateSchema, this.cookie_name); + this.state = this.cookie.read() ?? this.defaultState(); + } - const gameState = cookie.read() ?? defaultGameState; + protected defaultState(): GameState { + return { + stage: GameStage.NEXT, + history: [], + step: 0, + score: 0, + options: [], + solution: '' + }; + } - function saveGame() { - cookie.write(gameState); + save() { + this.cookie.write(this.state); } - function resetGame() { - if (gameState.stage === GameStage.GAME_OVER) { - Object.assign(gameState, defaultGameState); - saveGame(); + reset() { + if (this.state.stage === GameStage.GAME_OVER) { + this.state = this.defaultState(); + this.save(); } } - - return { - gameState, - saveGame, - resetGame - }; } diff --git a/src/routes/quiz/+page.server.ts b/src/routes/quiz/+page.server.ts index f96f04cc12e47c755a7bd1ee2fa984ae65dac284..19af07168aa66fd727eb7c989df84feb3e034775 100644 --- a/src/routes/quiz/+page.server.ts +++ b/src/routes/quiz/+page.server.ts @@ -5,7 +5,7 @@ import { fail, redirect } from '@sveltejs/kit'; import { GetPromotionStore, UserDetailsStore } from '$houdini'; import { pageIterator } from '$lib/graphql/query'; import { getRandomItems } from '$lib/utils'; -import { GameStage, loadGameState } from '$lib/game'; +import { GameStage, Game } from '$lib/game'; type Option = { value: string; @@ -13,28 +13,28 @@ type Option = { }; export async function load(event) { - let { gameState, saveGame } = loadGameState(event.cookies); + const game = new Game(event.cookies); - if (gameState.stage === GameStage.GAME_OVER) redirect(303, '/quiz/game-over'); + if (game.state.stage === GameStage.GAME_OVER) redirect(303, '/quiz/game-over'); - if (gameState.stage === GameStage.NEXT) { + if (game.state.stage === GameStage.NEXT) { const pagination = pageIterator(event, GetPromotionStore, { promotion: 2023 }); const promotion = await Array.fromAsync(pagination); const all = new Set(promotion.map((p) => p.id)); - const previous = new Set(gameState.history); + const previous = new Set(game.state.history); const available = all.difference(previous); - gameState.options = getRandomItems(Array.from(available), 4); - gameState.solution = getRandomItems([...gameState.options], 1)[0]; - gameState.stage = GameStage.PLAYING; - gameState.step++; + game.state.options = getRandomItems(Array.from(available), 4); + game.state.solution = getRandomItems([...game.state.options], 1)[0]; + game.state.stage = GameStage.PLAYING; + game.state.step++; } const details = await new UserDetailsStore().fetch({ event, - variables: { idList: gameState.options } + variables: { idList: game.state.options } }); if (details.errors) { @@ -45,51 +45,47 @@ export async function load(event) { const options: Option[] = users.map((node) => ({ label: node.nickname!, value: node.id })) ?? []; - console.log(gameState); + game.save(); - saveGame(); - - const photo = users.find((node) => node.id === gameState.solution)?.photo!; + const photo = users.find((node) => node.id === game.state.solution)?.photo!; const form = await superValidate(zod(schema)); - - return { form, options, score: gameState.score, step: gameState.step, photo }; + return { form, options, score: game.state.score, step: game.state.step, photo }; } export const actions = { async results(event) { const form = await superValidate(event, zod(schema)); - console.log(form); if (!form.valid) { return fail(400, { form }); } - let { gameState, saveGame } = loadGameState(event.cookies); + const state = new Game(event.cookies); - if (form.data.choice === gameState.solution) { - gameState.score += 10; + if (form.data.choice === state.state.solution) { + state.state.score += 10; } - gameState.history.push(gameState.solution); - saveGame(); + state.state.history.push(state.state.solution); + state.save(); return { form, - solution: gameState.solution + solution: state.state.solution }; }, async next(event) { const form = await superValidate(zod(schema)); - let { gameState, saveGame } = loadGameState(event.cookies); + const game = new Game(event.cookies); - if (gameState.step >= 10) { - gameState.stage = GameStage.GAME_OVER; + if (game.state.step >= 10) { + game.state.stage = GameStage.GAME_OVER; } else { - gameState.stage = GameStage.NEXT; + game.state.stage = GameStage.NEXT; } - saveGame(); + game.save(); return { form }; } diff --git a/src/routes/quiz/game-over/+page.server.ts b/src/routes/quiz/game-over/+page.server.ts index e4f80ae32dbfca88766f26cb85e0c18741f48472..4c65ae412285cd0f9d7aa1ec650b175ceffe8de5 100644 --- a/src/routes/quiz/game-over/+page.server.ts +++ b/src/routes/quiz/game-over/+page.server.ts @@ -1,18 +1,18 @@ -import { GameStage, loadGameState } from '$lib/game'; +import { GameStage, Game } from '$lib/game'; import { redirect } from '@sveltejs/kit'; export async function load(event) { - let { gameState } = loadGameState(event.cookies); + const game = new Game(event.cookies); - if (gameState.stage !== GameStage.GAME_OVER) redirect(303, '.'); + if (game.state.stage !== GameStage.GAME_OVER) redirect(303, '/quiz'); - return { score: gameState.score }; + return { score: game.state.score }; } export const actions = { async default(event) { - let { resetGame } = loadGameState(event.cookies); + const game = new Game(event.cookies); - resetGame(); + game.reset(); } }; diff --git a/src/routes/quiz/game-over/+page.svelte b/src/routes/quiz/game-over/+page.svelte index 528fb154a94713758ec7b482f678e21cb4c86a39..46bcb4709e05454004e3ea7b320e1dfd1f6df92c 100644 --- a/src/routes/quiz/game-over/+page.svelte +++ b/src/routes/quiz/game-over/+page.svelte @@ -1,4 +1,6 @@ <script lang="ts"> + import { enhance } from '$app/forms'; + export let data; </script> @@ -11,12 +13,13 @@ <span class="translate-y-[-30px] text-9xl text-[#fa9f9b]">{data.score}</span> pts </span> - <form method="post"> + <form method="post" use:enhance> <button type="submit" class="cursor-pointer rounded-2xl border-transparent bg-[#f32c22] px-[1.5em] py-[0.5em] text-2xl text-[#333] transition-[0.35s] hover:bg-[#333] hover:text-[#f65a52] focus:border focus:border-dotted focus:border-[#f87f79] focus:[outline:none]" - >Rejouer</button > + Rejouer + </button> </form> </section> </div>