Fully set up username/password auth
This commit is contained in:
parent
bcdad3442c
commit
aeba55b315
13 changed files with 194 additions and 59 deletions
|
@ -2,6 +2,7 @@
|
|||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"semi": false,
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [
|
||||
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -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",
|
||||
|
|
8
src/app.d.ts
vendored
8
src/app.d.ts
vendored
|
@ -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,6 +11,10 @@ declare global {
|
|||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
interface Locals {
|
||||
user: FlatDocumentData<z.infer<typeof user>, string> | null;
|
||||
session: FlatDocumentData<z.infer<typeof session>, string> | null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
33
src/hooks.server.ts
Normal file
33
src/hooks.server.ts
Normal file
|
@ -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);
|
||||
};
|
|
@ -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<Result<FlatDocumentData<z.infer<typeof session>, string>, null>> {
|
||||
async function createSessionForUser(
|
||||
userId: string
|
||||
): Promise<Option<FlatDocumentData<z.infer<typeof session>, string>>> {
|
||||
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, {
|
||||
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 Err(null)
|
||||
return Ok((await db.session.find(sessionId))?.flat()!)
|
||||
},
|
||||
{ expireIn: sessionTimeSpan.milliseconds() }
|
||||
)
|
||||
if (!createdSession.ok) return None
|
||||
return Some((await db.session.find(sessionId))?.flat()!)
|
||||
}
|
||||
|
||||
async function deleteSession(sessionId: string): Promise<void> {
|
||||
await db.session.delete(sessionId)
|
||||
}
|
||||
|
||||
export {
|
||||
createSessionForUser,
|
||||
deleteSession
|
||||
async function getUserAndSession(
|
||||
sessionId: string
|
||||
): Promise<
|
||||
Option<{
|
||||
user: FlatDocumentData<z.infer<(typeof import('$lib/db'))['user']>, string>
|
||||
session: FlatDocumentData<z.infer<(typeof import('$lib/db'))['session']>, 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 }
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<script></script>
|
5
src/routes/+page.ts
Normal file
5
src/routes/+page.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { redirect } from "@sveltejs/kit";
|
||||
|
||||
export async function load() {
|
||||
return redirect(302, '/app')
|
||||
}
|
7
src/routes/app/+page.server.ts
Normal file
7
src/routes/app/+page.server.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export async function load({ locals }) {
|
||||
if(!locals.session) {
|
||||
return redirect(302, "/auth")
|
||||
}
|
||||
}
|
3
src/routes/app/+page.svelte
Normal file
3
src/routes/app/+page.svelte
Normal file
|
@ -0,0 +1,3 @@
|
|||
<script>
|
||||
|
||||
</script>
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { superForm } from 'sveltekit-superforms';
|
||||
import { page } from "$app/stores"
|
||||
const { data } = $props();
|
||||
const { enhance, message, constraints, errors, form } = superForm(data.form);
|
||||
import { superForm } from 'sveltekit-superforms'
|
||||
import { page } from '$app/stores'
|
||||
const { data } = $props()
|
||||
const { enhance, message, errors, form, allErrors } = superForm(data.form)
|
||||
let password = $state('')
|
||||
let username = $state('')
|
||||
$effect(() => {
|
||||
form.update(f => {
|
||||
form.update((f) => {
|
||||
f.username = username
|
||||
f.password = password
|
||||
console.log(f)
|
||||
|
@ -54,19 +54,24 @@
|
|||
></path
|
||||
></svg>
|
||||
</a>
|
||||
<a href="{$page.url.pathname}/oauth/github" class="btn grow btn-outline join-item">
|
||||
<svg class="size-5" viewBox="0 0 98 96" xmlns="http://www.w3.org/2000/svg"
|
||||
<a href="{$page.url.pathname}/oauth/github" class="btn grow btn-outline join-item group">
|
||||
<svg
|
||||
class="size-5 text-[#24292f] dark:text-[#fff] group-hover:text-[#fff] dark:group-hover:text-[#24292f] transition-colors"
|
||||
viewBox="0 0 98 96"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
|
||||
fill="#24292f"></path
|
||||
fill="currentColor"></path
|
||||
></svg>
|
||||
</a>
|
||||
</div>
|
||||
<span class="divider">or</span>
|
||||
<form class="flex-col flex gap-y-4" method="post" use:enhance>
|
||||
<label class="input input-bordered flex items-center gap-2" class:input-error={!!$errors.username}>
|
||||
<label
|
||||
class="input input-bordered flex items-center gap-2"
|
||||
class:input-error={!!$errors.username}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
|
@ -89,7 +94,9 @@
|
|||
<span
|
||||
class="opacity-0 hidden transition-opacity duration-1000 text-error"
|
||||
class:showerror={$errors.username}>{$errors.username?.join(' & ')}</span>
|
||||
<label class="input input-bordered flex items-center gap-2" class:input-error={!!$errors.password}>
|
||||
<label
|
||||
class="input input-bordered flex items-center gap-2"
|
||||
class:input-error={!!$errors.password}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
|
|
15
src/routes/auth/logout/+server.ts
Normal file
15
src/routes/auth/logout/+server.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { cookieController, deleteSession } from '$lib/auth';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ locals, cookies }) {
|
||||
console.log(locals)
|
||||
if(locals.session) {
|
||||
await deleteSession(locals.session.id)
|
||||
const sessionCookie = cookieController.createBlankCookie();
|
||||
cookies.set(sessionCookie.name, sessionCookie.value, {
|
||||
path: ".",
|
||||
...sessionCookie.attributes
|
||||
});
|
||||
}
|
||||
return redirect(302, '/')
|
||||
}
|
Loading…
Reference in a new issue