added login with pages and database, also correct errors in score submit
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user