added login with pages and database, also correct errors in score submit
This commit is contained in:
@@ -1 +1 @@
|
||||
/nix/store/qfli8mq0fxc3lfj1w7yv00zgf8n3fa6c-devenv-shell
|
||||
/nix/store/kap8myx3a7djmgsaaspb46azzkhyrnmn-devenv-shell
|
||||
@@ -1 +1 @@
|
||||
/nix/store/vsp3fis0yyfrw5zdw1r908cz5wkwy1qs-tasks.json
|
||||
/nix/store/2f9x4skfyh2x0rkfacr2w0v3c2vm405w-tasks.json
|
||||
@@ -6,5 +6,4 @@
|
||||
/home/user01/Projects/score-system/devenv.nix
|
||||
/home/user01/Projects/score-system/devenv.yaml
|
||||
/home/user01/.config/nixpkgs/overlays
|
||||
/home/user01/.config/nixpkgs/overlays.nix
|
||||
/home/user01/Projects/score-system/.env
|
||||
/home/user01/.config/nixpkgs/overlays.nix
|
||||
Binary file not shown.
Binary file not shown.
2258
.devenv/shell-20cef00395041120.sh
Executable file
2258
.devenv/shell-20cef00395041120.sh
Executable file
File diff suppressed because it is too large
Load Diff
2258
.devenv/shell-2b51e7588d77ce70.sh
Executable file
2258
.devenv/shell-2b51e7588d77ce70.sh
Executable file
File diff suppressed because it is too large
Load Diff
2258
.devenv/shell-3503282b7604ae54.sh
Executable file
2258
.devenv/shell-3503282b7604ae54.sh
Executable file
File diff suppressed because it is too large
Load Diff
2258
.devenv/shell-3ccc9ebdaaad8b3f.sh
Executable file
2258
.devenv/shell-3ccc9ebdaaad8b3f.sh
Executable file
File diff suppressed because it is too large
Load Diff
2258
.devenv/shell-4a9089e6e3ce3083.sh
Executable file
2258
.devenv/shell-4a9089e6e3ce3083.sh
Executable file
File diff suppressed because it is too large
Load Diff
2258
.devenv/shell-50ea0b87d2caf049.sh
Executable file
2258
.devenv/shell-50ea0b87d2caf049.sh
Executable file
File diff suppressed because it is too large
Load Diff
2265
.devenv/shell-93a57e7732b1fa8a.sh
Executable file
2265
.devenv/shell-93a57e7732b1fa8a.sh
Executable file
File diff suppressed because it is too large
Load Diff
2258
.devenv/shell-9b7949b96e5561ee.sh
Executable file
2258
.devenv/shell-9b7949b96e5561ee.sh
Executable file
File diff suppressed because it is too large
Load Diff
2258
.devenv/shell-ad083a082e46f0e9.sh
Executable file
2258
.devenv/shell-ad083a082e46f0e9.sh
Executable file
File diff suppressed because it is too large
Load Diff
2258
.devenv/shell-bc17db56c7f8a19a.sh
Executable file
2258
.devenv/shell-bc17db56c7f8a19a.sh
Executable file
File diff suppressed because it is too large
Load Diff
2258
.devenv/shell-d2d0e9bf7bb10942.sh
Executable file
2258
.devenv/shell-d2d0e9bf7bb10942.sh
Executable file
File diff suppressed because it is too large
Load Diff
2258
.devenv/shell-deca24b8fe0d1abe.sh
Executable file
2258
.devenv/shell-deca24b8fe0d1abe.sh
Executable file
File diff suppressed because it is too large
Load Diff
2258
.devenv/shell-e8cdd2fa2b8a2526.sh
Executable file
2258
.devenv/shell-e8cdd2fa2b8a2526.sh
Executable file
File diff suppressed because it is too large
Load Diff
3133
.devenv/shell-env.sh
3133
.devenv/shell-env.sh
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -23,7 +23,7 @@ autoload -Uz add-zsh-hook
|
||||
|
||||
__devenv_reload_apply() {
|
||||
# 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)
|
||||
local reload_output
|
||||
reload_output=$(bash -c '
|
||||
@@ -158,8 +158,8 @@ _before=$(mktemp)
|
||||
__devenv_capture_env > "$_before"
|
||||
|
||||
# Source new devenv environment
|
||||
source "/tmp/devenv-reload-6816.sh"
|
||||
rm -f "/tmp/devenv-reload-6816.sh"
|
||||
source "/tmp/nix-shell-28554-2490132097/devenv-reload-30583.sh"
|
||||
rm -f "/tmp/nix-shell-28554-2490132097/devenv-reload-30583.sh"
|
||||
|
||||
# Compute new diff
|
||||
__devenv_compute_diff "$_before"
|
||||
|
||||
15
;
Normal file
15
;
Normal 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";
|
||||
};
|
||||
};
|
||||
}
|
||||
18
bun.lock
18
bun.lock
@@ -6,6 +6,8 @@
|
||||
"name": "score-system",
|
||||
"dependencies": {
|
||||
"@catppuccin/tailwindcss": "^1.0.0",
|
||||
"@oslojs/crypto": "^1.0.1",
|
||||
"@oslojs/encoding": "^1.1.0",
|
||||
"csv-parse": "^6.2.1",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
},
|
||||
@@ -20,7 +22,7 @@
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/bun": "^1.3.13",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/node": "^22",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
@@ -31,7 +33,7 @@
|
||||
"svelte": "^5.55.2",
|
||||
"svelte-check": "^4.4.6",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^6.0.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.58.1",
|
||||
"vite": "^8.0.7",
|
||||
},
|
||||
@@ -266,6 +268,14 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -422,7 +432,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
|
||||
@@ -1,11 +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.exec = "bun run dev";
|
||||
server = {
|
||||
ports.http.allocate = 5173;
|
||||
exec = "bun --bun run dev";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/bun": "^1.3.13",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/node": "^22",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
@@ -41,12 +41,14 @@
|
||||
"svelte": "^5.55.2",
|
||||
"svelte-check": "^4.4.6",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^6.0.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.58.1",
|
||||
"vite": "^8.0.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@catppuccin/tailwindcss": "^1.0.0",
|
||||
"@oslojs/crypto": "^1.0.1",
|
||||
"@oslojs/encoding": "^1.1.0",
|
||||
"csv-parse": "^6.2.1",
|
||||
"prettier-plugin-svelte": "^3.5.1"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
event_type,division,event_state,winner,time_completed
|
||||
100m Sprint,Year 7,0,,
|
||||
100m Sprint,Year 8,0,,
|
||||
100m Sprint,Year 9,2,East,
|
||||
100m Sprint,Year 10,1,,
|
||||
100m Sprint,Year 9,0,,
|
||||
100m Sprint,Year 10,0,,
|
||||
200m Sprint,Year 7,0,,
|
||||
200m Sprint,Year 8,0,,
|
||||
200m Sprint,Year 9,0,,
|
||||
|
||||
|
@@ -59,11 +59,11 @@ Dante Sorensen,1000m Long Distance,0,C
|
||||
Mia Nguyen,1000m Long Distance,0,C
|
||||
Oluseun Alade,1000m Long Distance,0,C
|
||||
Tobias Crane,1000m Long Distance,0,C
|
||||
Alaric Olawale,100m Sprint,1,A
|
||||
Ava Mackenzie,100m Sprint,2,A
|
||||
Bjorn Oyelaran,100m Sprint,3,A
|
||||
Jack Simmons,100m Sprint,4,A
|
||||
Kai Johansson,100m Sprint,5,A
|
||||
Alaric Olawale,100m Sprint,0,A
|
||||
Ava Mackenzie,100m Sprint,0,A
|
||||
Bjorn Oyelaran,100m Sprint,0,A
|
||||
Jack Simmons,100m Sprint,0,A
|
||||
Kai Johansson,100m Sprint,0,A
|
||||
Clark Kent,100m Sprint,0,B
|
||||
Cormac Halvorsen,100m Sprint,0,B
|
||||
Marcus Okafor,100m Sprint,0,B
|
||||
|
||||
|
@@ -1,7 +1,9 @@
|
||||
preset,placement,points
|
||||
1,1,10
|
||||
1,2,7
|
||||
1,3,5
|
||||
1,1,5
|
||||
1,2,4
|
||||
1,3,3
|
||||
1,4,2
|
||||
1,5,1
|
||||
2,1,50
|
||||
2,2,30
|
||||
2,3,10
|
||||
|
||||
|
@@ -43,11 +43,18 @@ async function seed() {
|
||||
await db.delete(schema.players);
|
||||
await db.delete(schema.divisions);
|
||||
await db.delete(schema.teams);
|
||||
await db.delete(schema.scorers);
|
||||
await db.delete(schema.sessions);
|
||||
|
||||
await client.execute('DELETE FROM sqlite_sequence');
|
||||
|
||||
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 ---
|
||||
const teamsCSV = readCSV('teams.csv');
|
||||
for (const row of teamsCSV) {
|
||||
|
||||
@@ -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
73
src/lib/server/auth.ts
Normal 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;
|
||||
}
|
||||
@@ -1,6 +1,21 @@
|
||||
import { sql, eq } from 'drizzle-orm';
|
||||
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', {
|
||||
id: integer('players_id').primaryKey({ autoIncrement: true }),
|
||||
firstName: text('firstName').notNull(),
|
||||
|
||||
@@ -10,12 +10,17 @@ export async function POST({ request }: any) {
|
||||
|
||||
// If there is no request then dont respond
|
||||
if (!responseBody) {
|
||||
return new Response('nuh uh');
|
||||
return new Error('nuh uh');
|
||||
} else {
|
||||
console.log(JSON.stringify(responseBody));
|
||||
if (responseBody.eventId) {
|
||||
// Get the event
|
||||
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;
|
||||
console.log(scoringPreset);
|
||||
|
||||
@@ -56,6 +61,7 @@ export async function POST({ request }: any) {
|
||||
if (currentPlayer.scores.length > 0) {
|
||||
if (score > 0) {
|
||||
// put the scores on the board baby
|
||||
// THIS SHOULDNT BE REFERENCED THIS IS INTENDED
|
||||
let newScoreLedgerEntry = await db
|
||||
.insert(schema.scoreLedger)
|
||||
.values({ ledgerID: ledgerEntryId, teamID: currentPlayerTeam, points: score });
|
||||
@@ -74,6 +80,6 @@ export async function POST({ request }: any) {
|
||||
// Update the frontends
|
||||
globalEmitter.emit('scoreUpdate');
|
||||
// Return a resonse because
|
||||
return new Response('coolsies uh');
|
||||
return new Response('coolsies');
|
||||
}
|
||||
}
|
||||
|
||||
9
src/routes/event/scoring/[eventId]/+page.server.ts
Normal file
9
src/routes/event/scoring/[eventId]/+page.server.ts
Normal 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 };
|
||||
};
|
||||
@@ -196,6 +196,7 @@
|
||||
}))
|
||||
})
|
||||
});
|
||||
console.log(res);
|
||||
if (res.ok) {
|
||||
localStorage.removeItem(`scores-${eventId}`);
|
||||
localStorage.removeItem(`sortByScore-${eventId}`);
|
||||
|
||||
26
src/routes/login/+page.server.ts
Normal file
26
src/routes/login/+page.server.ts
Normal 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, '/');
|
||||
}
|
||||
};
|
||||
72
src/routes/login/+page.svelte
Normal file
72
src/routes/login/+page.svelte
Normal 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>
|
||||
35
src/routes/signup/+page.server.ts
Normal file
35
src/routes/signup/+page.server.ts
Normal 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, '/');
|
||||
}
|
||||
};
|
||||
76
src/routes/signup/+page.svelte
Normal file
76
src/routes/signup/+page.svelte
Normal 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>
|
||||
@@ -5,14 +5,22 @@
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"types": ["bun"],
|
||||
"types": ["bun", "node"],
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.js",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.svelte",
|
||||
"scripts/**/*.ts"
|
||||
]
|
||||
|
||||
|
||||
// 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
|
||||
//
|
||||
|
||||
@@ -2,4 +2,11 @@ import tailwindcss from '@tailwindcss/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ['**/.devenv/**']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user