From f7d327c0788ae43daf1e31acdc53baf16d4e37dd Mon Sep 17 00:00:00 2001 From: Sofiane Lasri-Trienpont <alasri250@gmail.com> Date: Sat, 9 Nov 2024 17:12:11 +0100 Subject: [PATCH] feat(auth): implement authentication system with register, login, and middleware Add API endpoints for user registration and login using bcrypt for password hashing and comparison. Introduce a Pinia store for managing authentication state, including login, register, and logout actions. Implement middleware to protect admin routes, redirecting unauthenticated users to the login page. Integrate Prisma for database interactions. --- middleware/auth.ts | 6 ++++ server/api/auth/login.ts | 23 +++++++++++++ server/api/auth/register.ts | 29 ++++++++++++++++ server/api/auth/userExists.ts | 6 ++++ server/prisma.ts | 5 +++ stores/auth.ts | 64 +++++++++++++++++++++++++++++++++++ 6 files changed, 133 insertions(+) create mode 100644 middleware/auth.ts create mode 100644 server/api/auth/login.ts create mode 100644 server/api/auth/register.ts create mode 100644 server/api/auth/userExists.ts create mode 100644 server/prisma.ts create mode 100644 stores/auth.ts diff --git a/middleware/auth.ts b/middleware/auth.ts new file mode 100644 index 0000000..caeee18 --- /dev/null +++ b/middleware/auth.ts @@ -0,0 +1,6 @@ +export default defineNuxtRouteMiddleware((to) => { + const isAuthenticated = false; // TODO: check if user is authenticated + if (!isAuthenticated && to.path.startsWith('/admin')) { + return navigateTo('/login'); + } +}); \ No newline at end of file diff --git a/server/api/auth/login.ts b/server/api/auth/login.ts new file mode 100644 index 0000000..858c241 --- /dev/null +++ b/server/api/auth/login.ts @@ -0,0 +1,23 @@ +import { compare } from 'bcrypt'; +import { prisma } from '~/server/prisma'; + +export default defineEventHandler(async (event) => { + const userExists = await prisma.user.count(); + if (!userExists) { + throw createError({ statusCode: 403, statusMessage: 'No users exist' }); + } + + const { email, password } = await readBody(event); + + const user = await prisma.user.findUnique({ + where: { email }, + }); + + if (!user || !(await compare(password, user.password))) { + throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' }); + } + + // Set up session or token logic here + + return { user }; +}); \ No newline at end of file diff --git a/server/api/auth/register.ts b/server/api/auth/register.ts new file mode 100644 index 0000000..9ecf56f --- /dev/null +++ b/server/api/auth/register.ts @@ -0,0 +1,29 @@ +import { hash } from 'bcrypt'; +import { prisma } from '~/server/prisma'; + +export default defineEventHandler(async (event) => { + const userExists = await prisma.user.count(); + if (userExists) { + throw createError({ statusCode: 403, statusMessage: 'User already exists' }); + } + + const { email, name, password } = await readBody(event); + + if (!/[A-Z]/.test(password) || !/[a-z]/.test(password) || !/[0-9]/.test(password) || !/[^A-Za-z0-9]/.test(password)) { + return new Response('Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character', { + status: 422, + }); + } + + const hashedPassword = await hash(password, 10); + + const user = await prisma.user.create({ + data: { + email, + name, + password: hashedPassword, + }, + }); + + return { user }; +}); \ No newline at end of file diff --git a/server/api/auth/userExists.ts b/server/api/auth/userExists.ts new file mode 100644 index 0000000..d0ecb41 --- /dev/null +++ b/server/api/auth/userExists.ts @@ -0,0 +1,6 @@ +import {prisma} from "~/server/prisma"; + +export default defineEventHandler(async (event) => { + const userCount = await prisma.user.count(); + return { userExists: userCount > 0 }; +}) \ No newline at end of file diff --git a/server/prisma.ts b/server/prisma.ts new file mode 100644 index 0000000..2e08334 --- /dev/null +++ b/server/prisma.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +export { prisma }; \ No newline at end of file diff --git a/stores/auth.ts b/stores/auth.ts new file mode 100644 index 0000000..f0c899f --- /dev/null +++ b/stores/auth.ts @@ -0,0 +1,64 @@ +import { defineStore } from 'pinia'; +import { useRouter } from 'vue-router'; + +export const useAuthStore = defineStore('auth', { + state: () => ({ + user: null, + }), + + actions: { + async login(email: string, password: string) { + try { + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + }); + + if (!response.ok) { + // Promise.reject() is used to avoid "'throw' of exception caught locally" warning + // https://stackoverflow.com/a/60725482/12680199 + return Promise.reject(Error('Invalid credentials')); + } + + this.user = await response.json(); + const router = useRouter(); + await router.push('/admin'); + } catch (error) { + console.error(error); + } + }, + + async register(email: string, password: string, name: string) { + try { + const response = await fetch('/api/auth/register', { + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password, name }), + }); + + if (!response.ok) { + // Promise.reject() is used to avoid "'throw' of exception caught locally" warning + // https://stackoverflow.com/a/60725482/12680199 + return Promise.reject(Error('Error registering user')); + } + + this.user = await response.json(); + const router = useRouter(); + await router.push('/admin'); + } catch (error) { + console.error(error); + } + }, + + async logout() { + this.user = null; + const router = useRouter(); + await router.push('/'); + } + } +}) \ No newline at end of file -- GitLab