From 0afa045449caefc8df4f7c098606d75e4e8df8c8 Mon Sep 17 00:00:00 2001
From: Sofiane Lasri-Trienpont <alasri250@gmail.com>
Date: Sat, 9 Nov 2024 17:12:21 +0100
Subject: [PATCH] feat: add authentication flows and basic page structure

- Implement login and registration pages with validation
- Add middleware to verify user existence for route protection
- Establish admin dashboard with middleware authentication
- Update app.vue for layout and loading improvements
- Create auth layout with navigation button
- Add initial index page with welcome component
---
 app.vue                              |  7 +-
 layouts/auth.vue                     | 21 ++++++
 middleware/verifyUserDoesntExists.ts | 16 +++++
 middleware/verifyUserExists.ts       | 16 +++++
 pages/admin.vue                      | 15 +++++
 pages/index.vue                      | 11 ++++
 pages/login.vue                      | 87 +++++++++++++++++++++++++
 pages/register.vue                   | 95 ++++++++++++++++++++++++++++
 8 files changed, 266 insertions(+), 2 deletions(-)
 create mode 100644 layouts/auth.vue
 create mode 100644 middleware/verifyUserDoesntExists.ts
 create mode 100644 middleware/verifyUserExists.ts
 create mode 100644 pages/admin.vue
 create mode 100644 pages/index.vue
 create mode 100644 pages/login.vue
 create mode 100644 pages/register.vue

diff --git a/app.vue b/app.vue
index 09f935b..8f57c25 100644
--- a/app.vue
+++ b/app.vue
@@ -1,6 +1,9 @@
 <template>
   <div>
-    <NuxtRouteAnnouncer />
-    <NuxtWelcome />
+    <NuxtLoadingIndicator />
+
+    <NuxtLayout>
+      <NuxtPage />
+    </NuxtLayout>
   </div>
 </template>
diff --git a/layouts/auth.vue b/layouts/auth.vue
new file mode 100644
index 0000000..921b9e7
--- /dev/null
+++ b/layouts/auth.vue
@@ -0,0 +1,21 @@
+<script setup lang="ts">
+
+</script>
+
+<template>
+  <div class="h-screen flex items-center justify-center overlay">
+    <UButton
+        icon="i-heroicons-home"
+        label="Accueil"
+        to="/"
+        color="black"
+        class="absolute top-4"
+    />
+
+    <slot/>
+  </div>
+</template>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/middleware/verifyUserDoesntExists.ts b/middleware/verifyUserDoesntExists.ts
new file mode 100644
index 0000000..a4c30af
--- /dev/null
+++ b/middleware/verifyUserDoesntExists.ts
@@ -0,0 +1,16 @@
+export default defineNuxtRouteMiddleware(async (to) => {
+    let userExists = false;
+    try {
+        await useFetch('/api/auth/userExists', {
+            onResponse: ({response}) => {
+                userExists = response._data.userExists;
+            },
+        });
+    } catch (error) {
+        console.error('Error checking if user exists', error);
+        return abortNavigation();
+    }
+    if (userExists) {
+        return navigateTo('/login');
+    }
+});
\ No newline at end of file
diff --git a/middleware/verifyUserExists.ts b/middleware/verifyUserExists.ts
new file mode 100644
index 0000000..3bff935
--- /dev/null
+++ b/middleware/verifyUserExists.ts
@@ -0,0 +1,16 @@
+export default defineNuxtRouteMiddleware(async (to) => {
+    let userExists = false;
+    try {
+        await useFetch('/api/auth/userExists', {
+            onResponse: ({response}) => {
+                userExists = response._data.userExists;
+            },
+        });
+    } catch (error) {
+        console.error('Error checking if user exists', error);
+        return abortNavigation();
+    }
+    if (!userExists) {
+        return navigateTo('/register');
+    }
+});
\ No newline at end of file
diff --git a/pages/admin.vue b/pages/admin.vue
new file mode 100644
index 0000000..67818b9
--- /dev/null
+++ b/pages/admin.vue
@@ -0,0 +1,15 @@
+<script setup lang="ts">
+definePageMeta({
+  middleware: 'auth'
+})
+</script>
+
+<template>
+  <div>
+    <h1>Admin Dashboard</h1>
+  </div>
+</template>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/pages/index.vue b/pages/index.vue
new file mode 100644
index 0000000..0bfc5e2
--- /dev/null
+++ b/pages/index.vue
@@ -0,0 +1,11 @@
+<script setup lang="ts">
+
+</script>
+
+<template>
+  <NuxtWelcome />
+</template>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/pages/login.vue b/pages/login.vue
new file mode 100644
index 0000000..48c84f5
--- /dev/null
+++ b/pages/login.vue
@@ -0,0 +1,87 @@
+<script setup lang="ts">
+import type {FormError} from "#ui/types";
+
+definePageMeta({
+  layout: 'auth',
+  middleware: 'verify-user-exists'
+});
+
+useSeoMeta({
+  title: 'Connexion',
+})
+
+const uiOptions = {
+  default: {
+    submitButton: {
+      label: 'Se connecter'
+    }
+  }
+};
+
+const formFields = [{
+  type: 'email',
+  label: 'Adresse email',
+  name: 'email',
+  placeholder: 'Entrez votre adresse email',
+  color: 'gray',
+  required: true
+}, {
+  type: 'password',
+  label: 'Mot de passe',
+  name: 'password',
+  placeholder: 'Entrez votre mot de passe',
+  color: 'gray',
+  required: true
+}];
+
+const validate = (values: any) => {
+  const errors: FormError[] = [];
+  if(!values.email) {
+    errors.push({ path: 'email', message: 'Veuillez entrer votre adresse email' });
+  }
+  if(!values.password) {
+    errors.push({ path: 'password', message: 'Veuillez entrer votre mot de passe' });
+  }
+  return errors;
+};
+
+const authStore = useAuthStore();
+const loginErrorMessage = ref<string | null>(null);
+const isLoggingIn = ref(false);
+
+async function onSubmit(data: any) {
+  isLoggingIn.value = true;
+  try {
+    await authStore.login(data.email, data.password);
+  } catch (error) {
+    console.error('Login failed', error);
+    loginErrorMessage.value = 'Identifiants incorrects';
+  } finally {
+    isLoggingIn.value = false;
+  }
+}
+</script>
+
+<template>
+  <UCard class="max-w-sm w-full">
+    <UAuthForm
+        title="Connexion"
+        description="Connectez-vous pour accéder à l'administration."
+        align="top"
+        icon="i-heroicons-user-circle"
+        :validate="validate"
+        :fields="formFields"
+        :loading="isLoggingIn"
+        :ui="uiOptions"
+        @submit="onSubmit"
+    >
+      <template #password-hint>
+        <NuxtLink to="/" class="text-primary font-medium">Mot de passe oublié ?</NuxtLink>
+      </template>
+      <template #validation v-if="loginErrorMessage !== null">
+        <UAlert color="red" icon="i-heroicons-information-circle-20-solid"
+                :title="loginErrorMessage" />
+      </template>
+    </UAuthForm>
+  </UCard>
+</template>
\ No newline at end of file
diff --git a/pages/register.vue b/pages/register.vue
new file mode 100644
index 0000000..6cb5974
--- /dev/null
+++ b/pages/register.vue
@@ -0,0 +1,95 @@
+<script setup lang="ts">
+import type {FormError} from "#ui/types";
+definePageMeta({
+  layout: 'auth',
+  middleware: 'verify-user-doesnt-exists'
+})
+
+useSeoMeta({
+  title: 'Inscription',
+})
+
+const uiOptions = {
+  default: {
+    submitButton: {
+      label: 'Créer un compte'
+    }
+  }
+};
+
+const formFields = [{
+  name: 'name',
+  type: 'text',
+  label: 'Nom d\'utilisateur',
+  placeholder: 'Entrez votre nom d\'utilisateur',
+  required: true,
+  description: "Il ne s'agit là que d'un nom d'affichage, vous pourrez le modifier plus tard.",
+}, {
+  type: 'email',
+  label: 'Adresse email',
+  name: 'email',
+  placeholder: 'Entrez votre adresse email',
+  color: 'gray',
+  required: true,
+  description: "Soyez sûr d'entrer une adresse valide, vous serez le seul maître à bord. 👀"
+}, {
+  type: 'password',
+  label: 'Mot de passe',
+  name: 'password',
+  placeholder: 'Entrez votre mot de passe',
+  color: 'gray',
+  required: true
+}];
+
+const validate = (values: any) => {
+  const errors: FormError[] = [];
+  if (!values.email) {
+    errors.push({path: 'email', message: 'Veuillez entrer votre adresse email'});
+  }
+  if (!values.password) {
+    errors.push({path: 'password', message: 'Veuillez entrer votre mot de passe'});
+  } else {
+    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)) {
+      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'});
+    }
+  }
+  if (!values.name) {
+    errors.push({path: 'name', message: 'Veuillez entrer votre nom d\'utilisateur'});
+  } else {
+    if (values.name.length < 3 || values.name.length > 32) {
+      errors.push({path: 'name', message: 'Votre nom d\'utilisateur doit contenir entre 3 et 32 caractères'});
+    }
+  }
+  return errors;
+};
+
+const isRegisteringIn = ref(false);
+
+async function onSubmit(data: any) {
+  console.log('Registering in with data', data);
+}
+</script>
+
+<template>
+  <UCard class="max-w-sm w-full">
+    <UAuthForm
+        title="Inscription"
+        description="Bienvenue chez vous ! Créez un compte pour finaliser l'installation de votre site internet."
+        align="top"
+        icon="i-heroicons-user-circle"
+        :validate="validate"
+        :fields="formFields"
+        :loading="isRegisteringIn"
+        :ui="uiOptions"
+        @submit="onSubmit"
+    >
+    </UAuthForm>
+  </UCard>
+</template>
+
+<style scoped>
+
+</style>
\ No newline at end of file
-- 
GitLab