From 4df2bc697dc7130fc019937c2cf3c49d1669cdc0 Mon Sep 17 00:00:00 2001 From: voidarc Date: Sun, 5 Jul 2026 13:46:16 +0100 Subject: [PATCH] style changes and stuff --- scripts/seed.ts | 3 ++- src/lib/server/password.ts | 24 +++++++++++++++++ src/routes/layout.css | 42 ++++++++++++++++------------- src/routes/login/+page.server.ts | 8 +++--- src/routes/register/+page.server.ts | 6 ++--- 5 files changed, 56 insertions(+), 27 deletions(-) create mode 100644 src/lib/server/password.ts diff --git a/scripts/seed.ts b/scripts/seed.ts index 05d2028..fae0b45 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -50,7 +50,8 @@ async function seed() { console.log('Database reset complete. Seeding...'); - let passwordHash = await Bun.password.hash('password'); + const { hash } = await import('../src/lib/server/password.js'); + let passwordHash = await hash('password'); await db .insert(schema.scorers) .values({ diff --git a/src/lib/server/password.ts b/src/lib/server/password.ts new file mode 100644 index 0000000..b62b052 --- /dev/null +++ b/src/lib/server/password.ts @@ -0,0 +1,24 @@ +import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto'; +import { promisify } from 'node:util'; + +const scryptAsync = promisify(scrypt); +const SALT_LEN = 32; +const KEY_LEN = 64; + +export async function hash(password: string): Promise { + const salt = randomBytes(SALT_LEN); + const derivedKey = (await scryptAsync(password, salt, KEY_LEN)) as Buffer; + return `${salt.toString('hex')}:${derivedKey.toString('hex')}`; +} + +export async function verify(password: string, stored: string): Promise { + const [saltHex, keyHex] = stored.split(':'); + if (!saltHex || !keyHex) return false; + + const salt = Buffer.from(saltHex, 'hex'); + const key = Buffer.from(keyHex, 'hex'); + const derivedKey = (await scryptAsync(password, salt, KEY_LEN)) as Buffer; + + if (key.length !== derivedKey.length) return false; + return timingSafeEqual(key, derivedKey); +} diff --git a/src/routes/layout.css b/src/routes/layout.css index 19b3639..eb1a6a8 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -101,6 +101,10 @@ align-items: center; padding: 0 14px; min-width: 0; + gap: 8px; +} +.runner .score-fg { + padding: 0 8px; } .score-meta { display: flex; @@ -120,20 +124,13 @@ text-shadow: 0 0 6px rgb(0 0 0 / 50%); } .score-name { - font-size: clamp(20px, 5.5vw, 40px); + font-size: clamp(18px, 3.5vw, 22px); white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; } .score-pts { font-size: clamp(20px, 5.5vw, 40px); letter-spacing: 2px; - position: absolute; - right: 14px; - top: 50%; - transform: translateY(-50%); - z-index: 2; + flex-shrink: 0; text-shadow: 0 0 8px rgb(0 0 0 / 80%), 0 0 4px rgb(0 0 0 / 60%); } .winner .score-meta { @@ -147,7 +144,6 @@ } .winner .score-name { font-size: clamp(34px, 9vw, 64px); - max-width: 70%; } .winner .score-pts { font-size: clamp(34px, 9vw, 64px); @@ -163,42 +159,52 @@ .score-rank { margin-bottom: 0; } - .score-name { - font-size: clamp(18px, 5vw, 28px); + .runner .score-rank { + font-size: clamp(16px, 3.5vw, 20px); + } + .runner .score-name { + font-size: clamp(20px, 5vw, 26px); } .score-pts { font-size: clamp(22px, 6vw, 34px); } .winner .score-name { font-size: clamp(26px, 7vw, 42px); - max-width: 60%; } .winner .score-pts { font-size: clamp(28px, 8vw, 48px); } } -/* Responsive runners-up: 1 col <480px, 2 cols 480-699px, 4 cols ≥700px */ +/* Responsive runners-up: 1 col <480px, 2 cols 480-950px, 4 cols >950px */ .runners-grid { display: grid; gap: 10px; grid-template-columns: 1fr; transition: gap 0.3s ease; } -@media (min-width: 480px) { +@media (min-width: 480px) and (max-width: 950px) { .runners-grid { grid-template-columns: repeat(2, 1fr); } .winner { - aspect-ratio: 2 / 1; + aspect-ratio: 12 / 5; } .runner { aspect-ratio: 2 / 1; } + + .runner .score-rank { + font-size: clamp(18px, 3vw, 22px); + } + + .runner .score-name { + font-size: clamp(24px, 4vw, 30px); + } } -@media (min-width: 700px) { +@media (min-width: 951px) { .runners-grid { /* Use --runner-count so fewer teams fill the row evenly */ grid-template-columns: repeat(min(var(--runner-count), 4), 1fr); @@ -378,7 +384,7 @@ } } -@media (min-width: 700px) { +@media (min-width: 951px) { .player-box { max-width: calc(25% - 6px); } diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts index d308e43..485ab3e 100644 --- a/src/routes/login/+page.server.ts +++ b/src/routes/login/+page.server.ts @@ -1,12 +1,10 @@ -// src/routes/login/+page.server.ts import { fail, redirect } from '@sveltejs/kit'; import { eq } from 'drizzle-orm'; import { db } from '$lib/server/db'; import { scorers } from '$lib/server/db/schema'; import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth'; -import type { Actions } from '../login/$types'; - -import type { PageServerLoad } from '../login/$types'; +import { verify } from '$lib/server/password'; +import type { Actions, PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ locals }) => { return { user: locals.user }; @@ -19,7 +17,7 @@ export const actions: Actions = { const password = data.get('password') as string; const [user] = await db.select().from(scorers).where(eq(scorers.username, username)); - if (!user || !(await Bun.password.verify(password, user.passwordHash))) { + if (!user || !(await verify(password, user.passwordHash))) { return fail(400, { message: 'Invalid credentials' }); } diff --git a/src/routes/register/+page.server.ts b/src/routes/register/+page.server.ts index 54434d3..06a7401 100644 --- a/src/routes/register/+page.server.ts +++ b/src/routes/register/+page.server.ts @@ -1,11 +1,11 @@ -// src/routes/signup/+page.server.ts import { fail, redirect } from '@sveltejs/kit'; import { eq } from 'drizzle-orm'; import { db } from '$lib/server/db'; import { scorers } from '$lib/server/db/schema'; import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth'; +import { hash } from '$lib/server/password'; import { getSetting } from '$lib/server/databaseManager'; -import type { PageServerLoad, Actions } from '../signup/$types'; +import type { PageServerLoad, Actions } from './$types'; export const load: PageServerLoad = async () => { const disabled = (await getSetting('disableRegistration')) === 'true'; @@ -32,7 +32,7 @@ export const actions: Actions = { return fail(400, { message: 'Username already taken' }); } - const passwordHash = await Bun.password.hash(password); + const passwordHash = await hash(password); const userId = crypto.randomUUID(); await db.insert(scorers).values({ id: userId, username, passwordHash });