From 9e669cb8477d2c671ebac83301b58f0f4a92e361 Mon Sep 17 00:00:00 2001 From: Sofiane Lasri-Trienpont <alasri250@gmail.com> Date: Sat, 9 Nov 2024 20:31:17 +0100 Subject: [PATCH] feat(auth): enhance session management and improve user flow - Add type annotations for H3Event and EventHandlerRequest - Refactor session generation to include user info in cookies - Improve password validation for non-development environments - Simplify session setting in login and register handlers - Adjust routing logic after login and registration - Remove redundant router pushes in auth store functions - Update auth middleware to use session token for validation - Display error messages on login and registration failures --- middleware/auth.ts | 4 ++-- pages/login.vue | 3 +++ pages/register.vue | 21 ++++++++++++++++-- server/api/auth/login.ts | 12 +++------- server/api/auth/register.ts | 20 ++++++----------- server/utils/generateSession.ts | 39 +++++++++++++++++++++++++++++---- stores/auth.ts | 8 +------ 7 files changed, 70 insertions(+), 37 deletions(-) diff --git a/middleware/auth.ts b/middleware/auth.ts index caeee18..5048b24 100644 --- a/middleware/auth.ts +++ b/middleware/auth.ts @@ -1,6 +1,6 @@ export default defineNuxtRouteMiddleware((to) => { - const isAuthenticated = false; // TODO: check if user is authenticated - if (!isAuthenticated && to.path.startsWith('/admin')) { + const hasToken = useCookie('session_token'); + if (!hasToken && to.path.startsWith('/admin')) { return navigateTo('/login'); } }); \ No newline at end of file diff --git a/pages/login.vue b/pages/login.vue index 48c84f5..03db95b 100644 --- a/pages/login.vue +++ b/pages/login.vue @@ -1,5 +1,6 @@ <script setup lang="ts"> import type {FormError} from "#ui/types"; +import {useRouter} from "#vue-router"; definePageMeta({ layout: 'auth', @@ -48,6 +49,7 @@ const validate = (values: any) => { const authStore = useAuthStore(); const loginErrorMessage = ref<string | null>(null); const isLoggingIn = ref(false); +const router = useRouter(); async function onSubmit(data: any) { isLoggingIn.value = true; @@ -58,6 +60,7 @@ async function onSubmit(data: any) { loginErrorMessage.value = 'Identifiants incorrects'; } finally { isLoggingIn.value = false; + await router.push('/admin'); } } </script> diff --git a/pages/register.vue b/pages/register.vue index 6cb5974..a3091d1 100644 --- a/pages/register.vue +++ b/pages/register.vue @@ -1,5 +1,6 @@ <script setup lang="ts"> import type {FormError} from "#ui/types"; +import {useRouter} from "#vue-router"; definePageMeta({ layout: 'auth', middleware: 'verify-user-doesnt-exists' @@ -52,7 +53,7 @@ const validate = (values: any) => { if (values.password.length < 6 || values.password.length > 32) { errors.push({path: 'password', message: 'Votre mot de passe doit contenir entre 6 et 32 caractères'}); } - if (!/[A-Z]/.test(values.password) || !/[a-z]/.test(values.password) || !/[0-9]/.test(values.password) || !/[^A-Za-z0-9]/.test(values.password)) { + if ((!/[A-Z]/.test(values.password) || !/[a-z]/.test(values.password) || !/[0-9]/.test(values.password) || !/[^A-Za-z0-9]/.test(values.password)) && process.env.NODE_ENV !== 'development') { errors.push({path: 'password', message: 'Votre mot de passe doit contenir au moins une majuscule, une minuscule, un chiffre et un caractère spécial'}); } } @@ -66,10 +67,22 @@ const validate = (values: any) => { return errors; }; +const authStore = useAuthStore(); const isRegisteringIn = ref(false); +const registerErrorMessage = ref<string | null>(null); +const router = useRouter(); async function onSubmit(data: any) { - console.log('Registering in with data', data); + isRegisteringIn.value = true; + try { + await authStore.register(data.email, data.password, data.name); + } catch (error) { + console.error('Register failed', error); + registerErrorMessage.value = 'Une erreur est survenue lors de la création de votre compte'; + } finally { + isRegisteringIn.value = false; + await router.push('/admin'); + } } </script> @@ -86,6 +99,10 @@ async function onSubmit(data: any) { :ui="uiOptions" @submit="onSubmit" > + <template #validation v-if="registerErrorMessage !== null"> + <UAlert color="red" icon="i-heroicons-information-circle-20-solid" + :title="registerErrorMessage" /> + </template> </UAuthForm> </UCard> </template> diff --git a/server/api/auth/login.ts b/server/api/auth/login.ts index 1e83da5..f2d5a99 100644 --- a/server/api/auth/login.ts +++ b/server/api/auth/login.ts @@ -1,8 +1,9 @@ import {compare} from 'bcrypt'; import {generateSession} from "~/server/utils/generateSession"; import prisma from "~/lib/prisma"; +import {EventHandlerRequest, H3Event} from "h3"; -export default defineEventHandler(async (event) => { +export default defineEventHandler(async (event: H3Event<EventHandlerRequest>) => { const userExists = await prisma.user.count(); if (!userExists) { throw createError({statusCode: 403, statusMessage: 'No users exist'}); @@ -18,14 +19,7 @@ export default defineEventHandler(async (event) => { throw createError({statusCode: 401, statusMessage: 'Invalid credentials'}); } - const generatedSession = await generateSession(user.id); - - setCookie(event, 'session', generatedSession.token, { - expires: generatedSession.expiresAt, - httpOnly: true, - secure: true, - sameSite: 'strict', - }); + const generatedSession = await generateSession(user, event); return { id: user.id, diff --git a/server/api/auth/register.ts b/server/api/auth/register.ts index 8c21932..42f8369 100644 --- a/server/api/auth/register.ts +++ b/server/api/auth/register.ts @@ -1,16 +1,17 @@ -import { hash } from 'bcrypt'; +import {hash} from 'bcrypt'; import {generateSession} from "~/server/utils/generateSession"; import prisma from "~/lib/prisma"; +import {EventHandlerRequest, H3Event} from "h3"; -export default defineEventHandler(async (event) => { +export default defineEventHandler(async (event: H3Event<EventHandlerRequest>) => { const userExists = await prisma.user.count(); if (userExists) { - throw createError({ statusCode: 403, statusMessage: 'User already exists' }); + throw createError({statusCode: 403, statusMessage: 'User already exists'}); } - const { email, name, password } = await readBody(event); + 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)) { + if ((!/[A-Z]/.test(password) || !/[a-z]/.test(password) || !/[0-9]/.test(password) || !/[^A-Za-z0-9]/.test(password)) && process.env.NODE_ENV !== 'development') { return new Response('Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character', { status: 422, }); @@ -26,14 +27,7 @@ export default defineEventHandler(async (event) => { }, }); - const generatedSession = await generateSession(user.id); - - setCookie(event, 'session', generatedSession.token, { - expires: generatedSession.expiresAt, - httpOnly: true, - secure: true, - sameSite: 'strict', - }); + await generateSession(user, event); return { id: user.id, diff --git a/server/utils/generateSession.ts b/server/utils/generateSession.ts index 7c143b0..6c58b74 100644 --- a/server/utils/generateSession.ts +++ b/server/utils/generateSession.ts @@ -1,5 +1,7 @@ import * as crypto from 'crypto'; import prisma from "~/lib/prisma"; +import {EventHandlerRequest, H3Event} from "h3"; +import {User} from "@prisma/client"; async function generateSessionToken(): Promise<string> { let generatedToken = crypto.randomBytes(32).toString('hex'); @@ -17,23 +19,52 @@ async function generateSessionToken(): Promise<string> { return generatedToken; } -async function generateSession(userId: number): Promise<{ expiresAt: Date; token: string }> { +/** + * Generates a session for a user and sets a cookie with the session token + * @param user The ID of the user to generate a session for + * @param event The H3 event to set the cookie on + */ +async function generateSession(user: User, event: H3Event<EventHandlerRequest>): Promise<{ + expiresAt: Date; + token: string +}> { const token = await generateSessionToken(); const expirationDate = new Date(); expirationDate.setDate(expirationDate.getDate() + 7); - prisma.session.create({ + const session = await prisma.session.create({ data: { token, - userId, + userId: user.id, expiresAt: expirationDate, } }); + setCookie(event, 'session_token', session.token, { + expires: session.expiresAt, + httpOnly: true, + secure: true, + sameSite: 'strict', + }); + + setCookie(event, 'userId', session.userId.toString(), { + expires: session.expiresAt, + httpOnly: false, + secure: true, + sameSite: 'strict', + }); + + setCookie(event, 'name', user.name, { + expires: session.expiresAt, + httpOnly: false, + secure: true, + sameSite: 'strict', + }); + return { token, expiresAt: expirationDate, }; } -export { generateSession }; \ No newline at end of file +export {generateSession}; \ No newline at end of file diff --git a/stores/auth.ts b/stores/auth.ts index f0c899f..4cd6035 100644 --- a/stores/auth.ts +++ b/stores/auth.ts @@ -24,8 +24,6 @@ export const useAuthStore = defineStore('auth', { } this.user = await response.json(); - const router = useRouter(); - await router.push('/admin'); } catch (error) { console.error(error); } @@ -48,17 +46,13 @@ export const useAuthStore = defineStore('auth', { } this.user = await response.json(); - const router = useRouter(); - await router.push('/admin'); } catch (error) { console.error(error); } }, - async logout() { + logout() { this.user = null; - const router = useRouter(); - await router.push('/'); } } }) \ No newline at end of file -- GitLab