nuxt-auth-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,307 @@
1
+ # nuxt-auth-kit
2
+
3
+ Package Nuxt d'authentification clé-en-main pour API Laravel. Installez-le une fois, utilisez-le dans tous vos projets.
4
+
5
+ ## Fonctionnalités
6
+
7
+ - ✅ **Connexion / Déconnexion**
8
+ - ✅ **Inscription** avec confirmation de mot de passe
9
+ - ✅ **Profil connecté** (`useAuth().user`)
10
+ - ✅ **Modification du profil** (nom, email, avatar)
11
+ - ✅ **Changement de mot de passe**
12
+ - ✅ **Mot de passe oublié** (envoi d'email)
13
+ - ✅ **Réinitialisation de mot de passe** (via token)
14
+ - ✅ **Gestion des rôles & permissions** (RBAC)
15
+ - ✅ **Middlewares** : `auth`, `guest`, `role`
16
+ - ✅ **Session persistante** via cookie sécurisé
17
+ - ✅ **Composants Vue** prêts à l'emploi (design Hotelook-like)
18
+ - ✅ **TypeScript** complet
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install nuxt-auth-kit
24
+ # ou
25
+ yarn add nuxt-auth-kit
26
+ # ou
27
+ pnpm add nuxt-auth-kit
28
+ ```
29
+
30
+ ## Configuration
31
+
32
+ Dans `nuxt.config.ts` :
33
+
34
+ ```ts
35
+ export default defineNuxtConfig({
36
+ modules: [
37
+ 'nuxt-auth-kit',
38
+ '@nuxt/ui' // requis pour les styles
39
+ ],
40
+
41
+ nuxtAuthKit: {
42
+ apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:8000',
43
+
44
+ // Endpoints Laravel (personnalisables)
45
+ endpoints: {
46
+ login: '/api/auth/login',
47
+ register: '/api/auth/register',
48
+ logout: '/api/auth/logout',
49
+ me: '/api/auth/me',
50
+ updateProfile: '/api/auth/profile',
51
+ updatePassword: '/api/auth/password',
52
+ forgotPassword: '/api/auth/forgot-password',
53
+ resetPassword: '/api/auth/reset-password'
54
+ },
55
+
56
+ // Redirections
57
+ redirects: {
58
+ login: '/auth/login',
59
+ home: '/',
60
+ afterLogout: '/auth/login'
61
+ },
62
+
63
+ // Cookie
64
+ tokenCookieName: 'auth_token',
65
+
66
+ // RBAC
67
+ rbac: {
68
+ superAdminRole: 'super-admin',
69
+ defaultUserRole: 'user'
70
+ }
71
+ }
72
+ })
73
+ ```
74
+
75
+ Dans `.env` :
76
+ ```env
77
+ NUXT_PUBLIC_API_BASE=https://api.monprojet.com
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Utilisation
83
+
84
+ ### Pages d'authentification
85
+
86
+ #### `pages/auth/login.vue`
87
+ ```vue
88
+ <template>
89
+ <AuthLayout app-name="MonApp" :quote="quote">
90
+ <AuthLoginForm
91
+ :roles="[
92
+ { value: 'user', label: 'En tant qu\'utilisateur' },
93
+ { value: 'owner', label: 'En tant que propriétaire' }
94
+ ]"
95
+ :show-social="true"
96
+ @forgot-password="navigateTo('/auth/forgot-password')"
97
+ @register="navigateTo('/auth/register')"
98
+ />
99
+ </AuthLayout>
100
+ </template>
101
+
102
+ <script setup lang="ts">
103
+ definePageMeta({ middleware: 'guest' })
104
+
105
+ const quote = {
106
+ text: 'Une expérience fluide et agréable. La plateforme rend tout si simple.',
107
+ author: 'Alex Mitchell',
108
+ location: 'Amsterdam'
109
+ }
110
+ </script>
111
+ ```
112
+
113
+ #### `pages/auth/register.vue`
114
+ ```vue
115
+ <template>
116
+ <AuthLayout app-name="MonApp">
117
+ <AuthRegisterForm @login="navigateTo('/auth/login')" />
118
+ </AuthLayout>
119
+ </template>
120
+
121
+ <script setup lang="ts">
122
+ definePageMeta({ middleware: 'guest' })
123
+ </script>
124
+ ```
125
+
126
+ #### `pages/auth/forgot-password.vue`
127
+ ```vue
128
+ <template>
129
+ <AuthLayout app-name="MonApp">
130
+ <AuthForgotPasswordForm @back-to-login="navigateTo('/auth/login')" />
131
+ </AuthLayout>
132
+ </template>
133
+
134
+ <script setup lang="ts">
135
+ definePageMeta({ middleware: 'guest' })
136
+ </script>
137
+ ```
138
+
139
+ #### `pages/auth/reset-password.vue`
140
+ ```vue
141
+ <template>
142
+ <AuthLayout app-name="MonApp">
143
+ <AuthResetPasswordForm @back-to-login="navigateTo('/auth/login')" />
144
+ </AuthLayout>
145
+ </template>
146
+
147
+ <script setup lang="ts">
148
+ definePageMeta({ middleware: 'guest' })
149
+ </script>
150
+ ```
151
+
152
+ ---
153
+
154
+ ### Page profil
155
+
156
+ #### `pages/profile/index.vue`
157
+ ```vue
158
+ <template>
159
+ <div class="max-w-2xl mx-auto py-10 px-4 space-y-10">
160
+ <ProfileUpdateForm title="Mon profil" :show-avatar="true" />
161
+ <hr class="border-gray-200" />
162
+ <ProfileUpdatePasswordForm title="Changer le mot de passe" />
163
+ </div>
164
+ </template>
165
+
166
+ <script setup lang="ts">
167
+ definePageMeta({ middleware: 'auth' })
168
+ </script>
169
+ ```
170
+
171
+ ---
172
+
173
+ ### Le composable `useAuth`
174
+
175
+ ```ts
176
+ const {
177
+ user, // Ref<AuthUser | null>
178
+ isAuthenticated, // ComputedRef<boolean>
179
+ isGuest, // ComputedRef<boolean>
180
+ loading, // Ref<boolean>
181
+
182
+ // Actions
183
+ login,
184
+ register,
185
+ logout,
186
+ fetchUser,
187
+ updateProfile,
188
+ updatePassword,
189
+ forgotPassword,
190
+ resetPassword,
191
+
192
+ // RBAC
193
+ hasRole, // (role: string | string[]) => boolean
194
+ hasPermission // (perm: string | string[]) => boolean
195
+ } = useAuth()
196
+ ```
197
+
198
+ #### Exemples RBAC
199
+ ```ts
200
+ const { hasRole, hasPermission, user } = useAuth()
201
+
202
+ // Vérifier un rôle
203
+ if (hasRole('admin')) { ... }
204
+ if (hasRole(['admin', 'manager'])) { ... }
205
+
206
+ // Vérifier une permission
207
+ if (hasPermission('edit-posts')) { ... }
208
+
209
+ // Dans un template
210
+ <div v-if="hasRole('admin')">Section admin</div>
211
+ ```
212
+
213
+ ---
214
+
215
+ ### Middlewares
216
+
217
+ ```ts
218
+ // Page protégée (utilisateurs connectés uniquement)
219
+ definePageMeta({ middleware: 'auth' })
220
+
221
+ // Page publique (invités uniquement, redirige si connecté)
222
+ definePageMeta({ middleware: 'guest' })
223
+
224
+ // Page avec rôle requis
225
+ definePageMeta({
226
+ middleware: 'role',
227
+ roles: ['admin', 'manager'] // au moins un des rôles
228
+ })
229
+ ```
230
+
231
+ ---
232
+
233
+ ## API Laravel attendue
234
+
235
+ Le package attend les endpoints suivants (personnalisables) :
236
+
237
+ | Méthode | Route | Description |
238
+ |---------|-------|-------------|
239
+ | `POST` | `/api/auth/login` | Connexion → `{ user, token }` |
240
+ | `POST` | `/api/auth/register` | Inscription → `{ user, token }` |
241
+ | `POST` | `/api/auth/logout` | Déconnexion |
242
+ | `GET` | `/api/auth/me` | Utilisateur connecté → `{ user }` |
243
+ | `PUT` | `/api/auth/profile` | Mise à jour profil → `{ user }` |
244
+ | `PUT` | `/api/auth/password` | Changement mot de passe |
245
+ | `POST` | `/api/auth/forgot-password` | Email de réinitialisation |
246
+ | `POST` | `/api/auth/reset-password` | Réinitialisation avec token |
247
+
248
+ ### Exemple avec Laravel Sanctum
249
+
250
+ ```php
251
+ // routes/api.php
252
+ Route::prefix('auth')->group(function () {
253
+ Route::post('login', [AuthController::class, 'login']);
254
+ Route::post('register', [AuthController::class, 'register']);
255
+ Route::post('forgot-password', [AuthController::class, 'forgotPassword']);
256
+ Route::post('reset-password', [AuthController::class, 'resetPassword']);
257
+
258
+ Route::middleware('auth:sanctum')->group(function () {
259
+ Route::post('logout', [AuthController::class, 'logout']);
260
+ Route::get('me', [AuthController::class, 'me']);
261
+ Route::put('profile', [AuthController::class, 'updateProfile']);
262
+ Route::put('password', [AuthController::class, 'updatePassword']);
263
+ });
264
+ });
265
+ ```
266
+
267
+ ---
268
+
269
+ ## Architecture du package
270
+
271
+ ```
272
+ nuxt-auth-kit/
273
+ ├── src/
274
+ │ ├── module/
275
+ │ │ └── index.ts # Nuxt module definition
276
+ │ └── runtime/
277
+ │ ├── types/
278
+ │ │ └── index.ts # TypeScript types
279
+ │ ├── stores/
280
+ │ │ └── auth.ts # Pinia store
281
+ │ ├── composables/
282
+ │ │ └── useAuth.ts # Main composable
283
+ │ ├── plugins/
284
+ │ │ └── auth.ts # Session restore plugin
285
+ │ ├── middleware/
286
+ │ │ ├── auth.ts # Protected routes
287
+ │ │ ├── guest.ts # Guest-only routes
288
+ │ │ └── role.ts # Role-based access
289
+ │ └── components/
290
+ │ ├── auth/
291
+ │ │ ├── AuthLayout.vue # Split-screen layout
292
+ │ │ ├── LoginForm.vue # Login form
293
+ │ │ ├── RegisterForm.vue # Register form
294
+ │ │ ├── ForgotPasswordForm.vue # Forgot password
295
+ │ │ └── ResetPasswordForm.vue # Reset password
296
+ │ └── profile/
297
+ │ ├── UpdateProfileForm.vue # Profile update
298
+ │ └── UpdatePasswordForm.vue # Password update
299
+ ├── package.json
300
+ └── README.md
301
+ ```
302
+
303
+ ---
304
+
305
+ ## Mise en ligne (publication npm)
306
+
307
+ Voir le fichier `PUBLISH.md` pour la procédure complète.
@@ -0,0 +1,5 @@
1
+ module.exports = function(...args) {
2
+ return import('./module.mjs').then(m => m.default.call(this, ...args))
3
+ }
4
+ const _meta = module.exports.meta = require('./module.json')
5
+ module.exports.getMeta = () => Promise.resolve(_meta)
@@ -0,0 +1,5 @@
1
+ export { AuthUser, LoginCredentials, ModuleOptions, RegisterData } from '../dist/runtime/types/index.js';
2
+
3
+ declare const _default: any;
4
+
5
+ export { _default as default };
@@ -0,0 +1,5 @@
1
+ export { AuthUser, LoginCredentials, ModuleOptions, RegisterData } from '../dist/runtime/types/index.js';
2
+
3
+ declare const _default: any;
4
+
5
+ export { _default as default };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "nuxt-auth-kit",
3
+ "configKey": "nuxtAuthKit",
4
+ "compatibility": {
5
+ "nuxt": "^3.0.0"
6
+ },
7
+ "version": "1.0.0",
8
+ "builder": {
9
+ "@nuxt/module-builder": "0.8.4",
10
+ "unbuild": "2.0.0"
11
+ }
12
+ }
@@ -0,0 +1,92 @@
1
+ import { defineNuxtModule, createResolver, addPlugin, addImports, addRouteMiddleware, addComponent } from '@nuxt/kit';
2
+
3
+ const module = defineNuxtModule({
4
+ meta: {
5
+ name: "nuxt-auth-kit",
6
+ configKey: "nuxtAuthKit",
7
+ compatibility: {
8
+ nuxt: "^3.0.0"
9
+ }
10
+ },
11
+ defaults: {
12
+ apiBase: process.env.NUXT_PUBLIC_API_BASE || "http://localhost:8000",
13
+ tokenStorage: "cookie",
14
+ tokenCookieName: "auth_token",
15
+ endpoints: {
16
+ login: "/api/auth/login",
17
+ register: "/api/auth/register",
18
+ logout: "/api/auth/logout",
19
+ me: "/api/auth/me",
20
+ updateProfile: "/api/auth/profile",
21
+ updatePassword: "/api/auth/password",
22
+ forgotPassword: "/api/auth/forgot-password",
23
+ resetPassword: "/api/auth/reset-password"
24
+ },
25
+ redirects: {
26
+ login: "/auth/login",
27
+ home: "/",
28
+ afterLogout: "/auth/login"
29
+ },
30
+ rbac: {
31
+ superAdminRole: "super-admin",
32
+ defaultUserRole: "user"
33
+ }
34
+ },
35
+ setup(options, nuxt) {
36
+ const resolver = createResolver(import.meta.url);
37
+ nuxt.options.runtimeConfig.public.nuxtAuthKit = {
38
+ apiBase: options.apiBase,
39
+ endpoints: options.endpoints,
40
+ redirects: options.redirects,
41
+ tokenCookieName: options.tokenCookieName,
42
+ rbac: options.rbac
43
+ };
44
+ if (!nuxt.options.modules?.includes("@pinia/nuxt")) {
45
+ nuxt.options.modules.push("@pinia/nuxt");
46
+ }
47
+ addPlugin(resolver.resolve("./runtime/plugins/auth"));
48
+ addImports([
49
+ {
50
+ name: "useAuth",
51
+ as: "useAuth",
52
+ from: resolver.resolve("./runtime/composables/useAuth")
53
+ },
54
+ {
55
+ name: "useAuthStore",
56
+ as: "useAuthStore",
57
+ from: resolver.resolve("./runtime/stores/auth")
58
+ }
59
+ ]);
60
+ addRouteMiddleware({
61
+ name: "auth",
62
+ path: resolver.resolve("./runtime/middleware/auth"),
63
+ global: false
64
+ });
65
+ addRouteMiddleware({
66
+ name: "guest",
67
+ path: resolver.resolve("./runtime/middleware/guest"),
68
+ global: false
69
+ });
70
+ addRouteMiddleware({
71
+ name: "role",
72
+ path: resolver.resolve("./runtime/middleware/role"),
73
+ global: false
74
+ });
75
+ const components = [
76
+ // Auth
77
+ { name: "AuthLayout", filePath: resolver.resolve("./runtime/components/auth/AuthLayout.vue") },
78
+ { name: "AuthLoginForm", filePath: resolver.resolve("./runtime/components/auth/LoginForm.vue") },
79
+ { name: "AuthRegisterForm", filePath: resolver.resolve("./runtime/components/auth/RegisterForm.vue") },
80
+ { name: "AuthForgotPasswordForm", filePath: resolver.resolve("./runtime/components/auth/ForgotPasswordForm.vue") },
81
+ { name: "AuthResetPasswordForm", filePath: resolver.resolve("./runtime/components/auth/ResetPasswordForm.vue") },
82
+ // Profile
83
+ { name: "ProfileUpdateForm", filePath: resolver.resolve("./runtime/components/profile/UpdateProfileForm.vue") },
84
+ { name: "ProfileUpdatePasswordForm", filePath: resolver.resolve("./runtime/components/profile/UpdatePasswordForm.vue") }
85
+ ];
86
+ for (const component of components) {
87
+ addComponent(component);
88
+ }
89
+ }
90
+ });
91
+
92
+ export { module as default };
@@ -0,0 +1,117 @@
1
+ <template>
2
+ <div class="auth-layout min-h-screen flex">
3
+ <!-- Left Panel: Form -->
4
+ <div class="auth-layout__form w-full lg:w-1/2 flex flex-col justify-center px-8 md:px-16 py-12 bg-[#EEEEE6]">
5
+ <!-- Logo -->
6
+ <div class="mb-10">
7
+ <slot name="logo">
8
+ <div class="flex items-center gap-2">
9
+ <div class="w-10 h-10 bg-[#1B4332] rounded-xl flex items-center justify-center">
10
+ <svg class="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
11
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
12
+ </svg>
13
+ </div>
14
+ <span class="text-xl font-bold text-[#1B4332]">{{ appName }}</span>
15
+ </div>
16
+ </slot>
17
+ </div>
18
+
19
+ <!-- Form Content -->
20
+ <div class="max-w-md w-full mx-auto">
21
+ <slot />
22
+ </div>
23
+ </div>
24
+
25
+ <!-- Right Panel: Testimonial / Illustration -->
26
+ <div class="hidden lg:flex lg:w-1/2 bg-[#F5F5ED] flex-col justify-between p-16 relative overflow-hidden">
27
+ <div class="relative z-10">
28
+ <!-- Quote -->
29
+ <div class="text-[#D97706] text-5xl font-serif leading-none mb-6">"</div>
30
+ <blockquote class="text-3xl font-medium text-[#1a2e1a] leading-relaxed mb-4">
31
+ {{ quote.text }}
32
+ </blockquote>
33
+ <div class="text-[#D97706] text-5xl font-serif leading-none text-right mb-8">"</div>
34
+
35
+ <!-- Author -->
36
+ <div class="flex items-center gap-3">
37
+ <div v-if="quote.avatar" class="w-12 h-12 rounded-full overflow-hidden bg-gray-200">
38
+ <img :src="quote.avatar" :alt="quote.author" class="w-full h-full object-cover" />
39
+ </div>
40
+ <div v-else class="w-12 h-12 rounded-full bg-[#1B4332] flex items-center justify-center">
41
+ <span class="text-white text-lg font-semibold">{{ quote.author.charAt(0) }}</span>
42
+ </div>
43
+ <div>
44
+ <p class="font-semibold text-[#1a2e1a]">{{ quote.author }}</p>
45
+ <p class="text-sm text-[#6b7c6b]">{{ quote.location }}</p>
46
+ </div>
47
+ </div>
48
+ </div>
49
+
50
+ <!-- Illustration / Decoration -->
51
+ <div class="absolute bottom-0 right-0 left-0 pointer-events-none">
52
+ <slot name="illustration">
53
+ <svg viewBox="0 0 600 400" class="w-full opacity-80" fill="none" xmlns="http://www.w3.org/2000/svg">
54
+ <!-- City buildings illustration -->
55
+ <rect x="320" y="120" width="80" height="280" rx="4" stroke="#2d4a3e" stroke-width="2" fill="#F5EFD6" />
56
+ <rect x="340" y="140" width="12" height="18" rx="1" fill="#a8c4b0" stroke="#2d4a3e" stroke-width="1.5" />
57
+ <rect x="360" y="140" width="12" height="18" rx="1" fill="#a8c4b0" stroke="#2d4a3e" stroke-width="1.5" />
58
+ <rect x="340" y="175" width="12" height="18" rx="1" fill="#a8c4b0" stroke="#2d4a3e" stroke-width="1.5" />
59
+ <rect x="360" y="175" width="12" height="18" rx="1" fill="#a8c4b0" stroke="#2d4a3e" stroke-width="1.5" />
60
+ <rect x="340" y="210" width="12" height="18" rx="1" fill="#a8c4b0" stroke="#2d4a3e" stroke-width="1.5" />
61
+ <rect x="360" y="210" width="12" height="18" rx="1" fill="#a8c4b0" stroke="#2d4a3e" stroke-width="1.5" />
62
+
63
+ <rect x="410" y="60" width="100" height="340" rx="4" stroke="#2d4a3e" stroke-width="2" fill="#F0E8C8" />
64
+ <rect x="420" y="80" width="15" height="20" rx="1" fill="#b0cfc0" stroke="#2d4a3e" stroke-width="1.5" />
65
+ <rect x="445" y="80" width="15" height="20" rx="1" fill="#b0cfc0" stroke="#2d4a3e" stroke-width="1.5" />
66
+ <rect x="470" y="80" width="15" height="20" rx="1" fill="#b0cfc0" stroke="#2d4a3e" stroke-width="1.5" />
67
+ <rect x="420" y="115" width="15" height="20" rx="1" fill="#b0cfc0" stroke="#2d4a3e" stroke-width="1.5" />
68
+ <rect x="445" y="115" width="15" height="20" rx="1" fill="#b0cfc0" stroke="#2d4a3e" stroke-width="1.5" />
69
+ <rect x="470" y="115" width="15" height="20" rx="1" fill="#b0cfc0" stroke="#2d4a3e" stroke-width="1.5" />
70
+ <rect x="420" y="150" width="15" height="20" rx="1" fill="#b0cfc0" stroke="#2d4a3e" stroke-width="1.5" />
71
+ <rect x="445" y="150" width="15" height="20" rx="1" fill="#b0cfc0" stroke="#2d4a3e" stroke-width="1.5" />
72
+ <rect x="470" y="150" width="15" height="20" rx="1" fill="#b0cfc0" stroke="#2d4a3e" stroke-width="1.5" />
73
+
74
+ <rect x="520" y="180" width="70" height="220" rx="4" stroke="#2d4a3e" stroke-width="2" fill="#d4e8d8" />
75
+ <rect x="530" y="200" width="12" height="16" rx="1" fill="#88b898" stroke="#2d4a3e" stroke-width="1.5" />
76
+ <rect x="550" y="200" width="12" height="16" rx="1" fill="#88b898" stroke="#2d4a3e" stroke-width="1.5" />
77
+ <rect x="530" y="230" width="12" height="16" rx="1" fill="#88b898" stroke="#2d4a3e" stroke-width="1.5" />
78
+ <rect x="550" y="230" width="12" height="16" rx="1" fill="#88b898" stroke="#2d4a3e" stroke-width="1.5" />
79
+
80
+ <!-- Trees -->
81
+ <circle cx="350" cy="370" r="20" stroke="#2d4a3e" stroke-width="2" fill="#a8d4b0" />
82
+ <rect x="347" y="370" width="6" height="30" fill="#2d4a3e" />
83
+ <circle cx="480" cy="365" r="16" stroke="#2d4a3e" stroke-width="2" fill="#88c498" />
84
+ <rect x="477" y="365" width="5" height="25" fill="#2d4a3e" />
85
+
86
+ <!-- Person -->
87
+ <circle cx="300" cy="340" r="18" stroke="#2d4a3e" stroke-width="2" fill="#d4c8a8" />
88
+ <path d="M280 400 Q300 355 320 400" stroke="#2d4a3e" stroke-width="2" fill="#a8c4b0" />
89
+ </svg>
90
+ </slot>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </template>
95
+
96
+ <script setup lang="ts">
97
+ withDefaults(defineProps<{
98
+ appName?: string
99
+ quote?: {
100
+ text: string
101
+ author: string
102
+ location: string
103
+ avatar?: string
104
+ }
105
+ }>(), {
106
+ appName: 'App',
107
+ quote: () => ({
108
+ text: 'Une expérience fluide et agréable. La plateforme rend tout si simple.',
109
+ author: 'Alex Mitchell',
110
+ location: 'Paris, France'
111
+ })
112
+ })
113
+ </script>
114
+
115
+ <style scoped>
116
+ .auth-layout{font-family:inherit}
117
+ </style>
@@ -0,0 +1,110 @@
1
+ <template>
2
+ <div class="auth-forgot-password">
3
+ <div v-if="!sent">
4
+ <h1 class="text-3xl font-bold text-[#1a2e1a] mb-2">{{ title }}</h1>
5
+ <p class="text-[#6b7c6b] mb-8">{{ subtitle }}</p>
6
+
7
+ <form @submit.prevent="handleSubmit" class="space-y-4">
8
+ <div v-if="error" class="bg-red-50 border border-red-200 text-red-700 rounded-xl px-4 py-3 text-sm">
9
+ {{ error }}
10
+ </div>
11
+
12
+ <div>
13
+ <label class="block text-sm font-medium text-[#1a2e1a] mb-1.5">
14
+ Email <span class="text-red-500">*</span>
15
+ </label>
16
+ <div class="relative">
17
+ <span class="absolute left-4 top-1/2 -translate-y-1/2 text-[#8a9a8a]">
18
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
19
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
20
+ </svg>
21
+ </span>
22
+ <input
23
+ v-model="email"
24
+ type="email"
25
+ placeholder="hello@example.com"
26
+ required
27
+ autocomplete="email"
28
+ class="w-full bg-white border border-[#e0e0d8] rounded-2xl py-3 pl-11 pr-4 text-[#1a2e1a] placeholder-[#aab4aa] focus:outline-none focus:border-[#1B4332] focus:ring-2 focus:ring-[#1B4332]/20 transition"
29
+ />
30
+ </div>
31
+ </div>
32
+
33
+ <button
34
+ type="submit"
35
+ :disabled="loading"
36
+ class="w-full bg-[#1B4332] hover:bg-[#163828] text-[#D4FF6B] font-semibold py-3.5 rounded-2xl transition-colors disabled:opacity-60 mt-2"
37
+ >
38
+ <span v-if="!loading">Envoyer le lien de réinitialisation</span>
39
+ <span v-else class="flex items-center justify-center gap-2">
40
+ <svg class="animate-spin w-4 h-4" viewBox="0 0 24 24" fill="none">
41
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
42
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
43
+ </svg>
44
+ Envoi en cours...
45
+ </span>
46
+ </button>
47
+ </form>
48
+ </div>
49
+
50
+ <!-- Success state -->
51
+ <div v-else class="text-center">
52
+ <div class="w-16 h-16 bg-[#1B4332]/10 rounded-full flex items-center justify-center mx-auto mb-6">
53
+ <svg class="w-8 h-8 text-[#1B4332]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
54
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
55
+ </svg>
56
+ </div>
57
+ <h2 class="text-2xl font-bold text-[#1a2e1a] mb-3">Email envoyé !</h2>
58
+ <p class="text-[#6b7c6b] mb-8">
59
+ Si un compte existe pour <strong>{{ email }}</strong>, vous recevrez un lien de réinitialisation sous peu.
60
+ </p>
61
+ <button
62
+ type="button"
63
+ @click="sent = false; email = ''"
64
+ class="text-[#1B4332] font-semibold hover:underline text-sm"
65
+ >
66
+ Utiliser un autre email
67
+ </button>
68
+ </div>
69
+
70
+ <p class="text-center text-sm text-[#6b7c6b] mt-6">
71
+ <button type="button" @click="$emit('back-to-login')" class="text-[#1B4332] font-semibold hover:underline flex items-center gap-1 mx-auto">
72
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
73
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
74
+ </svg>
75
+ Retour à la connexion
76
+ </button>
77
+ </p>
78
+ </div>
79
+ </template>
80
+
81
+ <script setup lang="ts">
82
+ import { ref } from 'vue'
83
+ import { useAuth } from '../../composables/useAuth'
84
+
85
+ withDefaults(defineProps<{
86
+ title?: string
87
+ subtitle?: string
88
+ }>(), {
89
+ title: 'Mot de passe oublié ?',
90
+ subtitle: 'Saisissez votre email pour recevoir un lien de réinitialisation.'
91
+ })
92
+
93
+ defineEmits<{ 'back-to-login': [] }>()
94
+
95
+ const { forgotPassword, loading } = useAuth()
96
+
97
+ const email = ref('')
98
+ const error = ref<string | null>(null)
99
+ const sent = ref(false)
100
+
101
+ async function handleSubmit() {
102
+ error.value = null
103
+ const result = await forgotPassword({ email: email.value })
104
+ if (result.success) {
105
+ sent.value = true
106
+ } else {
107
+ error.value = result.error?.message || 'Une erreur est survenue'
108
+ }
109
+ }
110
+ </script>