diff --git a/bun.lockb b/bun.lockb index f1ef47e..49f3097 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index f820d9a..22de0de 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "vitest": "^2.0.4" }, "dependencies": { + "@lucia-auth/adapter-prisma": "^4.0.1", "@pothos/core": "^4.3.0", "@pothos/plugin-prisma": "^4.4.0", "@prisma/client": "6.0.1", @@ -59,9 +60,11 @@ "dayjs": "^1.11.13", "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", "zod": "^3.24.0" } -} \ No newline at end of file +} diff --git a/prisma/dev.db b/prisma/dev.db index 7886134..86ecbef 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ diff --git a/prisma/migrations/20241210153750_init/migration.sql b/prisma/migrations/20241210153750_init/migration.sql deleted file mode 100644 index 16407fb..0000000 --- a/prisma/migrations/20241210153750_init/migration.sql +++ /dev/null @@ -1,19 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "email" TEXT NOT NULL, - "name" TEXT -); - --- CreateTable -CREATE TABLE "Post" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "title" TEXT NOT NULL, - "content" TEXT, - "published" BOOLEAN NOT NULL DEFAULT false, - "authorId" INTEGER NOT NULL, - CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/prisma/migrations/20241211200947_add_timestamps/migration.sql b/prisma/migrations/20241211200947_add_timestamps/migration.sql deleted file mode 100644 index d4aa687..0000000 --- a/prisma/migrations/20241211200947_add_timestamps/migration.sql +++ /dev/null @@ -1,37 +0,0 @@ -/* - Warnings: - - - Made the column `content` on table `Post` required. This step will fail if there are existing NULL values in that column. - - Made the column `name` on table `User` required. This step will fail if there are existing NULL values in that column. - -*/ --- RedefineTables -PRAGMA defer_foreign_keys=ON; -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_Post" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "title" TEXT NOT NULL, - "content" TEXT NOT NULL, - "published" BOOLEAN DEFAULT false, - "authorId" INTEGER NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); -INSERT INTO "new_Post" ("authorId", "content", "id", "published", "title") SELECT "authorId", "content", "id", "published", "title" FROM "Post"; -DROP TABLE "Post"; -ALTER TABLE "new_Post" RENAME TO "Post"; -CREATE TABLE "new_User" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "email" TEXT, - "name" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); -INSERT INTO "new_User" ("email", "id", "name") SELECT "email", "id", "name" FROM "User"; -DROP TABLE "User"; -ALTER TABLE "new_User" RENAME TO "User"; -CREATE UNIQUE INDEX "User_id_key" ON "User"("id"); -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); -PRAGMA foreign_keys=ON; -PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/migrations/20241215222709_removed_issue/migration.sql b/prisma/migrations/20241215222709_removed_issue/migration.sql new file mode 100644 index 0000000..1aada7c --- /dev/null +++ b/prisma/migrations/20241215222709_removed_issue/migration.sql @@ -0,0 +1,24 @@ +/* + Warnings: + + - The primary key for the `Post` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Post" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "published" BOOLEAN DEFAULT false, + "authorId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Post" ("authorId", "content", "createdAt", "id", "published", "title", "updatedAt") SELECT "authorId", "content", "createdAt", "id", "published", "title", "updatedAt" FROM "Post"; +DROP TABLE "Post"; +ALTER TABLE "new_Post" RENAME TO "Post"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c2faf75..bdf9ac4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,21 +15,34 @@ datasource db { } model User { - id Int @id @default(autoincrement()) @unique - email String? @unique + id String @id @default(uuid()) + 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 + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + userId String + user User @relation(references: [id], fields: [userId]) +} + + model Post { - id Int @id @default(autoincrement()) + id String @id @default(uuid()) title String content String published Boolean? @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId Int + author User @relation(references: [id], fields: [authorId]) + authorId String createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt } \ No newline at end of file diff --git a/src/app.d.ts b/src/app.d.ts index da08e6d..bb055d9 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -2,8 +2,12 @@ // for information about these interfaces declare global { namespace App { + // interface Error {} - // interface Locals {} + interface Locals { + user: import("lucia").User | null; + session: import('lucia').Session | null; + } // interface PageData {} // interface PageState {} // interface Platform {} diff --git a/src/lib/server/lucia.ts b/src/lib/server/lucia.ts new file mode 100644 index 0000000..ac99da9 --- /dev/null +++ b/src/lib/server/lucia.ts @@ -0,0 +1,31 @@ +import { Lucia } from 'lucia'; +import { PrismaAdapter } from '@lucia-auth/adapter-prisma'; +import { prisma } from '$lib/server/prisma'; + +const adapter = new PrismaAdapter(prisma.session, prisma.user); +// expect error (see next section) +export const auth = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: process.env.NODE_ENV === 'production' + } + }, + getUserAttributes: (attributes) => { + return { + email: attributes.email + }; + } +}); + +declare module 'lucia' { + interface Register { + Lucia: typeof Lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + email: string; +} + +export type Auth = typeof auth; diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 614de2d..3e2a230 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,18 +1,17 @@ import { prisma } from '$lib/server/prisma'; - export async function load(event) { const userId = event.cookies.get('user'); - if (!userId && isNaN(Number(userId))) { + if (!userId) { return { authenticated: false }; } const user = await prisma.user.findUnique({ where: { - id: Number(userId) + id: userId } }); return { authenticated: !!user }; -} \ No newline at end of file +} diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts index 5216540..dc69fae 100644 --- a/src/routes/login/+page.server.ts +++ b/src/routes/login/+page.server.ts @@ -1,6 +1,8 @@ import { logger } from '$lib/server/logger'; import { prisma } from '$lib/server/prisma'; import { error, redirect, type Actions } from '@sveltejs/kit'; +import { Argon2id } from 'oslo/password'; +import { auth } from '$lib/server/lucia.js'; export const actions = { login: async (event) => { @@ -17,30 +19,46 @@ export const actions = { logger.error('User not found! ${user}'); return error(401); } - event.cookies.set('user', String(user.id), { + const password = form.get('password') as string; + if (!password) { + return error(401, 'Password is required'); + } + const validPassword = await new Argon2id().verify(user.password, password); + if (!validPassword) { + return error(400, 'Password is 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') || !form.has('name')) { + if (!form.has('email') || !form.has('name') || !form.has('password')) { return error(400); } + const password = form.get('password') as string; + 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 + name: form.get('name') as string, + password: hashedPassword } }); + const session = await auth.createSession(user.id.toString(), {}); + const sessionCookie = auth.createSessionCookie(session.id); if (!user) { return error(500); } - event.cookies.set('user', String(user.id), { + event.cookies.set(sessionCookie.name, sessionCookie.value, { path: '/', maxAge: 120 }); redirect(302, '/'); } -} satisfies Actions; \ No newline at end of file +} satisfies Actions;