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 @@
+
+
+
+
+
+
+
+
Bienvenue sur votre plateforme de formation !
+
+ Vous êtes maintenant connecté avec le plan
+ {{ getPlanName(authStore.user?.plan) }}
+
+
+ ✅ Email vérifié
+
+
+ ⚠️ Email non vérifié
+
+
+
+
+
+
Cours disponibles
+
12
+
Cours en développement web
+
+
+
Progression
+
0%
+
Commencez votre premier cours
+
+
+
Certificats
+
0
+
Certificats obtenus
+
+
+
+
+
Actions rapides
+
+
+ 📚
+ Parcourir les cours
+
+
+ 🎯
+ Voir mes objectifs
+
+
+ 👥
+ Communauté
+
+
+ ⚙️
+ Paramètres
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+ Retour à la connexion
+
+
+
+
+
+
+
+
+
+
+
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.startTrial') }}
-
+
{{ $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 @@
+
+
+
+
+
+
+
+
+
+ Mot de passe oublié ?
+
+
+ ou
+
+
+ Créer un compte
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
Vérifiez votre boîte mail
+
+ Nous avons envoyé un email de vérification à
+ {{ userEmail }}
+
+
+ Cliquez sur le lien dans l'email pour activer votre compte et commencer
+ votre formation en développement web.
+
+
+
+
+
+ Renvoyer l'email
+
+
+
+ J'ai vérifié mon email
+
+
+
+
+
Conseils :
+
+ Vérifiez votre dossier spam si vous ne trouvez pas l'email
+ Assurez-vous d'avoir saisi la bonne adresse email
+ L'email peut prendre quelques minutes à arriver
+
+
+
+
+
+
+
+ Retour à la connexion
+
+
+
+
+
+
+
+
+
+
+
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']
}
+ }
})