diff --git a/.env b/.env index a706777..617c806 100644 --- a/.env +++ b/.env @@ -4,4 +4,8 @@ TWILIO_AUTH_TOKEN= TWILIO_PHONE_NUMBER= # PRISMA -DATABASE_URL="postgres://hestia:test123@localhost:5432/hestia" \ No newline at end of file +DATABASE_URL="postgres://hestia:test123@localhost:5432/hestia" + +# CLERK +PUBLIC_CLERK_PUBLISHABLE_KEY=secret_do_not_commit_or_change_this_create_.env.local_instead +CLERK_SECRET_KEY=secret_do_not_commit_or_change_this_create_.env.local_instead diff --git a/.gitignore b/.gitignore index 8c331d8..66c56cc 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ node_modules # OS .DS_Store Thumbs.db +.idea # Vite vite.config.js.timestamp-* diff --git a/bun.lockb b/bun.lockb index 4d06590..509b39c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 307fe62..b9dbb60 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,10 @@ "version": "0.0.1", "type": "module", "scripts": { - "dev": "bun validate-env && bun database:up && bun prisma:push && vite dev", + "dev": "bun validate-env && bun database:up && bun prisma:generate && vite dev", "build": "vite build", "build-storybook": "storybook build", - "database:up": "docker compose -p hestia -f devops/docker-compose.dev.yml up -d && docker compose -p hestia -f devops/docker-compose.dev.yml -f devops/docker-compose.wait.yml run --rm wait -c hestia-database:5432", + "database:up": "docker compose -p hestia -f devops/docker-compose.dev.yml up -d && docker compose -p hestia -f devops/docker-compose.dev.yml -f devops/docker-compose.wait.yml run --rm wait -c hestia-database:5432 && bun prisma:push", "database:down": "docker compose -p hestia -f devops/docker-compose.dev.yml down", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", @@ -65,19 +65,19 @@ "vitest": "^2.0.4" }, "dependencies": { + "@clerk/backend": "1.21.4", + "@clerk/themes": "^2.2.3", "@flaticon/flaticon-uicons": "^3.3.1", "@inlang/paraglide-sveltekit": "^0.15.0", - "@lucia-auth/adapter-prisma": "^4.0.1", "@pothos/core": "^4.3.0", "@pothos/plugin-prisma": "^4.4.0", "@prisma/client": "6.0.1", "@tailwindcss/typography": "^0.5.15", + "clerk-sveltekit": "https://pkg.pr.new/wobsoriano/clerk-sveltekit@ca15d4e", "dayjs": "^1.11.13", "eslint_d": "^14.3.0", "graphql": "^16.9.0", "graphql-yoga": "^5.10.4", - "lucia": "^3.2.2", - "oslo": "^1.2.1", "pino": "^9.5.0", "pino-pretty": "^13.0.0", "tailwind-merge": "^2.5.5", diff --git a/prisma/migrations/20250105021234_add_clerk_auth/migration.sql b/prisma/migrations/20250105021234_add_clerk_auth/migration.sql new file mode 100644 index 0000000..a37b472 --- /dev/null +++ b/prisma/migrations/20250105021234_add_clerk_auth/migration.sql @@ -0,0 +1,24 @@ +/* + Warnings: + + - You are about to drop the column `password` on the `User` table. All the data in the column will be lost. + - You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost. + - A unique constraint covering the columns `[clerkId]` on the table `User` will be added. If there are existing duplicate values, this will fail. + - Added the required column `clerkId` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Session" DROP CONSTRAINT "Session_userId_fkey"; + +-- DropIndex +DROP INDEX "User_email_key"; + +-- AlterTable +ALTER TABLE "User" DROP COLUMN "password", +ADD COLUMN "clerkId" TEXT NOT NULL; + +-- DropTable +DROP TABLE "Session"; + +-- CreateIndex +CREATE UNIQUE INDEX "User_clerkId_key" ON "User"("clerkId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a23c87a..090ea2e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,24 +15,12 @@ datasource db { } model User { - id String @id @default(uuid()) + id String @id @default(uuid()) + clerkId String @unique - email String? @unique - name String - password String - posts Post[] - sessions Session[] - - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt -} - -model Session { - id String @id @default(uuid()) - - expiresAt DateTime - user User @relation(references: [id], fields: [userId]) - userId String + email String? + name String + posts Post[] createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt diff --git a/src/app.d.ts b/src/app.d.ts index c8414b8..086138d 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -5,8 +5,11 @@ declare global { namespace App { // interface Error {} interface Locals { - user: import('lucia').User | null; - session: import('lucia').Session | null; + auth: { + userId?: string; + orgId?: string | null; + sessionId?: string; + }; } // interface PageData {} // interface PageState {} diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..e182571 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,3 @@ +import { withClerkHandler } from 'clerk-sveltekit/server'; + +export const handle = withClerkHandler(); diff --git a/src/lib/components/Navigation/Navbar/Navbar.svelte b/src/lib/components/Navigation/Navbar/Navbar.svelte index bc396ca..ce41227 100644 --- a/src/lib/components/Navigation/Navbar/Navbar.svelte +++ b/src/lib/components/Navigation/Navbar/Navbar.svelte @@ -1,5 +1,6 @@
- {@render children()} + + {@render children()} +
diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 5d81608..0f94248 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,5 +1,3 @@ import { validateSession } from '$lib/server/auth'; -export async function load(event) { - await validateSession(event); -} +export const load = async (event) => validateSession(event); diff --git a/src/routes/app/+layout.server.ts b/src/routes/app/+layout.server.ts index 894e1f4..0f94248 100644 --- a/src/routes/app/+layout.server.ts +++ b/src/routes/app/+layout.server.ts @@ -1,10 +1,3 @@ import { validateSession } from '$lib/server/auth'; -export async function load(event) { - const { - user: { password: _, ...rest }, - } = await validateSession(event); - return { - user: rest, - }; -} +export const load = async (event) => validateSession(event); diff --git a/src/routes/app/+layout.svelte b/src/routes/app/+layout.svelte index 94df874..a01f36b 100644 --- a/src/routes/app/+layout.svelte +++ b/src/routes/app/+layout.svelte @@ -2,6 +2,8 @@ import { Navbar } from '$lib/components/Navigation'; import type { User } from '@prisma/client'; import type { Snippet } from 'svelte'; + import { onMount } from 'svelte'; + import 'clerk-sveltekit/client'; type Props = { children: Snippet; @@ -11,6 +13,35 @@ }; let { children, data }: Props = $props(); + + let clerk; + + onMount(async () => { + clerk = window.Clerk; + + if (clerk) { + if (!clerk.user || clerk.user?.organizationMemberships.length === 0) { + console.debug('No organizations found'); + await clerk.signOut(); + return; + } + + /** + * Set the active organization to the first one in the list, this is temporary until we let the user choose the organization + */ + try { + // Set the active organization to the first one in the list + await clerk.setActive({ + organization: clerk.user?.organizationMemberships[0].organization.id, + }); + console.debug( + `Active organization set to ${clerk.user?.organizationMemberships[0].organization.id}` + ); + } catch (error) { + console.error('Error setting active organization:', error); + } + } + }); diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts deleted file mode 100644 index 620225f..0000000 --- a/src/routes/login/+page.server.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { messages } from '$lib/i18n'; -import { logger } from '$lib/server/logger'; -import { auth } from '$lib/server/lucia.js'; -import { prisma } from '$lib/server/prisma'; -import { fail, redirect, type Actions } from '@sveltejs/kit'; -import { Argon2id } from 'oslo/password'; -import { string } from 'zod'; - -export const actions = { - login: async (event) => { - const form = await event.request.formData(); - const email = form.get('email'); - if (typeof email !== 'string') { - return fail(400, { email: { error: messages.login_error_email_required() } }); - } - - const password = form.get('password') as string; - if (!password) { - return fail(400, { password: { error: messages.login_error_password_required() } }); - } - - const user = await prisma.user.findUnique({ - where: { - email: email, - }, - }); - if (!user) { - logger.error(`User not found! ${email}`); - return fail(404, { - email: { value: email, error: messages.login_error_user_not_found() }, - }); - } - - const validPassword = await new Argon2id().verify(user.password, password); - if (!validPassword) { - return fail(400, { - password: { error: messages.login_error_password_incorrect() }, - }); - } - const session = await auth.createSession(user.id, []); - const sessionCookie = auth.createSessionCookie(session.id); - event.cookies.set(sessionCookie.name, sessionCookie.value, { - path: '/', - maxAge: 120, - }); - redirect(302, '/'); - }, - - register: async (event) => { - const form = await event.request.formData(); - if (!form.has('email')) { - return fail(400, { email: { error: messages.login_error_email_required() } }); - } - const { success, data: email, error } = string().email().safeParse(form.get('email')); - if (!success) { - logger.error(error); - return fail(400, { - email: { value: email, error: messages.login_error_email_incorrect() }, - }); - } - - const password = form.get('password'); - if (typeof password !== 'string') { - return fail(400, { password: { error: messages.login_error_password_required() } }); - } - const name = form.get('name'); - if (typeof name !== 'string') { - return fail(400, { name: { error: messages.login_error_name_required() } }); - } - - const usersWithEmail = await prisma.user.count({ where: { email: email } }); - if (usersWithEmail !== 0) { - return fail(409, { - email: { value: email, error: messages.login_error_email_inuse() }, - }); - } - - const hashedPassword = await new Argon2id().hash(password); - const user = await prisma.user.create({ - data: { - email: form.get('email') as string, - name: form.get('name') as string, - password: hashedPassword, - }, - }); - - const session = await auth.createSession(user.id.toString(), {}); - const sessionCookie = auth.createSessionCookie(session.id); - event.cookies.set(sessionCookie.name, sessionCookie.value, { - path: '/', - maxAge: 120, - }); - - redirect(303, '/'); - }, -} satisfies Actions; diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index 8c8ee0c..c73f28d 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -1,77 +1,23 @@ -{#snippet userIcon()} - -{/snippet} - -{#snippet passwordIcon()} - -{/snippet} - -{#snippet nameIcon()} - -{/snippet} - -{#snippet alert(message: string)} - -{/snippet} - -{#snippet Form(variant: 'login' | 'register')} -
-
- - - {#if variant === 'register'} - - {/if} -
-
- -
-
-{/snippet} -
-
-
- {#if form} - {@render alert(Object.values(form)[0].error)} - {/if} - -
- {@render Form(tab === 0 ? 'login' : 'register')} +
+