From 70da1db8334a49636b1e4e31ff9d7d2f359a2c1a Mon Sep 17 00:00:00 2001 From: fucksophie Date: Fri, 2 Jan 2026 16:18:08 +0200 Subject: [PATCH] make login/register pages work, get rid of database.db, and remove references --- database.db | Bin 24576 -> 28672 bytes drizzle/0001_melted_goliath.sql | 2 + drizzle/meta/0001_snapshot.json | 123 ++++++++++++++++++++ drizzle/meta/_journal.json | 7 ++ references/demo/+page.svelte | 5 - references/demo/lucia/+page.server.ts | 31 ----- references/demo/lucia/+page.svelte | 12 -- references/demo/lucia/login/+page.server.ts | 107 ----------------- references/demo/lucia/login/+page.svelte | 34 ------ {references => src}/hooks.server.ts | 1 - src/lib/server/auth.ts | 33 +++++- src/lib/server/db/schema.ts | 1 + src/routes/+page.server.ts | 18 +++ src/routes/+page.svelte | 10 +- src/routes/app/+page.svelte | 1 + src/routes/login/+page.server.ts | 62 ++++++++++ src/routes/login/+page.svelte | 26 +++++ src/routes/register/+page.server.ts | 66 +++++++++++ src/routes/register/+page.svelte | 15 ++- 19 files changed, 357 insertions(+), 197 deletions(-) create mode 100644 drizzle/0001_melted_goliath.sql create mode 100644 drizzle/meta/0001_snapshot.json delete mode 100644 references/demo/+page.svelte delete mode 100644 references/demo/lucia/+page.server.ts delete mode 100644 references/demo/lucia/+page.svelte delete mode 100644 references/demo/lucia/login/+page.server.ts delete mode 100644 references/demo/lucia/login/+page.svelte rename {references => src}/hooks.server.ts (99%) create mode 100644 src/routes/+page.server.ts create mode 100644 src/routes/app/+page.svelte create mode 100644 src/routes/login/+page.server.ts create mode 100644 src/routes/login/+page.svelte create mode 100644 src/routes/register/+page.server.ts diff --git a/database.db b/database.db index 38b717492564a16189622ad646d532d432c445ef..29da474fbc8f3fa63022393fb1265d6e334c76ef 100644 GIT binary patch delta 809 zcmbu6&2JJx7>9Q$1>3kg(uhz^bPr7|vBCXdzuGiy)8$ab1t}z@iGi8j8CbO}vOvqJ zU2h(Y8qUVQLGb3qt0xohCLTO^>RDrA+)_<6T0J<)T;9C%d!F~5gIDyyd;0N(*bGHc z(cu0#lTQ|;*%>PR@H4}nry>kZvG2fZhGt(dL$>_fe{g!*PftV>@i={dWiZD+N-l6U zyaS!PHLipAI-Hp=ab`J}OAUrhBMd%KOM{E7if=`tiK}tipKzKs-s^g}T^+xwcAL(< zELkIOY~{yRE#IUGqW#RqRda`cuJ=~E%F zNP(wdaTGg~2Kz8H=`+h=Nl{cp5Edod;*ljnMUX8biHc2h1Ti9-Ch$m6Z3SvNMiPQh zfPyL!C=v~dHuUO!b+=x>?ZLL9xP80lBD>|P&VFqN(qjY>5wEC%Y9Uc3I@WEO*ThX( zmNgz*RuHBtwuXr;>IxyMl#w%*kl_i@WmQKRr2U0a{W=|dQV4V@&;>7p!5pxk4f_oJ zzhi}rAZ6;0=Kl~s_p><@*wMhDfD^b?!MuN#jxJNPem-^5-aNO}aPZ*S%B8{$^IDr1 z?nvAHtt{$jQe{0~YOc+>#VajC@Ou4bWw)nP8n&@r?3NAVI-f&kbFI`8TFr7UZ@Zh` saoDN{?X=r#3|Bvbd z8{WyAcr#cU#hKYB%kh~{w&9Iq*HmX=6So$fe4bxo^CdnbMm9$NxeWYsHw!vU=Fd%J zWNqZNWl1qiOi4;HO0qCXPEIy6wlFa_F-=LfNV71qFg8fCNH#DsHa0a&PBltRG%z(W zPfSa-Fi1AAv`9`hHn2=FPAN$?Pc2SQEHyDI%qvJM&MGb_DJo3PNXe}C3*iE~osoYx z1OINIl^gl}U051bGz&y%0 g*xc0BJ151v%sZ((a+AXgehFMQZ3_6!&kD8;0EmZ!mH+?% diff --git a/drizzle/0001_melted_goliath.sql b/drizzle/0001_melted_goliath.sql new file mode 100644 index 0000000..f35bca2 --- /dev/null +++ b/drizzle/0001_melted_goliath.sql @@ -0,0 +1,2 @@ +ALTER TABLE `user` ADD `email` text NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`); \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..efbb0c0 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,123 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "8eb771ed-36ad-43c2-a76a-3c704801326a", + "prevId": "053c418c-34da-4776-88a4-2e048c6a4637", + "tables": { + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_username_unique": { + "name": "user_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 9d1b186..a51921f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1767358712855, "tag": "0000_hard_thaddeus_ross", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1767361487418, + "tag": "0001_melted_goliath", + "breakpoints": true } ] } \ No newline at end of file diff --git a/references/demo/+page.svelte b/references/demo/+page.svelte deleted file mode 100644 index 501e687..0000000 --- a/references/demo/+page.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - -lucia diff --git a/references/demo/lucia/+page.server.ts b/references/demo/lucia/+page.server.ts deleted file mode 100644 index 9fe83e5..0000000 --- a/references/demo/lucia/+page.server.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as auth from '$lib/server/auth'; -import { fail, redirect } from '@sveltejs/kit'; -import { getRequestEvent } from '$app/server'; -import type { Actions, PageServerLoad } from './$types'; - -export const load: PageServerLoad = async () => { - const user = requireLogin(); - return { user }; -}; - -export const actions: Actions = { - logout: async (event) => { - if (!event.locals.session) { - return fail(401); - } - await auth.invalidateSession(event.locals.session.id); - auth.deleteSessionTokenCookie(event); - - return redirect(302, '/demo/lucia/login'); - } -}; - -function requireLogin() { - const { locals } = getRequestEvent(); - - if (!locals.user) { - return redirect(302, '/demo/lucia/login'); - } - - return locals.user; -} diff --git a/references/demo/lucia/+page.svelte b/references/demo/lucia/+page.svelte deleted file mode 100644 index cefb2d1..0000000 --- a/references/demo/lucia/+page.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - -

Hi, {data.user.username}!

-

Your user ID is {data.user.id}.

-
- -
diff --git a/references/demo/lucia/login/+page.server.ts b/references/demo/lucia/login/+page.server.ts deleted file mode 100644 index 97d1235..0000000 --- a/references/demo/lucia/login/+page.server.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { hash, verify } from '@node-rs/argon2'; -import { encodeBase32LowerCase } from '@oslojs/encoding'; -import { fail, redirect } from '@sveltejs/kit'; -import { eq } from 'drizzle-orm'; -import * as auth from '$lib/server/auth'; -import { db } from '$lib/server/db'; -import * as table from '$lib/server/db/schema'; -import type { Actions, PageServerLoad } from './$types'; - -export const load: PageServerLoad = async (event) => { - if (event.locals.user) { - return redirect(302, '/demo/lucia'); - } - return {}; -}; - -export const actions: Actions = { - login: async (event) => { - const formData = await event.request.formData(); - const username = formData.get('username'); - const password = formData.get('password'); - - if (!validateUsername(username)) { - return fail(400, { - message: 'Invalid username (min 3, max 31 characters, alphanumeric only)' - }); - } - if (!validatePassword(password)) { - return fail(400, { message: 'Invalid password (min 6, max 255 characters)' }); - } - - const results = await db.select().from(table.user).where(eq(table.user.username, username)); - - const existingUser = results.at(0); - if (!existingUser) { - return fail(400, { message: 'Incorrect username or password' }); - } - - const validPassword = await verify(existingUser.passwordHash, password, { - memoryCost: 19456, - timeCost: 2, - outputLen: 32, - parallelism: 1 - }); - if (!validPassword) { - return fail(400, { message: 'Incorrect username or password' }); - } - - const sessionToken = auth.generateSessionToken(); - const session = await auth.createSession(sessionToken, existingUser.id); - auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); - - return redirect(302, '/demo/lucia'); - }, - register: async (event) => { - const formData = await event.request.formData(); - const username = formData.get('username'); - const password = formData.get('password'); - - if (!validateUsername(username)) { - return fail(400, { message: 'Invalid username' }); - } - if (!validatePassword(password)) { - return fail(400, { message: 'Invalid password' }); - } - - const userId = generateUserId(); - const passwordHash = await hash(password, { - // recommended minimum parameters - memoryCost: 19456, - timeCost: 2, - outputLen: 32, - parallelism: 1 - }); - - try { - await db.insert(table.user).values({ id: userId, username, passwordHash }); - - const sessionToken = auth.generateSessionToken(); - const session = await auth.createSession(sessionToken, userId); - auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); - } catch { - return fail(500, { message: 'An error has occurred' }); - } - return redirect(302, '/demo/lucia'); - } -}; - -function generateUserId() { - // ID with 120 bits of entropy, or about the same as UUID v4. - const bytes = crypto.getRandomValues(new Uint8Array(15)); - const id = encodeBase32LowerCase(bytes); - return id; -} - -function validateUsername(username: unknown): username is string { - return ( - typeof username === 'string' && - username.length >= 3 && - username.length <= 31 && - /^[a-z0-9_-]+$/.test(username) - ); -} - -function validatePassword(password: unknown): password is string { - return typeof password === 'string' && password.length >= 6 && password.length <= 255; -} diff --git a/references/demo/lucia/login/+page.svelte b/references/demo/lucia/login/+page.svelte deleted file mode 100644 index 39c8037..0000000 --- a/references/demo/lucia/login/+page.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -

Login/Register

-
- - - - -
-

{form?.message ?? ''}

diff --git a/references/hooks.server.ts b/src/hooks.server.ts similarity index 99% rename from references/hooks.server.ts rename to src/hooks.server.ts index 3c37e24..e4d0856 100644 --- a/references/hooks.server.ts +++ b/src/hooks.server.ts @@ -12,7 +12,6 @@ const handleAuth: Handle = async ({ event, resolve }) => { } const { session, user } = await auth.validateSessionToken(sessionToken); - if (session) { auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); } else { diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 38c9930..79dac9a 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -1,7 +1,7 @@ import type { RequestEvent } from '@sveltejs/kit'; import { eq } from 'drizzle-orm'; import { sha256 } from '@oslojs/crypto/sha2'; -import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding'; +import { encodeBase32LowerCase, encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding'; import { db } from '$lib/server/db'; import * as table from '$lib/server/db/schema'; @@ -79,3 +79,34 @@ export function deleteSessionTokenCookie(event: RequestEvent) { path: '/' }); } + + +export function generateUserId() { + // ID with 120 bits of entropy, or about the same as UUID v4. + const bytes = crypto.getRandomValues(new Uint8Array(15)); + const id = encodeBase32LowerCase(bytes); + return id; +} + +export function validateUsername(username: unknown): username is string { + return ( + typeof username === 'string' && + username.length >= 3 && + username.length <= 31 && + /^[a-z0-9_-]+$/.test(username) + ); +} + +export function validatePassword(password: unknown): password is string { + return typeof password === 'string' && password.length >= 6 && password.length <= 255; +} + + +export function validateEmail(email: unknown): email is string { + return ( + typeof email === 'string' && + email.length >= 5 && + email.length <= 254 && + /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(email) + ); +} diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 6776983..153c605 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -4,6 +4,7 @@ export const user = sqliteTable('user', { id: text('id').primaryKey(), age: integer('age'), username: text('username').notNull().unique(), + email: text('email').notNull().unique(), passwordHash: text('password_hash').notNull() }); diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..4902fad --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,18 @@ +import * as auth from '$lib/server/auth'; +import { fail, redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async (event) => { + if (event.request.url.endsWith("/?logout")) { + if (!event.locals.session) { + return fail(401); + } + + await auth.invalidateSession(event.locals.session.id); + auth.deleteSessionTokenCookie(event); + + return redirect(302, '/login'); + } + + return { user: event.locals.user }; +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index d79cac4..2ee598f 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,6 +1,9 @@
@@ -30,6 +33,11 @@
- + {#if data.user?.id} + + + {:else} + + {/if}
diff --git a/src/routes/app/+page.svelte b/src/routes/app/+page.svelte new file mode 100644 index 0000000..63187ae --- /dev/null +++ b/src/routes/app/+page.svelte @@ -0,0 +1 @@ +

nothing here, yet!

diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts new file mode 100644 index 0000000..fd5d0ff --- /dev/null +++ b/src/routes/login/+page.server.ts @@ -0,0 +1,62 @@ +import { verify } from '@node-rs/argon2'; +import { fail, redirect } from '@sveltejs/kit'; +import { eq } from 'drizzle-orm'; +import * as auth from '$lib/server/auth'; +import { db } from '$lib/server/db'; +import * as table from '$lib/server/db/schema'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async (event) => { + if (event.locals.user) { + return redirect(302, '/demo/lucia'); + } + return {}; +}; + +export const actions: Actions = { + login: async (event) => { + const formData = await event.request.formData(); + const username = formData.get('username'); + const password = formData.get('password'); + + let username_is_email = false; + + if (!auth.validateUsername(username)) { + if (auth.validateEmail(username)) { + username_is_email = true; + } else { + return fail(400, { + message: 'Invalid username (min 3, max 31 characters, alphanumeric only)' + }); + } + } + if (!auth.validatePassword(password)) { + return fail(400, { message: 'Invalid password (min 6, max 255 characters)' }); + } + + const results = await db.select() + .from(table.user) + .where(username_is_email ? eq(table.user.email, username) : eq(table.user.username, username)); + + const existingUser = results.at(0); + if (!existingUser) { + return fail(400, { message: 'Incorrect username or password' }); + } + + const validPassword = await verify(existingUser.passwordHash, password, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1 + }); + if (!validPassword) { + return fail(400, { message: 'Incorrect username or password' }); + } + + const sessionToken = auth.generateSessionToken(); + const session = await auth.createSession(sessionToken, existingUser.id); + auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); + + return redirect(302, '/app'); + } +}; diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte new file mode 100644 index 0000000..9e0ad22 --- /dev/null +++ b/src/routes/login/+page.svelte @@ -0,0 +1,26 @@ + + +
+
+
+ +
+
+ + + + +
+ +

{form?.message ?? ''}

+ + +
+
diff --git a/src/routes/register/+page.server.ts b/src/routes/register/+page.server.ts new file mode 100644 index 0000000..f6bbf29 --- /dev/null +++ b/src/routes/register/+page.server.ts @@ -0,0 +1,66 @@ +import { hash } from '@node-rs/argon2'; +import { fail, redirect } from '@sveltejs/kit'; +import * as auth from '$lib/server/auth'; +import { db } from '$lib/server/db'; +import * as table from '$lib/server/db/schema'; +import type { Actions, PageServerLoad } from './$types'; +import { or } from 'drizzle-orm'; +import { eq } from 'drizzle-orm'; + +export const load: PageServerLoad = async (event) => { + if (event.locals.user) { + return redirect(302, '/demo/lucia'); + } + return {}; +}; + +export const actions: Actions = { + register: async (event) => { + const formData = await event.request.formData(); + const username = formData.get('username'); + const email = formData.get('email'); + const password = formData.get('password'); + + if (!auth.validateUsername(username)) { + return fail(400, { message: 'Invalid username' }); + } + + if(!auth.validateEmail(email)) { + return fail(400, { message: 'Invalid email' }); + } + + if (!auth.validatePassword(password)) { + return fail(400, { message: 'Invalid password' }); + } + + const userId = auth.generateUserId(); + const passwordHash = await hash(password, { + // recommended minimum parameters + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1 + }); + const results = await db.select() + .from(table.user) + .where(or(eq(table.user.email, email), + eq(table.user.username, username))); + + const existingUser = results.at(0); + if (existingUser) { + return fail(400, { message: 'Username or email already registered!' }); + } + + try { + await db.insert(table.user) + .values({ id: userId, email, username, passwordHash }); + + const sessionToken = auth.generateSessionToken(); + const session = await auth.createSession(sessionToken, userId); + auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); + } catch(e) { + return fail(500, { message: 'An error has occurred' }); + } + return redirect(302, '/app'); + } +}; diff --git a/src/routes/register/+page.svelte b/src/routes/register/+page.svelte index 43917dd..ba2e634 100644 --- a/src/routes/register/+page.svelte +++ b/src/routes/register/+page.svelte @@ -2,6 +2,9 @@ import Title from "$lib/components/extra/Title.svelte"; import Button from "$lib/components/ui/button/button.svelte"; import Input from "$lib/components/ui/input/input.svelte"; + import type { ActionData } from './$types'; + + let { form }: { form: ActionData } = $props();
@@ -9,12 +12,14 @@
+
+ + + - - + +
- - - +

{form?.message ?? ''}