diff --git a/.prettierrc b/.prettierrc index 89e142b..10e6cad 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,6 +2,7 @@ "useTabs": true, "singleQuote": true, "trailingComma": "none", + "semi": false, "printWidth": 100, "plugins": ["prettier-plugin-svelte"], "overrides": [ diff --git a/bun.lockb b/bun.lockb index 30652b6..5789f38 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 264df6c..11cbd31 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "devDependencies": { "@deno/kv": "^0.8.1", "@olli/kvdex": "npm:@jsr/olli__kvdex", + "@oxi/option": "npm:@jsr/oxi__option", "@oxi/result": "npm:@jsr/oxi__result", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", diff --git a/src/app.d.ts b/src/app.d.ts index 743f07b..82505ae 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,5 +1,9 @@ // See https://kit.svelte.dev/docs/types#app // for information about these interfaces + +import type { FlatDocumentData } from "@olli/kvdex" +import { user, session } from "$lib/db" +import z from "zod" declare global { namespace App { // interface Error {} @@ -7,7 +11,11 @@ declare global { // interface PageData {} // interface PageState {} // interface Platform {} + interface Locals { + user: FlatDocumentData, string> | null; + session: FlatDocumentData, string> | null; + } } } -export {}; +export { }; diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..fd4e407 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,33 @@ +import { cookieController, cookieExpiration, getUserAndSession } from "$lib/auth"; +import type { Handle } from "@sveltejs/kit"; +import { createDate } from "oslo"; + +export const handle: Handle = async ({ event, resolve }) => { + const sessionId = event.cookies.get("auth_session"); + if (!sessionId) { + event.locals.user = null; + event.locals.session = null; + return resolve(event); + } + + const res = await getUserAndSession(sessionId); + if(res.isNone()) { + const sessionCookie = cookieController.createBlankCookie(); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }); + event.locals.user = null; + event.locals.session = null; + return resolve(event); + } + const { session, user } = res.unwrap() + const sessionCookie = cookieController.createCookie(session.id) + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }) + event.locals.user = user; + event.locals.session = session; + return resolve(event); +}; \ No newline at end of file diff --git a/src/lib/auth.ts b/src/lib/auth.ts index b2bd9dd..1321f6f 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,30 +1,60 @@ -import type { z } from "zod" -import { db, session } from "./db" -import { Err, Ok, Result } from "@oxi/result" -import type { FlatDocumentData } from "@olli/kvdex" -import { nanoid } from "nanoid" -import { TimeSpan, createDate } from "oslo" -import { alphabet, generateRandomString } from "oslo/crypto" +import type { z } from 'zod' +import { db, session } from './db' +import { Err, Ok, Result } from '@oxi/result' +import { Option, None, Some } from '@oxi/option' +import type { FlatDocumentData } from '@olli/kvdex' +import { nanoid } from 'nanoid' +import { TimeSpan, createDate } from 'oslo' +import { alphabet, generateRandomString } from 'oslo/crypto' +import { CookieController } from "oslo/cookie" const sessionTimeSpan = new TimeSpan(1, 'w') -async function createSessionForUser(userId: string): Promise, string>, null>> { - const user = (await db.user.find(userId))?.flat() - if(!user) return Err(null) - const sessionId = generateRandomString(10, alphabet("0-9", "a-z")) - const createdSession = (await db.session.set(sessionId, { - expiresAt: createDate(sessionTimeSpan), - userId - }, { expireIn: sessionTimeSpan.milliseconds() })) - if(!createdSession.ok) return Err(null) - return Ok((await db.session.find(sessionId))?.flat()!) +async function createSessionForUser( + userId: string +): Promise, string>>> { + const user = (await db.user.find(userId))?.flat() + if (!user) return None + const sessionId = generateRandomString(21, alphabet('0-9', 'a-z')) + const createdSession = await db.session.set( + sessionId, + { + expiresAt: createDate(sessionTimeSpan), + userId + }, + { expireIn: sessionTimeSpan.milliseconds() } + ) + if (!createdSession.ok) return None + return Some((await db.session.find(sessionId))?.flat()!) } async function deleteSession(sessionId: string): Promise { - await db.session.delete(sessionId) + await db.session.delete(sessionId) } -export { - createSessionForUser, - deleteSession -} \ No newline at end of file +async function getUserAndSession( + sessionId: string +): Promise< + Option<{ + user: FlatDocumentData, string> + session: FlatDocumentData, string> + }> +> { + const session = (await db.session.find(sessionId))?.flat() + if (!session) return None + const user = (await db.user.find(session.userId))?.flat() + if (!user) return None + await db.session.update(sessionId, { + expiresAt: createDate(sessionTimeSpan) + }, { expireIn: sessionTimeSpan.milliseconds() }) + return Some({ user, session }) +} + +export const cookieExpiration = new TimeSpan(365 * 2, 'd') +export const cookieController = new CookieController('auth_session', { + httpOnly: true, + secure: true, + sameSite: "lax", + path: ".", +}, { expiresIn: cookieExpiration }) +export { createSessionForUser, deleteSession, getUserAndSession } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte deleted file mode 100644 index f0b453d..0000000 --- a/src/routes/+page.svelte +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/routes/+page.ts b/src/routes/+page.ts new file mode 100644 index 0000000..f7d2f63 --- /dev/null +++ b/src/routes/+page.ts @@ -0,0 +1,5 @@ +import { redirect } from "@sveltejs/kit"; + +export async function load() { + return redirect(302, '/app') +} \ No newline at end of file diff --git a/src/routes/app/+page.server.ts b/src/routes/app/+page.server.ts new file mode 100644 index 0000000..ac9b605 --- /dev/null +++ b/src/routes/app/+page.server.ts @@ -0,0 +1,7 @@ +import { redirect } from '@sveltejs/kit'; + +export async function load({ locals }) { + if(!locals.session) { + return redirect(302, "/auth") + } +} \ No newline at end of file diff --git a/src/routes/app/+page.svelte b/src/routes/app/+page.svelte new file mode 100644 index 0000000..bd92406 --- /dev/null +++ b/src/routes/app/+page.svelte @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/routes/auth/+page.server.ts b/src/routes/auth/+page.server.ts index 56bdadc..7a941a5 100644 --- a/src/routes/auth/+page.server.ts +++ b/src/routes/auth/+page.server.ts @@ -2,32 +2,50 @@ import { alphabet, generateRandomString } from 'oslo/crypto'; import { fail, message, setError, superValidate } from 'sveltekit-superforms'; import { zod } from 'sveltekit-superforms/adapters'; import { z } from 'zod'; -import { hash } from "@ts-rex/argon2" +import { hash, verify } from "@ts-rex/argon2" import { db } from '$lib/db.js'; +import { cookieController, cookieExpiration, createSessionForUser } from '$lib/auth.js'; +import { createDate } from 'oslo'; +import { redirect } from '@sveltejs/kit'; const schema = z.object({ username: z.string().min(4, "must be atleast 4 characters").max(32, "must be less than 32 characters").regex(/^[a-z0-9_\-]+$/i, `must be alphanumeric, with the exception of "_" and "-"`), password: z.string().min(8, "must be atleast 8 characters").max(255) }); -export async function load() { +export async function load({ locals }) { + if(locals.session) { + return redirect(302, '/app') + } const form = await superValidate(zod(schema)); return { form }; }; export const actions = { - login: async ({ request }) => { + login: async ({ request, cookies }) => { const form = await superValidate(request, zod(schema)); if (!form.valid) return fail(400, { form }); const { username, password } = form.data - - // TODO: Login user - return message(form, 'Login form submitted'); + const user = (await db.user.findByPrimaryIndex('username', username))?.flat(); + if (!user) return setError(form, "user does not exist") + if (!user.password) return setError(form, "this account does not have a password, maybe try a different method?") + const isvalid = verify(password, user.password); + if (!isvalid) return setError(form, "incorrect password") + const session = (await createSessionForUser(user.id)) + if (session.isSome()) { + const sessionCookie = cookieController.createCookie(session.unwrap().id) + cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }) + return redirect(302, '/app') + } else { + return fail(500, { form }) + } }, - signup: async ({ request }) => { + signup: async ({ request, cookies }) => { const form = await superValidate(request, zod(schema)); - console.log(form) if (!form.valid) return fail(400, { form }); const { username, password } = form.data @@ -35,15 +53,23 @@ export const actions = { const passwordHash = hash(password) const user = (await db.user.findByPrimaryIndex('username', username))?.flat() - if(user) return setError(form, 'Username already exists') - console.log(userId, password, passwordHash); - console.log(await db.user.set(userId, { + if (user) return setError(form, "username", 'username already exists') + await db.user.set(userId, { displayName: username, username, id: userId, password: passwordHash - })) - - return message(form, 'Signup form submitted'); + }) + const session = (await createSessionForUser(userId)) + if (session.isSome()) { + const sessionCookie = cookieController.createCookie(session.unwrap().id) + cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }) + return redirect(302, '/app') + } else { + return fail(500, { form }) + } } } \ No newline at end of file diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte index b553b4f..248507e 100644 --- a/src/routes/auth/+page.svelte +++ b/src/routes/auth/+page.svelte @@ -1,18 +1,18 @@
@@ -54,19 +54,24 @@ > - - +
or
-