From 7970bcc0fd52d7a3333fc5326cc0f99972dc2424 Mon Sep 17 00:00:00 2001 From: kayjaydee Date: Tue, 7 Apr 2026 22:43:41 +0200 Subject: [PATCH] feat(auth): scaffold auth pages and guards - Add Pinia auth store, types, and auth service (mocked) - Add login/register/verify/forgot password views and dashboard - Add reusable auth UI components (card, inputs, buttons) - Add router guards and protected routing wiring - Disable vue devtools plugin by default to avoid Node localStorage crash - Update dependencies for cookie handling - Comment out Umami script in index.html Made-with: Cursor --- formation.md | 180 +++++++++++++++ index.html | 4 +- package-lock.json | 34 +++ package.json | 2 + src/components/ui/AuthCard.vue | 88 ++++++++ src/components/ui/FormButton.vue | 177 +++++++++++++++ src/components/ui/FormInput.vue | 129 +++++++++++ src/router/guards.ts | 114 ++++++++++ src/router/index.ts | 62 +++++- src/services/authService.ts | 156 +++++++++++++ src/stores/auth.ts | 210 +++++++++++++++++ src/types/auth.ts | 52 +++++ src/views/DashboardView.vue | 293 ++++++++++++++++++++++++ src/views/ForgotPasswordView.vue | 162 ++++++++++++++ src/views/FormationPage.vue | 5 +- src/views/LoginView.vue | 203 +++++++++++++++++ src/views/RegisterView.vue | 372 +++++++++++++++++++++++++++++++ src/views/VerifyEmailView.vue | 244 ++++++++++++++++++++ vite.config.ts | 22 +- 19 files changed, 2495 insertions(+), 14 deletions(-) create mode 100644 formation.md create mode 100644 src/components/ui/AuthCard.vue create mode 100644 src/components/ui/FormButton.vue create mode 100644 src/components/ui/FormInput.vue create mode 100644 src/router/guards.ts create mode 100644 src/services/authService.ts create mode 100644 src/stores/auth.ts create mode 100644 src/types/auth.ts create mode 100644 src/views/DashboardView.vue create mode 100644 src/views/ForgotPasswordView.vue create mode 100644 src/views/LoginView.vue create mode 100644 src/views/RegisterView.vue create mode 100644 src/views/VerifyEmailView.vue diff --git a/formation.md b/formation.md new file mode 100644 index 0000000..99cc745 --- /dev/null +++ b/formation.md @@ -0,0 +1,180 @@ +Parfait Killian. Voici **tous les prompts structurés bloc par bloc** pour développer ta **plateforme de formation ultra sécurisée avec abonnement Stripe, cours vidéo/textes, exercices exécutables, progression, etc.** dans Cursor. + +Chaque bloc est indépendant et peut être copié-collé dans Cursor directement pour guider la création pas à pas. + +--- + +**🧱 Bloc 1 – Authentification sécurisée (frontend uniquement)** + +``` +Tu crées une plateforme de formation avec cours vidéo/textes, exercices interactifs, paiement par abonnement via Stripe, progression personnalisée et haut niveau de sécurité. + +Commence par créer l’authentification côté frontend (Vue 3 + TypeScript). + +Fonctionnalités : +- Inscription (email, mot de passe, plan choisi — non fonctionnel pour l’instant) +- Connexion avec token (JWT ou cookie, à simuler) +- Déconnexion +- Mot de passe oublié +- Vérification d’email (page après inscription) + +Implémente : +- un store Pinia `useAuthStore` +- une couche `authService.ts` avec appels mockés +- des vues `LoginView`, `RegisterView`, `ForgotPasswordView` +- composants UI réutilisables pour formulaire +- navigation protégée (route guards) +``` + +--- + +**🧱 Bloc 2 – Intégration Stripe (paiement + plans)** + +``` +Ajoute le système de paiement mensuel avec Stripe. + +Fonctionnalités : +- Plans d’abonnement multiples (mensuel, annuel, premium, etc.) +- Paiement sécurisé via Stripe Checkout ou Billing Portal +- Stockage du statut de souscription côté backend +- Accès conditionnel aux cours selon le plan + +Frontend : +- Crée une page "Plans" avec boutons "S’abonner" +- Intègre Stripe.js et redirige vers Checkout +- Gère le retour de paiement via `success_url` / `cancel_url` + +Backend (simulé ou prêt pour implémentation future) : +- Création d’un client Stripe à l’inscription +- Webhooks Stripe pour gérer statut de paiement +``` + +--- + +**🧱 Bloc 3 – Architecture des cours (textes + vidéos + code)** + +``` +Crée la structure d’un cours dans le système. + +Fonctionnalités : +- Un cours contient plusieurs chapitres +- Un chapitre contient des leçons (texte, vidéo, exercices) +- Une leçon contient du contenu markdown + vidéo + zone de code + +Modèle (à représenter en TypeScript dans frontend pour l’instant) : +- Course: id, title, description, cover, plan_required +- Chapter: id, course_id, title, position +- Lesson: id, chapter_id, title, content (markdown), video_url, exercise_type + +Crée les vues : +- `CourseOverview.vue` pour explorer un cours +- `LessonViewer.vue` avec affichage markdown + vidéo + éditeur de code +``` + +--- + +**🧱 Bloc 4 – Progression utilisateur dans les cours** + +``` +Crée un système pour suivre la progression individuelle de l’utilisateur. + +Fonctionnalités : +- Chaque utilisateur peut suivre plusieurs cours +- Chaque leçon peut être marquée comme complétée +- Affichage de la progression dans le cours (ex: 4/10 leçons terminées) +- Synchronisé avec le backend (mocké pour l’instant) + +Implémente : +- Un store Pinia `useProgressStore` +- Une structure : { user_id, course_id, completed_lessons: [lesson_ids] } +- Affiche des badges de progression dans l’UI +``` + +--- + +**🧱 Bloc 5 – Éditeur de code avec exécution sandboxée** + +``` +Ajoute une zone d’exécution de code dans certaines leçons (exercices interactifs). + +Fonctionnalités : +- Zone d’édition avec CodeMirror ou Monaco Editor +- Bouton "Exécuter" +- Envoi du code à une API d’exécution (Docker, VM, ou service type Piston ou Playground) +- Récupération de l’output stdout/stderr + +Commence avec un backend mocké (`codeRunnerService.ts`) qui renvoie une sortie simulée, prêt à être branché sur un vrai moteur plus tard. + +UI : +- Composant `CodeExercise.vue` avec éditeur + output + reset +- Validation des exercices dans la progression si output match attendu +``` + +--- + +**🧱 Bloc 6 – Sécurité avancée & protection des routes** + +``` +Implémente une sécurité frontend renforcée. + +Fonctionnalités : +- Redirection automatique si token expiré +- Accès conditionnel selon abonnement (plan actif ou non) +- Navigation dynamique : masquer les cours inaccessibles + +Ajoute : +- Middleware de navigation Vue Router pour bloquer l’accès aux routes protégées +- Contrôle d’accès par rôle/plan dans `authStore` +- Méthodes helpers comme `canAccess(course)` ou `hasPlan(planName)` +``` + +--- + +**🧱 Bloc 7 – Tableau de bord utilisateur (dashboard)** + +``` +Crée un dashboard utilisateur avec : +- Cours suivis et progression +- Plan actuel et facturation +- Historique des paiements (via Stripe si connecté) +- Accès aux paramètres de compte + +Crée la vue `DashboardView.vue` avec : +- Composants `UserCourses.vue`, `BillingInfo.vue`, `AccountSettings.vue` +- Requêtes via les stores et services simulés +``` + +--- + +**🧱 Bloc 8 – Administration des cours (interface admin)** + +``` +Crée une interface pour que l’administrateur puisse : +- Créer, modifier, supprimer des cours +- Ajouter des chapitres, leçons, vidéos, exercices + +Crée la vue `AdminCoursesView.vue` avec : +- Liste des cours avec boutons d’action +- Formulaires dynamiques pour ajouter chapitre/leçon +- Éditeur markdown intégré + upload vidéo + +Utilise Pinia ou un store temporaire pour gérer les états du formulaire. +``` + +--- + +**🧱 Bloc 9 – Gestion des erreurs et UX propre** + +``` +Ajoute une UX propre et robuste : +- Toasts pour succès/erreurs +- Indicateurs de chargement dans tous les appels API +- Pages 404 / 403 / erreur serveur +- Fallbacks pour vidéos, markdown, et éditeur de code + +Ajoute un composable `useToast.ts` pour centraliser l'affichage des messages et erreurs. +``` + +--- + +Souhaites-tu que je t’aide à écrire le backend ou les services API de ces blocs en parallèle ? (Rails, Node.js, autre ?) \ No newline at end of file diff --git a/index.html b/index.html index 2ad6184..22ca74a 100644 --- a/index.html +++ b/index.html @@ -236,8 +236,8 @@ ] } - + diff --git a/package-lock.json b/package-lock.json index 84e91f4..eff3153 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "portfolio", "version": "0.0.0", "dependencies": { + "@types/js-cookie": "^3.0.6", "@vueuse/head": "^2.0.0", + "js-cookie": "^3.0.5", "pinia": "^3.0.1", "vue": "^3.5.13", "vue-i18n": "^9.14.4", @@ -105,6 +107,7 @@ "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2103,6 +2106,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2116,6 +2125,7 @@ "integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2166,6 +2176,7 @@ "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.34.1", "@typescript-eslint/types": "8.34.1", @@ -2832,6 +2843,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3000,6 +3012,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -3382,6 +3395,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -3443,6 +3457,7 @@ "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3490,6 +3505,7 @@ "integrity": "sha512-A5dRYc3eQ5i2rJFBW8J6F69ur/H7YfYg+5SCg6v829FU0BhM4fUTrRVR2d4MdZgzw0ioJEk6otYHEAnoGFqO4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", @@ -4167,6 +4183,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5071,6 +5096,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5117,6 +5143,7 @@ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5234,6 +5261,7 @@ "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -5539,6 +5567,7 @@ "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.14.0", @@ -5590,6 +5619,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5652,6 +5682,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5767,6 +5798,7 @@ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -5945,6 +5977,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5964,6 +5997,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz", "integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.17", "@vue/compiler-sfc": "3.5.17", diff --git a/package.json b/package.json index 6f487da..a74d1c3 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "format": "prettier --write src/" }, "dependencies": { + "@types/js-cookie": "^3.0.6", "@vueuse/head": "^2.0.0", + "js-cookie": "^3.0.5", "pinia": "^3.0.1", "vue": "^3.5.13", "vue-i18n": "^9.14.4", diff --git a/src/components/ui/AuthCard.vue b/src/components/ui/AuthCard.vue new file mode 100644 index 0000000..236328f --- /dev/null +++ b/src/components/ui/AuthCard.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/src/components/ui/FormButton.vue b/src/components/ui/FormButton.vue new file mode 100644 index 0000000..d2eecc7 --- /dev/null +++ b/src/components/ui/FormButton.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/src/components/ui/FormInput.vue b/src/components/ui/FormInput.vue new file mode 100644 index 0000000..c43a1f8 --- /dev/null +++ b/src/components/ui/FormInput.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/src/router/guards.ts b/src/router/guards.ts new file mode 100644 index 0000000..4ed573e --- /dev/null +++ b/src/router/guards.ts @@ -0,0 +1,114 @@ +import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router' +import { useAuthStore } from '@/stores/auth' + +// Public routes that don't require authentication +const publicRoutes = [ + '/', + '/login', + '/register', + '/forgot-password', + '/verify-email', + '/projects', + '/about', + '/contact', + '/fiverr', + '/formation' +] + +// Routes that require authentication +const protectedRoutes = [ + '/dashboard', + '/profile', + '/courses', + '/progress' +] + +// Routes that require email verification +const verifiedEmailRoutes = [ + '/dashboard', + '/courses' +] + +export const requireAuth = async ( + to: RouteLocationNormalized, + from: RouteLocationNormalized, + next: NavigationGuardNext +) => { + const authStore = useAuthStore() + + // Initialize auth if not already done + if (!authStore.user && authStore.token) { + await authStore.initializeAuth() + } + + // Check if route requires authentication + const isProtectedRoute = protectedRoutes.some(route => + to.path.startsWith(route) + ) + + if (isProtectedRoute && !authStore.isAuthenticated) { + // Redirect to login with return URL + next({ + path: '/login', + query: { redirect: to.fullPath } + }) + return + } + + // Check if route requires email verification + const requiresEmailVerification = verifiedEmailRoutes.some(route => + to.path.startsWith(route) + ) + + if (requiresEmailVerification && authStore.user && !authStore.user.isEmailVerified) { + // Redirect to email verification + next({ + path: '/verify-email', + query: { email: authStore.user.email } + }) + return + } + + // If user is authenticated and trying to access auth pages, redirect to dashboard + if (authStore.isAuthenticated && ['/login', '/register'].includes(to.path)) { + next('/dashboard') + return + } + + next() +} + +export const requireGuest = ( + to: RouteLocationNormalized, + from: RouteLocationNormalized, + next: NavigationGuardNext +) => { + const authStore = useAuthStore() + + if (authStore.isAuthenticated) { + next('/dashboard') + return + } + + next() +} + +export const requireVerifiedEmail = ( + to: RouteLocationNormalized, + from: RouteLocationNormalized, + next: NavigationGuardNext +) => { + const authStore = useAuthStore() + + if (!authStore.isAuthenticated) { + next('/login') + return + } + + if (authStore.user && !authStore.user.isEmailVerified) { + next('/verify-email') + return + } + + next() +} diff --git a/src/router/index.ts b/src/router/index.ts index 9075136..f0e482f 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,6 +1,7 @@ import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vue-router' import { nextTick } from 'vue' import HomePage from '../views/HomePage.vue' +import { requireAuth, requireGuest, requireVerifiedEmail } from './guards' // Google Analytics gtag types declare global { @@ -48,6 +49,58 @@ const router = createRouter({ name: 'formation', component: () => import('../views/FormationPage.vue') }, + // Auth routes + { + path: '/login', + name: 'login', + component: () => import('../views/LoginView.vue'), + beforeEnter: requireGuest, + meta: { + title: 'Connexion - Plateforme de Formation', + description: 'Connectez-vous à votre compte pour accéder à vos formations en développement web.' + } + }, + { + path: '/register', + name: 'register', + component: () => import('../views/RegisterView.vue'), + beforeEnter: requireGuest, + meta: { + title: 'Inscription - Plateforme de Formation', + description: 'Créez votre compte pour accéder à nos formations en développement web.' + } + }, + { + path: '/forgot-password', + name: 'forgot-password', + component: () => import('../views/ForgotPasswordView.vue'), + beforeEnter: requireGuest, + meta: { + title: 'Mot de passe oublié - Plateforme de Formation', + description: 'Réinitialisez votre mot de passe pour accéder à votre compte.' + } + }, + { + path: '/verify-email', + name: 'verify-email', + component: () => import('../views/VerifyEmailView.vue'), + meta: { + title: 'Vérification Email - Plateforme de Formation', + description: 'Vérifiez votre adresse email pour activer votre compte et accéder aux formations.' + } + }, + // Protected routes + { + path: '/dashboard', + name: 'dashboard', + component: () => import('../views/DashboardView.vue'), + beforeEnter: requireVerifiedEmail, + meta: { + title: 'Tableau de bord - Plateforme de Formation', + description: 'Accédez à vos formations et suivez votre progression.', + requiresAuth: true + } + }, // TODO: page 404 { path: '/:pathMatch(.*)*', @@ -81,8 +134,11 @@ const router = createRouter({ } }) -// SEO Meta tags handler -router.beforeEach((to, from, next) => { +// Global navigation guard +router.beforeEach(async (to, from, next) => { + // Handle authentication + await requireAuth(to, from, next) + // Update document title if (to.meta?.title) { document.title = to.meta.title as string @@ -98,8 +154,6 @@ router.beforeEach((to, from, next) => { } metaDescription.setAttribute('content', to.meta.description as string) } - - next() }) // Google Analytics page view tracking function diff --git a/src/services/authService.ts b/src/services/authService.ts new file mode 100644 index 0000000..5a6234c --- /dev/null +++ b/src/services/authService.ts @@ -0,0 +1,156 @@ +import type { + LoginCredentials, + RegisterCredentials, + AuthResponse, + User, + ForgotPasswordRequest, + ResetPasswordRequest, + VerifyEmailRequest +} from '@/types/auth' + +// Mock data for development +const mockUsers: User[] = [ + { + id: '1', + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + plan: 'pro', + isEmailVerified: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + } +] + +const mockTokens: Record = { + 'test@example.com': 'mock-jwt-token-12345' +} + +// Simulate API delay +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +class AuthService { + private baseURL = '/api/auth' + + // Simulate API call + private async mockApiCall(data: T, success = true, delayMs = 1000): Promise { + await delay(delayMs) + + if (!success) { + throw new Error('API Error') + } + + return data + } + + async login(credentials: LoginCredentials): Promise { + // Simulate validation + const user = mockUsers.find(u => u.email === credentials.email) + + if (!user || credentials.password !== 'password') { + throw new Error('Email ou mot de passe incorrect') + } + + const token = mockTokens[credentials.email] || 'mock-jwt-token' + + return this.mockApiCall({ + user, + token, + refreshToken: 'mock-refresh-token' + }) + } + + async register(credentials: RegisterCredentials): Promise { + // Simulate validation + if (credentials.password !== credentials.confirmPassword) { + throw new Error('Les mots de passe ne correspondent pas') + } + + if (credentials.password.length < 6) { + throw new Error('Le mot de passe doit contenir au moins 6 caractères') + } + + // Check if user already exists + if (mockUsers.find(u => u.email === credentials.email)) { + throw new Error('Un compte avec cet email existe déjà') + } + + const newUser: User = { + id: Date.now().toString(), + email: credentials.email, + firstName: credentials.firstName, + lastName: credentials.lastName, + plan: credentials.plan, + isEmailVerified: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + } + + const token = `mock-jwt-token-${Date.now()}` + mockTokens[credentials.email] = token + mockUsers.push(newUser) + + return this.mockApiCall({ + user: newUser, + token, + refreshToken: 'mock-refresh-token' + }) + } + + async logout(): Promise { + return this.mockApiCall(undefined, true, 500) + } + + async forgotPassword(request: ForgotPasswordRequest): Promise { + // Simulate email validation + if (!request.email || !request.email.includes('@')) { + throw new Error('Email invalide') + } + + return this.mockApiCall(undefined, true, 800) + } + + async resetPassword(request: ResetPasswordRequest): Promise { + // Simulate validation + if (request.password !== request.confirmPassword) { + throw new Error('Les mots de passe ne correspondent pas') + } + + if (request.password.length < 6) { + throw new Error('Le mot de passe doit contenir au moins 6 caractères') + } + + if (!request.token) { + throw new Error('Token invalide') + } + + return this.mockApiCall(undefined, true, 1000) + } + + async verifyEmail(request: VerifyEmailRequest): Promise { + if (!request.token) { + throw new Error('Token invalide') + } + + return this.mockApiCall(undefined, true, 800) + } + + async refreshToken(): Promise { + // Simulate token refresh + const user = mockUsers[0] // Use first user for demo + const token = `mock-jwt-token-refreshed-${Date.now()}` + + return this.mockApiCall({ + user, + token, + refreshToken: 'mock-refresh-token-new' + }, true, 500) + } + + async getCurrentUser(): Promise { + // Simulate getting current user from token + return this.mockApiCall(mockUsers[0] || null, true, 300) + } +} + +export const authService = new AuthService() diff --git a/src/stores/auth.ts b/src/stores/auth.ts new file mode 100644 index 0000000..cac2e56 --- /dev/null +++ b/src/stores/auth.ts @@ -0,0 +1,210 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' +import { authService } from '@/services/authService' +import type { + User, + LoginCredentials, + RegisterCredentials, + ForgotPasswordRequest, + ResetPasswordRequest, + VerifyEmailRequest, + AuthState +} from '@/types/auth' + +export const useAuthStore = defineStore('auth', () => { + // State + const user = ref(null) + const token = ref(localStorage.getItem('auth_token')) + const isLoading = ref(false) + const error = ref(null) + + // Computed + const isAuthenticated = computed(() => !!user.value && !!token.value) + + // Actions + const setToken = (newToken: string | null) => { + token.value = newToken + if (newToken) { + localStorage.setItem('auth_token', newToken) + } else { + localStorage.removeItem('auth_token') + } + } + + const setUser = (newUser: User | null) => { + user.value = newUser + } + + const setError = (newError: string | null) => { + error.value = newError + } + + const clearError = () => { + error.value = null + } + + const login = async (credentials: LoginCredentials) => { + try { + isLoading.value = true + clearError() + + const response = await authService.login(credentials) + + setUser(response.user) + setToken(response.token) + + return response + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Erreur de connexion' + setError(errorMessage) + throw err + } finally { + isLoading.value = false + } + } + + const register = async (credentials: RegisterCredentials) => { + try { + isLoading.value = true + clearError() + + const response = await authService.register(credentials) + + setUser(response.user) + setToken(response.token) + + return response + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Erreur d\'inscription' + setError(errorMessage) + throw err + } finally { + isLoading.value = false + } + } + + const logout = async () => { + try { + isLoading.value = true + + await authService.logout() + } catch (err) { + console.error('Logout error:', err) + } finally { + setUser(null) + setToken(null) + isLoading.value = false + } + } + + const forgotPassword = async (request: ForgotPasswordRequest) => { + try { + isLoading.value = true + clearError() + + await authService.forgotPassword(request) + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Erreur lors de l\'envoi de l\'email' + setError(errorMessage) + throw err + } finally { + isLoading.value = false + } + } + + const resetPassword = async (request: ResetPasswordRequest) => { + try { + isLoading.value = true + clearError() + + await authService.resetPassword(request) + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Erreur lors de la réinitialisation du mot de passe' + setError(errorMessage) + throw err + } finally { + isLoading.value = false + } + } + + const verifyEmail = async (request: VerifyEmailRequest) => { + try { + isLoading.value = true + clearError() + + await authService.verifyEmail(request) + + // Update user verification status + if (user.value) { + user.value.isEmailVerified = true + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Erreur lors de la vérification de l\'email' + setError(errorMessage) + throw err + } finally { + isLoading.value = false + } + } + + const refreshToken = async () => { + try { + const response = await authService.refreshToken() + setUser(response.user) + setToken(response.token) + return response + } catch (err) { + console.error('Token refresh error:', err) + // If refresh fails, logout user + await logout() + throw err + } + } + + const getCurrentUser = async () => { + try { + if (!token.value) return null + + const currentUser = await authService.getCurrentUser() + if (currentUser) { + setUser(currentUser) + } + return currentUser + } catch (err) { + console.error('Get current user error:', err) + // If getting user fails, logout + await logout() + return null + } + } + + const initializeAuth = async () => { + if (token.value) { + await getCurrentUser() + } + } + + return { + // State + user, + token, + isLoading, + error, + + // Computed + isAuthenticated, + + // Actions + login, + register, + logout, + forgotPassword, + resetPassword, + verifyEmail, + refreshToken, + getCurrentUser, + initializeAuth, + setError, + clearError + } +}) diff --git a/src/types/auth.ts b/src/types/auth.ts new file mode 100644 index 0000000..5514407 --- /dev/null +++ b/src/types/auth.ts @@ -0,0 +1,52 @@ +export interface User { + id: string + email: string + firstName?: string + lastName?: string + plan?: string + isEmailVerified: boolean + createdAt: string + updatedAt: string +} + +export interface LoginCredentials { + email: string + password: string +} + +export interface RegisterCredentials { + email: string + password: string + confirmPassword: string + firstName?: string + lastName?: string + plan?: string +} + +export interface AuthResponse { + user: User + token: string + refreshToken?: string +} + +export interface ForgotPasswordRequest { + email: string +} + +export interface ResetPasswordRequest { + token: string + password: string + confirmPassword: string +} + +export interface VerifyEmailRequest { + token: string +} + +export interface AuthState { + user: User | null + token: string | null + isAuthenticated: boolean + isLoading: boolean + error: string | null +} diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue new file mode 100644 index 0000000..48484f3 --- /dev/null +++ b/src/views/DashboardView.vue @@ -0,0 +1,293 @@ + + + + + diff --git a/src/views/ForgotPasswordView.vue b/src/views/ForgotPasswordView.vue new file mode 100644 index 0000000..5f932d5 --- /dev/null +++ b/src/views/ForgotPasswordView.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/src/views/FormationPage.vue b/src/views/FormationPage.vue index db92245..d2485d8 100644 --- a/src/views/FormationPage.vue +++ b/src/views/FormationPage.vue @@ -56,9 +56,10 @@
- +

{{ $t('pricing.trialInfo') }}

diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue new file mode 100644 index 0000000..17796fd --- /dev/null +++ b/src/views/LoginView.vue @@ -0,0 +1,203 @@ + + + + + diff --git a/src/views/RegisterView.vue b/src/views/RegisterView.vue new file mode 100644 index 0000000..849a56d --- /dev/null +++ b/src/views/RegisterView.vue @@ -0,0 +1,372 @@ + + + + + diff --git a/src/views/VerifyEmailView.vue b/src/views/VerifyEmailView.vue new file mode 100644 index 0000000..18210bf --- /dev/null +++ b/src/views/VerifyEmailView.vue @@ -0,0 +1,244 @@ + + + + + diff --git a/vite.config.ts b/vite.config.ts index c52ad85..26ae1e4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,14 +2,23 @@ import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' -import vueDevTools from 'vite-plugin-vue-devtools' // https://vite.dev/config/ -export default defineConfig({ - plugins: [ - vue(), - vueDevTools(), - ], +export default defineConfig(async ({ command }) => { + // `vite-plugin-vue-devtools` (via @vue/devtools-kit) can access `localStorage` + // at import time, which crashes when Vite loads this config in Node. + // Load it lazily and only for the dev server. + const plugins = [vue()] + + // Opt-in only: enable by running `VITE_VUE_DEVTOOLS=true npm run dev` + // This avoids Node-side crashes when loading the Vite config. + if (command === 'serve' && process.env.VITE_VUE_DEVTOOLS === 'true') { + const { default: vueDevTools } = await import('vite-plugin-vue-devtools') + plugins.push(vueDevTools()) + } + + return { + plugins, resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) @@ -51,4 +60,5 @@ export default defineConfig({ optimizeDeps: { include: ['vue', 'vue-router'] } + } })