From 8d9afbe82e859eacdad42e6ccb6fa2c11d3144de Mon Sep 17 00:00:00 2001 From: Sofiane Lasri-Trienpont <alasri250@gmail.com> Date: Sat, 9 Nov 2024 18:29:23 +0100 Subject: [PATCH] feat(auth): implement session management for user authentication - Add Session model to Prisma schema for session handling - Implement session generation in `generateSession` utility - Update registration and login APIs to create and set sessions - Add middleware to skip user existence check in development - Add Prisma client singleton for better connection management - Include `@prisma/nuxt` module and `cookie` package in dependencies - Create migration for session table and unique token index --- lib/prisma.ts | 15 ++++ middleware/verifyUserDoesntExists.ts | 4 + middleware/verifyUserExists.ts | 4 + nuxt.config.ts | 2 +- package-lock.json | 89 ++++++++++++++++++- package.json | 4 +- .../20241109165932_session/migration.sql | 13 +++ prisma/schema.prisma | 11 +++ server/api/auth/login.ts | 28 ++++-- server/api/auth/register.ts | 18 +++- server/api/auth/userExists.ts | 2 +- server/prisma.ts | 5 -- server/utils/generateSession.ts | 39 ++++++++ 13 files changed, 215 insertions(+), 19 deletions(-) create mode 100644 lib/prisma.ts create mode 100644 prisma/migrations/20241109165932_session/migration.sql delete mode 100644 server/prisma.ts create mode 100644 server/utils/generateSession.ts diff --git a/lib/prisma.ts b/lib/prisma.ts new file mode 100644 index 0000000..08a8751 --- /dev/null +++ b/lib/prisma.ts @@ -0,0 +1,15 @@ +import { PrismaClient } from '@prisma/client' + +const prismaClientSingleton = () => { + return new PrismaClient() +} + +declare const globalThis: { + prismaGlobal: ReturnType<typeof prismaClientSingleton>; +} & typeof global; + +const prisma = globalThis.prismaGlobal ?? prismaClientSingleton() + +export default prisma + +if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma diff --git a/middleware/verifyUserDoesntExists.ts b/middleware/verifyUserDoesntExists.ts index a4c30af..e930ae4 100644 --- a/middleware/verifyUserDoesntExists.ts +++ b/middleware/verifyUserDoesntExists.ts @@ -1,4 +1,8 @@ export default defineNuxtRouteMiddleware(async (to) => { + if (process.env.NODE_ENV === 'development') { + return; + } + let userExists = false; try { await useFetch('/api/auth/userExists', { diff --git a/middleware/verifyUserExists.ts b/middleware/verifyUserExists.ts index 3bff935..1bf14d5 100644 --- a/middleware/verifyUserExists.ts +++ b/middleware/verifyUserExists.ts @@ -1,4 +1,8 @@ export default defineNuxtRouteMiddleware(async (to) => { + if (process.env.NODE_ENV === 'development') { + return; + } + let userExists = false; try { await useFetch('/api/auth/userExists', { diff --git a/nuxt.config.ts b/nuxt.config.ts index a0a3d16..2cdfb01 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -8,6 +8,6 @@ export default defineNuxtConfig({ enabled: true, }, }, - modules: ['@pinia/nuxt', '@nuxt/ui', '@vueuse/nuxt'], + modules: ['@pinia/nuxt', '@nuxt/ui', '@vueuse/nuxt', '@prisma/nuxt'], extends: ['@nuxt/ui-pro'], }) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b86f7e1..e27feba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,16 +10,18 @@ "@nuxt/ui": "^2.19.2", "@nuxt/ui-pro": "^1.5.0", "@pinia/nuxt": "^0.7.0", - "@prisma/client": "^5.22.0", + "@prisma/nuxt": "^0.0.35", "@vueuse/core": "^11.2.0", "@vueuse/nuxt": "^11.2.0", "bcrypt": "^5.1.1", + "cookie": "^1.0.1", "nuxt": "^3.14.159", "pinia": "^2.2.6", "vue": "latest", "vue-router": "latest" }, "devDependencies": { + "@prisma/client": "^5.22.0", "@types/bcrypt": "^5.0.2", "prisma": "^5.22.0" } @@ -1946,6 +1948,83 @@ "@prisma/debug": "5.22.0" } }, + "node_modules/@prisma/nuxt": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/@prisma/nuxt/-/nuxt-0.0.35.tgz", + "integrity": "sha512-p8AeISCMGDnMDRj7EOcvM/T61Qy7+RauzMR3zU7V5Jte2dexSg6OBxcsxJyss+qHJFi/Ego0NGuvitCkBXDwaw==", + "dependencies": { + "@nuxt/devtools-kit": "^1.3.3", + "@nuxt/kit": "^3.11.2", + "@prisma/client": "^5.17.0", + "chalk": "^5.3.0", + "defu": "^6.1.4", + "execa": "^8.0.1", + "prompts": "^2.4.2" + } + }, + "node_modules/@prisma/nuxt/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@prisma/nuxt/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@prisma/nuxt/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@prisma/nuxt/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/@prisma/nuxt/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@redocly/ajv": { "version": "8.11.2", "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", @@ -3999,6 +4078,14 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, + "node_modules/cookie": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.1.tgz", + "integrity": "sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==", + "engines": { + "node": ">=18" + } + }, "node_modules/cookie-es": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", diff --git a/package.json b/package.json index baadce1..74cd907 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,11 @@ "@nuxt/ui": "^2.19.2", "@nuxt/ui-pro": "^1.5.0", "@pinia/nuxt": "^0.7.0", - "@prisma/client": "^5.22.0", + "@prisma/nuxt": "^0.0.35", "@vueuse/core": "^11.2.0", "@vueuse/nuxt": "^11.2.0", "bcrypt": "^5.1.1", + "cookie": "^1.0.1", "nuxt": "^3.14.159", "pinia": "^2.2.6", "vue": "latest", @@ -26,6 +27,7 @@ "vue": "latest" }, "devDependencies": { + "@prisma/client": "^5.22.0", "@types/bcrypt": "^5.0.2", "prisma": "^5.22.0" } diff --git a/prisma/migrations/20241109165932_session/migration.sql b/prisma/migrations/20241109165932_session/migration.sql new file mode 100644 index 0000000..56276b3 --- /dev/null +++ b/prisma/migrations/20241109165932_session/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "Session" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "token" TEXT NOT NULL, + "expiresAt" DATETIME NOT NULL, + "userId" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_token_key" ON "Session"("token"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 755bb30..8fb9ba9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,6 +21,7 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) PasswordReset PasswordReset[] + Session Session[] } model PasswordReset { @@ -32,3 +33,13 @@ model PasswordReset { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) } + +model Session { + id Int @id @default(autoincrement()) + token String @unique + expiresAt DateTime + userId Int + user User @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) +} diff --git a/server/api/auth/login.ts b/server/api/auth/login.ts index 858c241..1e83da5 100644 --- a/server/api/auth/login.ts +++ b/server/api/auth/login.ts @@ -1,23 +1,35 @@ -import { compare } from 'bcrypt'; -import { prisma } from '~/server/prisma'; +import {compare} from 'bcrypt'; +import {generateSession} from "~/server/utils/generateSession"; +import prisma from "~/lib/prisma"; export default defineEventHandler(async (event) => { const userExists = await prisma.user.count(); if (!userExists) { - throw createError({ statusCode: 403, statusMessage: 'No users exist' }); + throw createError({statusCode: 403, statusMessage: 'No users exist'}); } - const { email, password } = await readBody(event); + const {email, password} = await readBody(event); const user = await prisma.user.findUnique({ - where: { email }, + where: {email}, }); if (!user || !(await compare(password, user.password))) { - throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' }); + throw createError({statusCode: 401, statusMessage: 'Invalid credentials'}); } - // Set up session or token logic here + const generatedSession = await generateSession(user.id); - return { user }; + setCookie(event, 'session', generatedSession.token, { + expires: generatedSession.expiresAt, + httpOnly: true, + secure: true, + sameSite: 'strict', + }); + + return { + id: user.id, + email: user.email, + name: user.name, + }; }); \ No newline at end of file diff --git a/server/api/auth/register.ts b/server/api/auth/register.ts index 9ecf56f..8c21932 100644 --- a/server/api/auth/register.ts +++ b/server/api/auth/register.ts @@ -1,5 +1,6 @@ import { hash } from 'bcrypt'; -import { prisma } from '~/server/prisma'; +import {generateSession} from "~/server/utils/generateSession"; +import prisma from "~/lib/prisma"; export default defineEventHandler(async (event) => { const userExists = await prisma.user.count(); @@ -25,5 +26,18 @@ export default defineEventHandler(async (event) => { }, }); - return { user }; + const generatedSession = await generateSession(user.id); + + setCookie(event, 'session', generatedSession.token, { + expires: generatedSession.expiresAt, + httpOnly: true, + secure: true, + sameSite: 'strict', + }); + + return { + id: user.id, + email: user.email, + name: user.name, + }; }); \ No newline at end of file diff --git a/server/api/auth/userExists.ts b/server/api/auth/userExists.ts index d0ecb41..ec9d97b 100644 --- a/server/api/auth/userExists.ts +++ b/server/api/auth/userExists.ts @@ -1,4 +1,4 @@ -import {prisma} from "~/server/prisma"; +import prisma from "~/lib/prisma"; export default defineEventHandler(async (event) => { const userCount = await prisma.user.count(); diff --git a/server/prisma.ts b/server/prisma.ts deleted file mode 100644 index 2e08334..0000000 --- a/server/prisma.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -export { prisma }; \ No newline at end of file diff --git a/server/utils/generateSession.ts b/server/utils/generateSession.ts new file mode 100644 index 0000000..7c143b0 --- /dev/null +++ b/server/utils/generateSession.ts @@ -0,0 +1,39 @@ +import * as crypto from 'crypto'; +import prisma from "~/lib/prisma"; + +async function generateSessionToken(): Promise<string> { + let generatedToken = crypto.randomBytes(32).toString('hex'); + + const tokenExists = await prisma.session.findUnique({ + where: { + token: generatedToken, + } + }); + + if (tokenExists) { + return generateSessionToken(); + } + + return generatedToken; +} + +async function generateSession(userId: number): Promise<{ expiresAt: Date; token: string }> { + const token = await generateSessionToken(); + const expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() + 7); + + prisma.session.create({ + data: { + token, + userId, + expiresAt: expirationDate, + } + }); + + return { + token, + expiresAt: expirationDate, + }; +} + +export { generateSession }; \ No newline at end of file -- GitLab