feat: Partial user auth

This commit is contained in:
April Hall 2025-02-09 19:58:50 -05:00
parent 7f6ed39c84
commit 37d13fd42b
Signed by: arithefirst
GPG Key ID: 4508A15C4DB91C5B
14 changed files with 305 additions and 5 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
BETTER_AUTH_SECRET=XXX # Generate your BetterAuth secret at https://better-auth.vercel.app/docs/installation#set-environment-variables
BETTER_AUTH_URL=http://localhost:3005 #Base URL of your app

3
.gitignore vendored
View File

@ -26,3 +26,6 @@ Thumbs.db
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Sqlite
src/lib/server/db/users.db

View File

@ -5,7 +5,7 @@
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSameLine": true,
"bracketSameLine": false,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{

BIN
bun.lockb

Binary file not shown.

View File

@ -10,6 +10,7 @@
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"migrate": "npx @better-auth/cli migrate --config './src/lib/server/db/auth'",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
},
@ -41,7 +42,10 @@
},
"dependencies": {
"@sveltejs/adapter-node": "^5.2.12",
"@types/better-sqlite3": "^7.6.12",
"@types/express": "^5.0.0",
"better-auth": "^1.1.16",
"better-sqlite3": "^11.8.1",
"cassandra-driver": "^4.7.2",
"express": "^4.21.2",
"lucide-svelte": "^0.474.0",

View File

@ -0,0 +1,5 @@
import { createAuthClient } from 'better-auth/svelte';
import { BETTER_AUTH_URL } from '$env/static/private';
export const authClient = createAuthClient({
baseURL: BETTER_AUTH_URL,
});

View File

@ -1,15 +1,22 @@
import { auth } from '$lib/server/db/auth';
import { svelteKitHandler } from 'better-auth/svelte-kit';
import { building } from '$app/environment';
import { startupSocketIOServer } from '$lib/functions/websocketConfig';
import type { Handle } from '@sveltejs/kit';
import { Server as SocketIOServer } from 'socket.io';
let io: SocketIOServer | undefined;
export const handle = (async ({ event, resolve }) => {
// Initialize WebSocket server if not building
if (!building) {
// @ts-expect-error hides incorrect error
startupSocketIOServer(event.locals.httpServer);
// @ts-expect-error hides incorrect error
event.locals.io = io;
}
return resolve(event, {
filterSerializedResponseHeaders: (name) => name === 'content-type',
});
// Handle authentication
const response = await svelteKitHandler({ event, resolve, auth });
return response;
}) satisfies Handle;

10
src/lib/server/db/auth.ts Normal file
View File

@ -0,0 +1,10 @@
import { betterAuth } from 'better-auth';
import Database from 'better-sqlite3';
export const auth = betterAuth({
database: new Database('./src/lib/server/db/users.db'),
emailAndPassword: {
enabled: true,
autoSignIn: true,
},
});

View File

@ -29,7 +29,6 @@ class Db {
}
try {
await this.client.execute(`CREATE KEYSPACE IF NOT EXISTS users WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1};`);
await this.client.execute(`CREATE KEYSPACE IF NOT EXISTS channels WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1};`);
} catch (e) {
console.log(`Error generating keyspaces: ${e as Error}`);

View File

@ -4,4 +4,39 @@ export const newChannelSchema = z.object({
channelName: z.string().min(1, 'Channel name is required'),
});
export const signupSchema = z
.object({
email: z.string().nonempty('An email is required').email('Please enter a valid email.'),
username: z
.string()
.min(3, 'Username must be at least 3 characters.')
.regex(/^(?![A-Z])/gm, 'Username cannot contain uppercase letters')
.regex(/^(?=[a-z0-9-_]+$)/gm, 'Username cannot contain special characters'),
password: z
.string()
.min(8, 'Password must be at least 8 characters.')
.regex(/(?=.*[A-Z])/gm, 'Password must contain at uppercase letter.')
.regex(/(?=.*[a-z])/gm, 'Password must contain at lowercase letter.')
.regex(/(?=.*\d)/gm, 'Password must contain at least one number.')
.regex(/(?=.*\W)/gm, 'Password must contain at least one special character'),
verify: z.string().nonempty('Passwords do not match.'),
})
.refine((schema) => schema.verify === schema.password, {
message: "Passwords don't match",
path: ['verify'],
});
export const loginSchema = z.object({
email: z.string().nonempty('An email is required').email('Please enter a valid email.'),
password: z
.string()
.min(8, 'Password must be at least 8 characters.')
.regex(/(?=.*[A-Z])/gm, 'Password must contain at uppercase letter.')
.regex(/(?=.*[a-z])/gm, 'Password must contain at lowercase letter.')
.regex(/(?=.*\d)/gm, 'Password must contain at least one number.')
.regex(/(?=.*\W)/gm, 'Password must contain at least one special character'),
});
export type NewChannelSchema = typeof newChannelSchema;
export type SignUpSchema = typeof signupSchema;
export type LogInSchema = typeof loginSchema;

View File

@ -0,0 +1,42 @@
import { loginSchema } from '$lib/types/schema';
import { message, setError, superValidate, fail } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import type { Actions } from './$types';
import { auth } from '$lib/server/db/auth';
import { APIError } from 'better-auth/api';
export const load = async () => {
const form = await superValidate(zod(loginSchema));
return { form };
};
export const actions = {
login: async ({ request }) => {
const form = await superValidate(request, zod(loginSchema));
const email = form.data.email;
const password = form.data.password;
if (!form.valid) {
return fail(400, { form });
}
try {
await auth.api.signInEmail({
body: {
email,
password,
},
});
} catch (e) {
if (e instanceof APIError) {
if (e.message === 'API Error: UNAUTHORIZED Invalid email or password') {
return setError(form, 'password', 'Invalid email or password', {
status: 401,
});
}
}
}
return message(form, 'Successfuly signed in.');
},
} satisfies Actions;

View File

@ -0,0 +1,62 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { superForm } from 'sveltekit-superforms';
let { data } = $props();
const { form, errors, message, enhance } = superForm(data.form);
</script>
<svelte:head>
<title>SVChat | Log In</title>
</svelte:head>
<main class="abs-center">
<Card.Root class="mx-auto">
<Card.Header>
<Card.Title class="text-2xl">Log In</Card.Title>
<Card.Description>Enter your email below to login to your account</Card.Description>
</Card.Header>
<Card.Content>
<form class="grid gap-4" method="POST" action="?/login" use:enhance>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
id="email"
type="text"
name="email"
placeholder="janedoe@example.com"
bind:value={$form.email}
aria-invalid={$errors.email ? 'true' : undefined}
/>
{#if $errors.email}<span class="text-sm text-red-500">{$errors.email[0]}</span>{/if}
</div>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="password">Password</Label>
<a href="##" class="ml-auto inline-block text-sm underline"> Forgot your password? </a>
</div>
<Input
id="password"
type="password"
name="password"
bind:value={$form.password}
aria-invalid={$errors.password ? 'true' : undefined}
placeholder="Password123!"
/>
{#if $errors.password}<span class="text-sm text-red-500">{$errors.password[0]}</span>{/if}
</div>
<Button type="submit" class="w-full">Log In</Button>
</form>
<p>
{#if $message}{$message}{/if}
</p>
<div class="mt-4 text-center text-sm">
Don&apos;t have an account?
<a href="/signup" class="underline"> Sign up </a>
</div>
</Card.Content>
</Card.Root>
</main>

View File

@ -0,0 +1,44 @@
import { signupSchema } from '$lib/types/schema';
import { message, setError, superValidate, fail } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import type { Actions } from './$types';
import { auth } from '$lib/server/db/auth';
import { APIError } from 'better-auth/api';
export const load = async () => {
const form = await superValidate(zod(signupSchema));
return { form };
};
export const actions = {
signup: async ({ request }) => {
const form = await superValidate(request, zod(signupSchema));
const email = form.data.email;
const password = form.data.password;
const name = form.data.username;
if (!form.valid) {
return fail(400, { form });
}
try {
await auth.api.signUpEmail({
body: {
name,
email,
password,
},
});
} catch (e) {
if (e instanceof APIError) {
if (e.message === 'API Error: UNAUTHORIZED Invalid email or password') {
return setError(form, 'verify', 'Invalid email or password', {
status: 401,
});
}
}
}
return message(form, 'Successfuly signed in.');
},
} satisfies Actions;

View File

@ -0,0 +1,87 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { superForm } from 'sveltekit-superforms';
let { data } = $props();
const { form, errors, message, enhance } = superForm(data.form);
</script>
<svelte:head>
<title>SVChat | Sign Up</title>
</svelte:head>
<main class="abs-center">
<Card.Root class="mx-auto">
<Card.Header>
<Card.Title class="text-2xl">Sign Up</Card.Title>
<Card.Description>Enter your email below to create an account</Card.Description>
</Card.Header>
<Card.Content>
<form class="grid gap-4" method="POST" action="?/signup" use:enhance>
<div class="grid gap-2">
<Label for="email">Username</Label>
<Input
id="username"
type="text"
name="username"
placeholder="janedoe"
bind:value={$form.username}
aria-invalid={$errors.username ? 'true' : undefined}
/>
{#if $errors.username}<span class="text-sm text-red-500">{$errors.username[0]}</span>{/if}
</div>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
id="email"
type="text"
name="email"
placeholder="janedoe@example.com"
bind:value={$form.email}
aria-invalid={$errors.email ? 'true' : undefined}
/>
{#if $errors.email}<span class="text-sm text-red-500">{$errors.email[0]}</span>{/if}
</div>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="password">Password</Label>
</div>
<Input
id="password"
type="password"
name="password"
bind:value={$form.password}
aria-invalid={$errors.password ? 'true' : undefined}
placeholder="Password123!"
/>
{#if $errors.password}<span class="text-sm text-red-500">{$errors.password[0]}</span>{/if}
</div>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="verify">Verify Password</Label>
</div>
<Input
id="verify"
type="password"
name="verify"
bind:value={$form.verify}
aria-invalid={$errors.verify ? 'true' : undefined}
placeholder="Password123!"
/>
{#if $errors.verify}<span class="text-sm text-red-500">{$errors.verify[0]}</span>{/if}
</div>
<Button type="submit" class="w-full">Sign Up</Button>
</form>
<p>
{#if $message}{$message}{/if}
</p>
<div class="mt-4 text-center text-sm">
Already have an account?
<a href="/login" class="underline"> Log in </a>
</div>
</Card.Content>
</Card.Root>
</main>