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

@@ -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 { 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(),

View File

@@ -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');
}
}

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) {
localStorage.removeItem(`scores-${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>