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 +307 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +5 -0
- package/dist/module.d.ts +5 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +92 -0
- package/dist/runtime/components/auth/AuthLayout.vue +117 -0
- package/dist/runtime/components/auth/ForgotPasswordForm.vue +110 -0
- package/dist/runtime/components/auth/LoginForm.vue +209 -0
- package/dist/runtime/components/auth/RegisterForm.vue +182 -0
- package/dist/runtime/components/auth/ResetPasswordForm.vue +134 -0
- package/dist/runtime/components/profile/UpdatePasswordForm.vue +115 -0
- package/dist/runtime/components/profile/UpdateProfileForm.vue +157 -0
- package/dist/runtime/composables/useAuth.d.ts +58 -0
- package/dist/runtime/composables/useAuth.js +214 -0
- package/dist/runtime/middleware/auth.d.ts +2 -0
- package/dist/runtime/middleware/auth.js +14 -0
- package/dist/runtime/middleware/guest.d.ts +2 -0
- package/dist/runtime/middleware/guest.js +11 -0
- package/dist/runtime/middleware/role.d.ts +2 -0
- package/dist/runtime/middleware/role.js +18 -0
- package/dist/runtime/plugins/auth.d.ts +2 -0
- package/dist/runtime/plugins/auth.js +20 -0
- package/dist/runtime/stores/auth.d.ts +38 -0
- package/dist/runtime/stores/auth.js +47 -0
- package/dist/runtime/types/index.d.ts +85 -0
- package/dist/runtime/types/index.js +0 -0
- package/dist/types.d.mts +1 -0
- package/dist/types.d.ts +1 -0
- package/package.json +42 -0
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.
|
package/dist/module.cjs
ADDED
package/dist/module.d.ts
ADDED
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -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>
|