feat: Partial user auth
This commit is contained in:
parent
7f6ed39c84
commit
37d13fd42b
2
.env.example
Normal file
2
.env.example
Normal 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
3
.gitignore
vendored
@ -26,3 +26,6 @@ Thumbs.db
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# Sqlite
|
||||
src/lib/server/db/users.db
|
||||
|
@ -5,7 +5,7 @@
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSameLine": true,
|
||||
"bracketSameLine": false,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
|
@ -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",
|
||||
|
5
src/lib/functions/auth/auth-client.ts
Normal file
5
src/lib/functions/auth/auth-client.ts
Normal 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,
|
||||
});
|
@ -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
10
src/lib/server/db/auth.ts
Normal 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,
|
||||
},
|
||||
});
|
@ -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}`);
|
||||
|
@ -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;
|
||||
|
42
src/routes/login/+page.server.ts
Normal file
42
src/routes/login/+page.server.ts
Normal 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;
|
62
src/routes/login/+page.svelte
Normal file
62
src/routes/login/+page.svelte
Normal 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't have an account?
|
||||
<a href="/signup" class="underline"> Sign up </a>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</main>
|
44
src/routes/signup/+page.server.ts
Normal file
44
src/routes/signup/+page.server.ts
Normal 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;
|
87
src/routes/signup/+page.svelte
Normal file
87
src/routes/signup/+page.svelte
Normal 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>
|
Loading…
Reference in New Issue
Block a user