diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e20558f --- /dev/null +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index ada5560..cd41439 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Sqlite +src/lib/server/db/users.db diff --git a/.prettierrc b/.prettierrc index fdd63a3..6afb3c3 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,7 +5,7 @@ "semi": true, "singleQuote": true, "trailingComma": "all", - "bracketSameLine": true, + "bracketSameLine": false, "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], "overrides": [ { diff --git a/bun.lockb b/bun.lockb index faadb5d..d876216 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index e7feb3b..b1ae95a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/lib/functions/auth/auth-client.ts b/src/lib/functions/auth/auth-client.ts new file mode 100644 index 0000000..5638b2a --- /dev/null +++ b/src/lib/functions/auth/auth-client.ts @@ -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, +}); diff --git a/src/lib/hooks.server.ts b/src/lib/hooks.server.ts index bcf6dcd..cc9a7e9 100644 --- a/src/lib/hooks.server.ts +++ b/src/lib/hooks.server.ts @@ -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; diff --git a/src/lib/server/db/auth.ts b/src/lib/server/db/auth.ts new file mode 100644 index 0000000..5d17dbd --- /dev/null +++ b/src/lib/server/db/auth.ts @@ -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, + }, +}); diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts index 6986542..84d5f2e 100644 --- a/src/lib/server/db/index.ts +++ b/src/lib/server/db/index.ts @@ -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}`); diff --git a/src/lib/types/schema.ts b/src/lib/types/schema.ts index cb6e77b..de5d456 100644 --- a/src/lib/types/schema.ts +++ b/src/lib/types/schema.ts @@ -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; diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts new file mode 100644 index 0000000..9131f67 --- /dev/null +++ b/src/routes/login/+page.server.ts @@ -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; diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte new file mode 100644 index 0000000..eded4c3 --- /dev/null +++ b/src/routes/login/+page.svelte @@ -0,0 +1,62 @@ + + + + SVChat | Log In + + + + + + Log In + Enter your email below to login to your account + + + + + Email + + {#if $errors.email}{$errors.email[0]}{/if} + + + + Password + Forgot your password? + + + {#if $errors.password}{$errors.password[0]}{/if} + + Log In + + + {#if $message}{$message}{/if} + + + Don't have an account? + Sign up + + + + diff --git a/src/routes/signup/+page.server.ts b/src/routes/signup/+page.server.ts new file mode 100644 index 0000000..8141d07 --- /dev/null +++ b/src/routes/signup/+page.server.ts @@ -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; diff --git a/src/routes/signup/+page.svelte b/src/routes/signup/+page.svelte new file mode 100644 index 0000000..d331e2a --- /dev/null +++ b/src/routes/signup/+page.svelte @@ -0,0 +1,87 @@ + + + + SVChat | Sign Up + + + + + + Sign Up + Enter your email below to create an account + + + + + Username + + {#if $errors.username}{$errors.username[0]}{/if} + + + Email + + {#if $errors.email}{$errors.email[0]}{/if} + + + + Password + + + {#if $errors.password}{$errors.password[0]}{/if} + + + + Verify Password + + + {#if $errors.verify}{$errors.verify[0]}{/if} + + Sign Up + + + {#if $message}{$message}{/if} + + + Already have an account? + Log in + + + +
+ {#if $message}{$message}{/if} +