added login with pages and database, also correct errors in score submit

This commit is contained in:
2026-06-22 13:36:34 +01:00
parent 192564fb79
commit fbc181c890
42 changed files with 31351 additions and 1599 deletions

View File

@@ -1 +1 @@
/nix/store/qfli8mq0fxc3lfj1w7yv00zgf8n3fa6c-devenv-shell /nix/store/kap8myx3a7djmgsaaspb46azzkhyrnmn-devenv-shell

View File

@@ -1 +1 @@
/nix/store/vsp3fis0yyfrw5zdw1r908cz5wkwy1qs-tasks.json /nix/store/2f9x4skfyh2x0rkfacr2w0v3c2vm405w-tasks.json

View File

@@ -7,4 +7,3 @@
/home/user01/Projects/score-system/devenv.yaml /home/user01/Projects/score-system/devenv.yaml
/home/user01/.config/nixpkgs/overlays /home/user01/.config/nixpkgs/overlays
/home/user01/.config/nixpkgs/overlays.nix /home/user01/.config/nixpkgs/overlays.nix
/home/user01/Projects/score-system/.env

Binary file not shown.

Binary file not shown.

2258
.devenv/shell-20cef00395041120.sh Executable file

File diff suppressed because it is too large Load Diff

2258
.devenv/shell-2b51e7588d77ce70.sh Executable file

File diff suppressed because it is too large Load Diff

2258
.devenv/shell-3503282b7604ae54.sh Executable file

File diff suppressed because it is too large Load Diff

2258
.devenv/shell-3ccc9ebdaaad8b3f.sh Executable file

File diff suppressed because it is too large Load Diff

2258
.devenv/shell-4a9089e6e3ce3083.sh Executable file

File diff suppressed because it is too large Load Diff

2258
.devenv/shell-50ea0b87d2caf049.sh Executable file

File diff suppressed because it is too large Load Diff

2265
.devenv/shell-93a57e7732b1fa8a.sh Executable file

File diff suppressed because it is too large Load Diff

2258
.devenv/shell-9b7949b96e5561ee.sh Executable file

File diff suppressed because it is too large Load Diff

2258
.devenv/shell-ad083a082e46f0e9.sh Executable file

File diff suppressed because it is too large Load Diff

2258
.devenv/shell-bc17db56c7f8a19a.sh Executable file

File diff suppressed because it is too large Load Diff

2258
.devenv/shell-d2d0e9bf7bb10942.sh Executable file

File diff suppressed because it is too large Load Diff

2258
.devenv/shell-deca24b8fe0d1abe.sh Executable file

File diff suppressed because it is too large Load Diff

2258
.devenv/shell-e8cdd2fa2b8a2526.sh Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -23,7 +23,7 @@ autoload -Uz add-zsh-hook
__devenv_reload_apply() { __devenv_reload_apply() {
# Source new environment if a reload is pending # Source new environment if a reload is pending
if [ -f "/tmp/devenv-reload-6816.sh" ]; then if [ -f "/tmp/nix-shell-28554-2490132097/devenv-reload-30583.sh" ]; then
# Shell out to bash to handle the env diff (bash syntax) # Shell out to bash to handle the env diff (bash syntax)
local reload_output local reload_output
reload_output=$(bash -c ' reload_output=$(bash -c '
@@ -158,8 +158,8 @@ _before=$(mktemp)
__devenv_capture_env > "$_before" __devenv_capture_env > "$_before"
# Source new devenv environment # Source new devenv environment
source "/tmp/devenv-reload-6816.sh" source "/tmp/nix-shell-28554-2490132097/devenv-reload-30583.sh"
rm -f "/tmp/devenv-reload-6816.sh" rm -f "/tmp/nix-shell-28554-2490132097/devenv-reload-30583.sh"
# Compute new diff # Compute new diff
__devenv_compute_diff "$_before" __devenv_compute_diff "$_before"

15
; Normal file
View File

@@ -0,0 +1,15 @@
{ pkgs, config, ... }: {
languages.typescript.enable = true;
dotenv.disableHint = true;
packages = with pkgs; [
bun
eslint_d
];
env.DEVSHELL_NAME = "󰏖 devenv/#fab387| Bun/yellow";
processes = {
server = {
ports.http.allocate = 5173;
exec = "bun run dev";
};
};
}

View File

@@ -6,6 +6,8 @@
"name": "score-system", "name": "score-system",
"dependencies": { "dependencies": {
"@catppuccin/tailwindcss": "^1.0.0", "@catppuccin/tailwindcss": "^1.0.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"csv-parse": "^6.2.1", "csv-parse": "^6.2.1",
"prettier-plugin-svelte": "^3.5.1", "prettier-plugin-svelte": "^3.5.1",
}, },
@@ -20,7 +22,7 @@
"@tailwindcss/forms": "^0.5.11", "@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"@types/bun": "^1.3.13", "@types/bun": "^1.3.14",
"@types/node": "^22", "@types/node": "^22",
"drizzle-kit": "^0.31.10", "drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
@@ -31,7 +33,7 @@
"svelte": "^5.55.2", "svelte": "^5.55.2",
"svelte-check": "^4.4.6", "svelte-check": "^4.4.6",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",
"typescript": "^6.0.2", "typescript": "^6.0.3",
"typescript-eslint": "^8.58.1", "typescript-eslint": "^8.58.1",
"vite": "^8.0.7", "vite": "^8.0.7",
}, },
@@ -266,6 +268,14 @@
"@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
"@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="],
"@oslojs/crypto": ["@oslojs/crypto@1.0.1", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="],
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
@@ -350,7 +360,7 @@
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
@@ -422,7 +432,7 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],

View File

@@ -1,11 +1,15 @@
{ pkgs, config, ... }: { { pkgs, config, ... }: {
languages.typescript.enable = true; languages.typescript.enable = true;
dotenv.disableHint = true;
packages = with pkgs; [ packages = with pkgs; [
bun bun
eslint_d eslint_d
]; ];
env.DEVSHELL_NAME = "󰏖 devenv/#fab387| Bun/yellow"; env.DEVSHELL_NAME = "󰏖 devenv/#fab387| Bun/yellow";
processes = { processes = {
server.exec = "bun run dev"; server = {
ports.http.allocate = 5173;
exec = "bun --bun run dev";
};
}; };
} }

View File

@@ -30,7 +30,7 @@
"@tailwindcss/forms": "^0.5.11", "@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"@types/bun": "^1.3.13", "@types/bun": "^1.3.14",
"@types/node": "^22", "@types/node": "^22",
"drizzle-kit": "^0.31.10", "drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
@@ -41,12 +41,14 @@
"svelte": "^5.55.2", "svelte": "^5.55.2",
"svelte-check": "^4.4.6", "svelte-check": "^4.4.6",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",
"typescript": "^6.0.2", "typescript": "^6.0.3",
"typescript-eslint": "^8.58.1", "typescript-eslint": "^8.58.1",
"vite": "^8.0.7" "vite": "^8.0.7"
}, },
"dependencies": { "dependencies": {
"@catppuccin/tailwindcss": "^1.0.0", "@catppuccin/tailwindcss": "^1.0.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"csv-parse": "^6.2.1", "csv-parse": "^6.2.1",
"prettier-plugin-svelte": "^3.5.1" "prettier-plugin-svelte": "^3.5.1"
} }

View File

@@ -1,8 +1,8 @@
event_type,division,event_state,winner,time_completed event_type,division,event_state,winner,time_completed
100m Sprint,Year 7,0,, 100m Sprint,Year 7,0,,
100m Sprint,Year 8,0,, 100m Sprint,Year 8,0,,
100m Sprint,Year 9,2,East, 100m Sprint,Year 9,0,,
100m Sprint,Year 10,1,, 100m Sprint,Year 10,0,,
200m Sprint,Year 7,0,, 200m Sprint,Year 7,0,,
200m Sprint,Year 8,0,, 200m Sprint,Year 8,0,,
200m Sprint,Year 9,0,, 200m Sprint,Year 9,0,,
1 event_type division event_state winner time_completed
2 100m Sprint Year 7 0
3 100m Sprint Year 8 0
4 100m Sprint Year 9 2 0 East
5 100m Sprint Year 10 1 0
6 200m Sprint Year 7 0
7 200m Sprint Year 8 0
8 200m Sprint Year 9 0

View File

@@ -59,11 +59,11 @@ Dante Sorensen,1000m Long Distance,0,C
Mia Nguyen,1000m Long Distance,0,C Mia Nguyen,1000m Long Distance,0,C
Oluseun Alade,1000m Long Distance,0,C Oluseun Alade,1000m Long Distance,0,C
Tobias Crane,1000m Long Distance,0,C Tobias Crane,1000m Long Distance,0,C
Alaric Olawale,100m Sprint,1,A Alaric Olawale,100m Sprint,0,A
Ava Mackenzie,100m Sprint,2,A Ava Mackenzie,100m Sprint,0,A
Bjorn Oyelaran,100m Sprint,3,A Bjorn Oyelaran,100m Sprint,0,A
Jack Simmons,100m Sprint,4,A Jack Simmons,100m Sprint,0,A
Kai Johansson,100m Sprint,5,A Kai Johansson,100m Sprint,0,A
Clark Kent,100m Sprint,0,B Clark Kent,100m Sprint,0,B
Cormac Halvorsen,100m Sprint,0,B Cormac Halvorsen,100m Sprint,0,B
Marcus Okafor,100m Sprint,0,B Marcus Okafor,100m Sprint,0,B
1 player_registered event_registered player_placement bracket
59 Mia Nguyen 1000m Long Distance 0 C
60 Oluseun Alade 1000m Long Distance 0 C
61 Tobias Crane 1000m Long Distance 0 C
62 Alaric Olawale 100m Sprint 1 0 A
63 Ava Mackenzie 100m Sprint 2 0 A
64 Bjorn Oyelaran 100m Sprint 3 0 A
65 Jack Simmons 100m Sprint 4 0 A
66 Kai Johansson 100m Sprint 5 0 A
67 Clark Kent 100m Sprint 0 B
68 Cormac Halvorsen 100m Sprint 0 B
69 Marcus Okafor 100m Sprint 0 B

View File

@@ -1,7 +1,9 @@
preset,placement,points preset,placement,points
1,1,10 1,1,5
1,2,7 1,2,4
1,3,5 1,3,3
1,4,2
1,5,1
2,1,50 2,1,50
2,2,30 2,2,30
2,3,10 2,3,10
1 preset placement points
2 1 1 10 5
3 1 2 7 4
4 1 3 5 3
5 1 4 2
6 1 5 1
7 2 1 50
8 2 2 30
9 2 3 10

View File

@@ -43,11 +43,18 @@ async function seed() {
await db.delete(schema.players); await db.delete(schema.players);
await db.delete(schema.divisions); await db.delete(schema.divisions);
await db.delete(schema.teams); await db.delete(schema.teams);
await db.delete(schema.scorers);
await db.delete(schema.sessions);
await client.execute('DELETE FROM sqlite_sequence'); await client.execute('DELETE FROM sqlite_sequence');
console.log('Database reset complete. Seeding...'); console.log('Database reset complete. Seeding...');
let passwordHash = await Bun.password.hash('password');
await db
.insert(schema.scorers)
.values({ id: crypto.randomUUID(), username: 'admin', passwordHash: passwordHash });
// --- 1. Teams --- // --- 1. Teams ---
const teamsCSV = readCSV('teams.csv'); const teamsCSV = readCSV('teams.csv');
for (const row of teamsCSV) { for (const row of teamsCSV) {

View File

@@ -0,0 +1,31 @@
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import {
getSessionToken,
validateSessionToken,
setSessionTokenCookie,
deleteSessionTokenCookie
} from '$lib/server/auth';
export const handle: Handle = async ({ event, resolve }) => {
const token = getSessionToken(event);
if (!token) {
event.locals.user = null;
event.locals.session = null;
return resolve(event);
}
const { session, user } = await validateSessionToken(token);
if (session) {
setSessionTokenCookie(event, token, session.expiresAt); // refresh cookie expiry
} else {
deleteSessionTokenCookie(event);
}
event.locals.user = user;
event.locals.session = session;
return resolve(event);
};

73
src/lib/server/auth.ts Normal file
View File

@@ -0,0 +1,73 @@
// src/lib/server/auth.ts
import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db';
import { sessions, scorers as scorers } from '$lib/server/db/schema';
import type { RequestEvent } from '@sveltejs/kit';
const DAY_MS = 1000 * 60 * 60 * 24;
const SESSION_COOKIE = 'session';
export function generateSessionToken(): string {
const bytes = new Uint8Array(20);
crypto.getRandomValues(bytes);
return encodeBase32LowerCaseNoPadding(bytes);
}
export async function createSession(token: string, userId: string) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const expiresAt = new Date(Date.now() + DAY_MS * 30);
await db.insert(sessions).values({ id: sessionId, userId, expiresAt });
return { id: sessionId, userId, expiresAt };
}
export async function validateSessionToken(token: string) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const [row] = await db
.select({ session: sessions, user: scorers })
.from(sessions)
.innerJoin(scorers, eq(sessions.userId, scorers.id))
.where(eq(sessions.id, sessionId));
if (!row) return { session: null, user: null };
// Expired — clean up and reject
if (Date.now() >= row.session.expiresAt.getTime()) {
await db.delete(sessions).where(eq(sessions.id, sessionId));
return { session: null, user: null };
}
// Sliding expiration: renew if past the halfway mark
if (Date.now() >= row.session.expiresAt.getTime() - DAY_MS * 15) {
const newExpiresAt = new Date(Date.now() + DAY_MS * 30);
await db.update(sessions).set({ expiresAt: newExpiresAt }).where(eq(sessions.id, sessionId));
row.session.expiresAt = newExpiresAt;
}
return { session: row.session, user: { id: row.user.id, username: row.user.username } };
}
export async function invalidateSession(sessionId: string) {
await db.delete(sessions).where(eq(sessions.id, sessionId));
}
export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date) {
event.cookies.set(SESSION_COOKIE, token, {
expires: expiresAt,
path: '/',
httpOnly: true,
secure: !import.meta.env.DEV, // allow http in local dev
sameSite: 'lax'
});
}
export function deleteSessionTokenCookie(event: RequestEvent) {
event.cookies.delete(SESSION_COOKIE, { path: '/' });
}
export function getSessionToken(event: RequestEvent) {
return event.cookies.get(SESSION_COOKIE) ?? null;
}

View File

@@ -1,6 +1,21 @@
import { sql, eq } from 'drizzle-orm'; import { sql, eq } from 'drizzle-orm';
import { integer, sqliteTable, text, sqliteView } from 'drizzle-orm/sqlite-core'; import { integer, sqliteTable, text, sqliteView } from 'drizzle-orm/sqlite-core';
export const scorers = sqliteTable('users', {
id: text('id').primaryKey(),
username: text('username').notNull().unique(),
role: text('scorer_role').notNull().default('scorer'),
passwordHash: text('password_hash').notNull()
});
export const sessions = sqliteTable('sessions', {
id: text('id').primaryKey(), // SHA-256 hash of the token, never the raw token
userId: text('user_id')
.notNull()
.references(() => scorers.id),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull()
});
export const players = sqliteTable('players', { export const players = sqliteTable('players', {
id: integer('players_id').primaryKey({ autoIncrement: true }), id: integer('players_id').primaryKey({ autoIncrement: true }),
firstName: text('firstName').notNull(), firstName: text('firstName').notNull(),

View File

@@ -10,12 +10,17 @@ export async function POST({ request }: any) {
// If there is no request then dont respond // If there is no request then dont respond
if (!responseBody) { if (!responseBody) {
return new Response('nuh uh'); return new Error('nuh uh');
} else { } else {
console.log(JSON.stringify(responseBody)); console.log(JSON.stringify(responseBody));
if (responseBody.eventId) { if (responseBody.eventId) {
// Get the event // Get the event
let eventData = await getRegisteredEvents(responseBody.eventId); let eventData = await getRegisteredEvents(responseBody.eventId);
// If the event hasnt started or ended
if (eventData.events[0].state != 1) {
return new Error();
}
let scoringPreset = eventData.events[0].scoringPreset; let scoringPreset = eventData.events[0].scoringPreset;
console.log(scoringPreset); console.log(scoringPreset);
@@ -56,6 +61,7 @@ export async function POST({ request }: any) {
if (currentPlayer.scores.length > 0) { if (currentPlayer.scores.length > 0) {
if (score > 0) { if (score > 0) {
// put the scores on the board baby // put the scores on the board baby
// THIS SHOULDNT BE REFERENCED THIS IS INTENDED
let newScoreLedgerEntry = await db let newScoreLedgerEntry = await db
.insert(schema.scoreLedger) .insert(schema.scoreLedger)
.values({ ledgerID: ledgerEntryId, teamID: currentPlayerTeam, points: score }); .values({ ledgerID: ledgerEntryId, teamID: currentPlayerTeam, points: score });
@@ -74,6 +80,6 @@ export async function POST({ request }: any) {
// Update the frontends // Update the frontends
globalEmitter.emit('scoreUpdate'); globalEmitter.emit('scoreUpdate');
// Return a resonse because // Return a resonse because
return new Response('coolsies uh'); return new Response('coolsies');
} }
} }

View File

@@ -0,0 +1,9 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user) {
throw redirect(303, '/login');
}
return { user: locals.user };
};

View File

@@ -196,6 +196,7 @@
})) }))
}) })
}); });
console.log(res);
if (res.ok) { if (res.ok) {
localStorage.removeItem(`scores-${eventId}`); localStorage.removeItem(`scores-${eventId}`);
localStorage.removeItem(`sortByScore-${eventId}`); localStorage.removeItem(`sortByScore-${eventId}`);

View File

@@ -0,0 +1,26 @@
// 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 './$types';
export const actions: Actions = {
default: async (event) => {
const data = await event.request.formData();
const username = data.get('username') as string;
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))) {
return fail(400, { message: 'Invalid credentials' });
}
const token = generateSessionToken();
const session = await createSession(token, user.id);
setSessionTokenCookie(event, token, session.expiresAt);
throw redirect(303, '/');
}
};

View File

@@ -0,0 +1,72 @@
<script lang="ts">
import type { ActionData } from './$types';
export let form: ActionData;
</script>
<div class="auth-card">
<h1>Log in</h1>
<form method="POST">
<label>
Username
<input name="username" type="text" autocomplete="username" required />
</label>
<label>
Password
<input name="password" type="password" autocomplete="current-password" required />
</label>
{#if form?.message}
<p class="error">{form.message}</p>
{/if}
<button type="submit">Log in</button>
</form>
<p class="switch">No account? <a href="/signup">Sign up</a></p>
</div>
<style>
.auth-card {
max-width: 320px;
margin: 4rem auto;
padding: 2rem;
border: 1px solid #ddd;
border-radius: 8px;
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
}
label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.9rem;
}
input {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
padding: 0.6rem;
border: none;
border-radius: 4px;
background: #333;
color: #fff;
cursor: pointer;
}
.error {
color: #c0392b;
font-size: 0.85rem;
margin: 0;
}
.switch {
margin-top: 1rem;
font-size: 0.85rem;
text-align: center;
}
</style>

View File

@@ -0,0 +1,35 @@
// 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 type { Actions } from './$types';
export const actions: Actions = {
default: async (event) => {
const data = await event.request.formData();
const username = data.get('username') as string;
const password = data.get('password') as string;
if (!username || !password || password.length < 8) {
return fail(400, { message: 'Username and an 8+ character password are required' });
}
const [existing] = await db.select().from(scorers).where(eq(scorers.username, username));
if (existing) {
return fail(400, { message: 'Username already taken' });
}
const passwordHash = await Bun.password.hash(password); // defaults to argon2id
const userId = crypto.randomUUID();
await db.insert(scorers).values({ id: userId, username, passwordHash });
const token = generateSessionToken();
const session = await createSession(token, userId);
setSessionTokenCookie(event, token, session.expiresAt);
throw redirect(303, '/');
}
};

View File

@@ -0,0 +1,76 @@
<script lang="ts">
import type { ActionData } from './$types';
export let form: ActionData;
</script>
<div class="auth-card">
<h1>Sign up</h1>
<form method="POST">
<label>
Username
<input name="username" type="text" autocomplete="username" required />
</label>
<label>
Password
<input name="password" type="password" autocomplete="new-password" minlength="8" required />
<small>At least 8 characters</small>
</label>
{#if form?.message}
<p class="error">{form.message}</p>
{/if}
<button type="submit">Create account</button>
</form>
<p class="switch">Already have an account? <a href="/login">Log in</a></p>
</div>
<style>
.auth-card {
max-width: 320px;
margin: 4rem auto;
padding: 2rem;
border: 1px solid #ddd;
border-radius: 8px;
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
}
label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.9rem;
}
input {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
small {
color: #888;
}
button {
padding: 0.6rem;
border: none;
border-radius: 4px;
background: #333;
color: #fff;
cursor: pointer;
}
.error {
color: #c0392b;
font-size: 0.85rem;
margin: 0;
}
.switch {
margin-top: 1rem;
font-size: 0.85rem;
text-align: center;
}
</style>

View File

@@ -5,14 +5,22 @@
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"esModuleInterop": true, "esModuleInterop": true,
"types": ["bun"], "types": ["bun", "node"],
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"moduleResolution": "bundler" "moduleResolution": "bundler"
} },
"include": [
"src/**/*.js",
"src/**/*.ts",
"src/**/*.svelte",
"scripts/**/*.ts"
]
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
// //

View File

@@ -2,4 +2,11 @@ import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
export default defineConfig({ plugins: [tailwindcss(), sveltekit()] }); export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
server: {
watch: {
ignored: ['**/.devenv/**']
}
}
});