create-crm-tmp 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/bin/create-crm-tmp.js +93 -0
- package/package.json +25 -0
- package/template/.prettierignore +33 -0
- package/template/.prettierrc.json +25 -0
- package/template/README.md +173 -0
- package/template/eslint.config.mjs +18 -0
- package/template/exemple-contacts.csv +11 -0
- package/template/next.config.ts +8 -0
- package/template/package.json +64 -0
- package/template/postcss.config.mjs +7 -0
- package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
- package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +582 -0
- package/template/prisma.config.ts +14 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
- package/template/src/app/(auth)/layout.tsx +3 -0
- package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
- package/template/src/app/(auth)/reset-password/page.tsx +146 -0
- package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
- package/template/src/app/(auth)/signin/page.tsx +166 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
- package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
- package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
- package/template/src/app/(dashboard)/layout.tsx +30 -0
- package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
- package/template/src/app/(dashboard)/templates/page.tsx +567 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
- package/template/src/app/(dashboard)/users/page.tsx +457 -0
- package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
- package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
- package/template/src/app/api/audit-logs/route.ts +57 -0
- package/template/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/src/app/api/auth/check-active/route.ts +31 -0
- package/template/src/app/api/auth/google/callback/route.ts +94 -0
- package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
- package/template/src/app/api/auth/google/route.ts +34 -0
- package/template/src/app/api/auth/google/status/route.ts +32 -0
- package/template/src/app/api/closing-reasons/route.ts +27 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
- package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
- package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
- package/template/src/app/api/contacts/[id]/route.ts +322 -0
- package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
- package/template/src/app/api/contacts/export/route.ts +270 -0
- package/template/src/app/api/contacts/import/route.ts +381 -0
- package/template/src/app/api/contacts/route.ts +283 -0
- package/template/src/app/api/dashboard/stats/route.ts +299 -0
- package/template/src/app/api/email/track/[id]/route.ts +68 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
- package/template/src/app/api/invite/complete/route.ts +88 -0
- package/template/src/app/api/invite/validate/route.ts +55 -0
- package/template/src/app/api/reminders/route.ts +95 -0
- package/template/src/app/api/reset-password/complete/route.ts +73 -0
- package/template/src/app/api/reset-password/request/route.ts +84 -0
- package/template/src/app/api/reset-password/validate/route.ts +49 -0
- package/template/src/app/api/reset-password/verify/route.ts +74 -0
- package/template/src/app/api/roles/[id]/route.ts +183 -0
- package/template/src/app/api/roles/route.ts +140 -0
- package/template/src/app/api/send/route.ts +282 -0
- package/template/src/app/api/settings/change-password/route.ts +95 -0
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
- package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
- package/template/src/app/api/settings/company/route.ts +121 -0
- package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
- package/template/src/app/api/settings/google-ads/route.ts +122 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
- package/template/src/app/api/settings/google-sheet/route.ts +254 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
- package/template/src/app/api/settings/meta-leads/route.ts +132 -0
- package/template/src/app/api/settings/profile/route.ts +42 -0
- package/template/src/app/api/settings/smtp/route.ts +130 -0
- package/template/src/app/api/settings/smtp/test/route.ts +121 -0
- package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
- package/template/src/app/api/settings/statuses/route.ts +83 -0
- package/template/src/app/api/statuses/route.ts +25 -0
- package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
- package/template/src/app/api/tasks/[id]/route.ts +728 -0
- package/template/src/app/api/tasks/meet/route.ts +240 -0
- package/template/src/app/api/tasks/route.ts +417 -0
- package/template/src/app/api/templates/[id]/route.ts +140 -0
- package/template/src/app/api/templates/route.ts +91 -0
- package/template/src/app/api/users/[id]/route.ts +168 -0
- package/template/src/app/api/users/list/route.ts +45 -0
- package/template/src/app/api/users/me/route.ts +48 -0
- package/template/src/app/api/users/route.ts +250 -0
- package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
- package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
- package/template/src/app/api/workflows/[id]/route.ts +192 -0
- package/template/src/app/api/workflows/process/route.ts +293 -0
- package/template/src/app/api/workflows/route.ts +124 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +1416 -0
- package/template/src/app/layout.tsx +31 -0
- package/template/src/app/page.tsx +32 -0
- package/template/src/components/dashboard/activity-chart.tsx +67 -0
- package/template/src/components/dashboard/contacts-chart.tsx +63 -0
- package/template/src/components/dashboard/recent-activity.tsx +164 -0
- package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
- package/template/src/components/dashboard/stat-card.tsx +61 -0
- package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
- package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
- package/template/src/components/editor.tsx +856 -0
- package/template/src/components/email-template.tsx +35 -0
- package/template/src/components/header.tsx +320 -0
- package/template/src/components/invitation-email-template.tsx +79 -0
- package/template/src/components/meet-cancellation-email-template.tsx +120 -0
- package/template/src/components/meet-confirmation-email-template.tsx +156 -0
- package/template/src/components/meet-update-email-template.tsx +209 -0
- package/template/src/components/page-header.tsx +61 -0
- package/template/src/components/reset-password-email-template.tsx +79 -0
- package/template/src/components/sidebar.tsx +294 -0
- package/template/src/components/skeleton.tsx +380 -0
- package/template/src/components/ui/commands.tsx +396 -0
- package/template/src/components/ui/components.tsx +150 -0
- package/template/src/components/ui/theme.tsx +5 -0
- package/template/src/components/view-as-banner.tsx +45 -0
- package/template/src/components/view-as-modal.tsx +186 -0
- package/template/src/contexts/mobile-menu-context.tsx +31 -0
- package/template/src/contexts/sidebar-context.tsx +107 -0
- package/template/src/contexts/task-reminder-context.tsx +239 -0
- package/template/src/contexts/view-as-context.tsx +84 -0
- package/template/src/hooks/use-user-role.ts +82 -0
- package/template/src/lib/audit-log.ts +45 -0
- package/template/src/lib/auth-client.ts +16 -0
- package/template/src/lib/auth.ts +35 -0
- package/template/src/lib/check-permission.ts +193 -0
- package/template/src/lib/contact-duplicate.ts +112 -0
- package/template/src/lib/contact-interactions.ts +371 -0
- package/template/src/lib/encryption.ts +99 -0
- package/template/src/lib/google-calendar.ts +300 -0
- package/template/src/lib/google-drive.ts +372 -0
- package/template/src/lib/permissions.ts +412 -0
- package/template/src/lib/prisma.ts +32 -0
- package/template/src/lib/roles.ts +120 -0
- package/template/src/lib/template-variables.ts +76 -0
- package/template/src/lib/utils.ts +46 -0
- package/template/src/lib/workflow-executor.ts +482 -0
- package/template/src/proxy.ts +91 -0
- package/template/tsconfig.json +34 -0
- package/template/vercel.json +8 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useRouter, useParams } from 'next/navigation';
|
|
5
|
+
import { Eye, EyeOff } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
export default function InvitePage() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const params = useParams();
|
|
10
|
+
const token = params.token as string;
|
|
11
|
+
|
|
12
|
+
const [password, setPassword] = useState('');
|
|
13
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
14
|
+
const [error, setError] = useState('');
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
const [validating, setValidating] = useState(true);
|
|
17
|
+
const [userEmail, setUserEmail] = useState('');
|
|
18
|
+
const [userName, setUserName] = useState('');
|
|
19
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
20
|
+
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
21
|
+
|
|
22
|
+
// Vérifier que le token est valide au chargement
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const validateToken = async () => {
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(`/api/invite/validate?token=${token}`);
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
setError(data.error || 'Lien invalide ou expiré');
|
|
31
|
+
setValidating(false);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setUserEmail(data.email);
|
|
36
|
+
setUserName(data.name || '');
|
|
37
|
+
setValidating(false);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
setError('Erreur lors de la validation du lien');
|
|
40
|
+
setValidating(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (token) {
|
|
45
|
+
validateToken();
|
|
46
|
+
}
|
|
47
|
+
}, [token]);
|
|
48
|
+
|
|
49
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
setError('');
|
|
52
|
+
|
|
53
|
+
if (password !== confirmPassword) {
|
|
54
|
+
setError('Les mots de passe ne correspondent pas');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (password.length < 6) {
|
|
59
|
+
setError('Le mot de passe doit contenir au moins 6 caractères');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setLoading(true);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch('/api/invite/complete', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
|
+
body: JSON.stringify({ token, password }),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const data = await response.json();
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error(data.error || 'Erreur lors de la définition du mot de passe');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Rediriger vers la page de connexion
|
|
79
|
+
router.push(
|
|
80
|
+
'/signin?message=Mot de passe défini avec succès, vous pouvez maintenant vous connecter',
|
|
81
|
+
);
|
|
82
|
+
} catch (err: any) {
|
|
83
|
+
setError(err.message);
|
|
84
|
+
} finally {
|
|
85
|
+
setLoading(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (validating) {
|
|
90
|
+
return (
|
|
91
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
92
|
+
<div className="w-full max-w-md rounded-2xl bg-white p-8 text-center shadow-xl">
|
|
93
|
+
<p className="text-gray-600">Vérification du lien...</p>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (error && !userEmail) {
|
|
100
|
+
return (
|
|
101
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
102
|
+
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
|
|
103
|
+
<div className="text-center">
|
|
104
|
+
<h1 className="text-2xl font-bold text-red-600">Lien invalide</h1>
|
|
105
|
+
<p className="mt-4 text-gray-600">{error}</p>
|
|
106
|
+
<button
|
|
107
|
+
onClick={() => router.push('/signin')}
|
|
108
|
+
className="mt-6 cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-700"
|
|
109
|
+
>
|
|
110
|
+
Retour à la connexion
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
120
|
+
<div className="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl sm:p-8">
|
|
121
|
+
<div className="mb-6 text-center sm:mb-8">
|
|
122
|
+
<h1 className="text-2xl font-bold text-gray-900 sm:text-3xl">
|
|
123
|
+
Définir votre mot de passe
|
|
124
|
+
</h1>
|
|
125
|
+
{userName && (
|
|
126
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
127
|
+
Bienvenue, <span className="font-semibold">{userName}</span>
|
|
128
|
+
</p>
|
|
129
|
+
)}
|
|
130
|
+
<p className="mt-1 text-sm break-all text-gray-500">{userEmail}</p>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{error && <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>}
|
|
134
|
+
|
|
135
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
136
|
+
<div>
|
|
137
|
+
<label className="block text-sm font-medium text-gray-700">Mot de passe</label>
|
|
138
|
+
<div className="relative mt-1">
|
|
139
|
+
<input
|
|
140
|
+
type={showPassword ? 'text' : 'password'}
|
|
141
|
+
required
|
|
142
|
+
minLength={6}
|
|
143
|
+
value={password}
|
|
144
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
145
|
+
className="block w-full rounded-lg border border-gray-300 px-4 py-3 pr-10 text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
146
|
+
placeholder="••••••••"
|
|
147
|
+
/>
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
onClick={() => setShowPassword((prev) => !prev)}
|
|
151
|
+
className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400 hover:text-gray-600"
|
|
152
|
+
aria-label={showPassword ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
|
|
153
|
+
>
|
|
154
|
+
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
<p className="mt-1 text-xs text-gray-500">Minimum 6 caractères</p>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div>
|
|
161
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
162
|
+
Confirmer le mot de passe
|
|
163
|
+
</label>
|
|
164
|
+
<div className="relative mt-1">
|
|
165
|
+
<input
|
|
166
|
+
type={showConfirmPassword ? 'text' : 'password'}
|
|
167
|
+
required
|
|
168
|
+
minLength={6}
|
|
169
|
+
value={confirmPassword}
|
|
170
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
171
|
+
className="block w-full rounded-lg border border-gray-300 px-4 py-3 pr-10 text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
172
|
+
placeholder="••••••••"
|
|
173
|
+
/>
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
onClick={() => setShowConfirmPassword((prev) => !prev)}
|
|
177
|
+
className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400 hover:text-gray-600"
|
|
178
|
+
aria-label={
|
|
179
|
+
showConfirmPassword
|
|
180
|
+
? 'Masquer la confirmation du mot de passe'
|
|
181
|
+
: 'Afficher la confirmation du mot de passe'
|
|
182
|
+
}
|
|
183
|
+
>
|
|
184
|
+
{showConfirmPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<button
|
|
190
|
+
type="submit"
|
|
191
|
+
disabled={loading}
|
|
192
|
+
className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
193
|
+
>
|
|
194
|
+
{loading ? 'Création du compte...' : 'Créer mon compte'}
|
|
195
|
+
</button>
|
|
196
|
+
</form>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, Suspense } from 'react';
|
|
4
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
5
|
+
import { Eye, EyeOff } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
function ResetPasswordCompleteContent() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const searchParams = useSearchParams();
|
|
10
|
+
const token = searchParams.get('token') || '';
|
|
11
|
+
|
|
12
|
+
const [password, setPassword] = useState('');
|
|
13
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
14
|
+
const [error, setError] = useState('');
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
const [validating, setValidating] = useState(true);
|
|
17
|
+
const [userEmail, setUserEmail] = useState('');
|
|
18
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
19
|
+
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
20
|
+
|
|
21
|
+
// Vérifier que le token est valide au chargement
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const validateToken = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(`/api/reset-password/validate?token=${token}`);
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
setError(data.error || 'Lien invalide ou expiré');
|
|
30
|
+
setValidating(false);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setUserEmail(data.email);
|
|
35
|
+
setValidating(false);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
setError('Erreur lors de la validation du lien');
|
|
38
|
+
setValidating(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (token) {
|
|
43
|
+
validateToken();
|
|
44
|
+
} else {
|
|
45
|
+
setError('Token manquant');
|
|
46
|
+
setValidating(false);
|
|
47
|
+
}
|
|
48
|
+
}, [token]);
|
|
49
|
+
|
|
50
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
setError('');
|
|
53
|
+
|
|
54
|
+
if (password !== confirmPassword) {
|
|
55
|
+
setError('Les mots de passe ne correspondent pas');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (password.length < 6) {
|
|
60
|
+
setError('Le mot de passe doit contenir au moins 6 caractères');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setLoading(true);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch('/api/reset-password/complete', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ token, password }),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(data.error || 'Erreur lors de la réinitialisation du mot de passe');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Rediriger vers la page de connexion
|
|
80
|
+
router.push(
|
|
81
|
+
'/signin?message=Mot de passe réinitialisé avec succès, vous pouvez maintenant vous connecter',
|
|
82
|
+
);
|
|
83
|
+
} catch (err: any) {
|
|
84
|
+
setError(err.message);
|
|
85
|
+
} finally {
|
|
86
|
+
setLoading(false);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (validating) {
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
93
|
+
<div className="w-full max-w-md rounded-2xl bg-white p-8 text-center shadow-xl">
|
|
94
|
+
<p className="text-gray-600">Vérification du lien...</p>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (error && !userEmail) {
|
|
101
|
+
return (
|
|
102
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
103
|
+
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
|
|
104
|
+
<div className="text-center">
|
|
105
|
+
<h1 className="text-2xl font-bold text-red-600">Lien invalide</h1>
|
|
106
|
+
<p className="mt-4 text-gray-600">{error}</p>
|
|
107
|
+
<button
|
|
108
|
+
onClick={() => router.push('/signin')}
|
|
109
|
+
className="mt-6 cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-700"
|
|
110
|
+
>
|
|
111
|
+
Retour à la connexion
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
121
|
+
<div className="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl sm:p-8">
|
|
122
|
+
<div className="mb-6 text-center sm:mb-8">
|
|
123
|
+
<h1 className="text-2xl font-bold text-gray-900 sm:text-3xl">Nouveau mot de passe</h1>
|
|
124
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
125
|
+
Définissez un nouveau mot de passe pour votre compte
|
|
126
|
+
</p>
|
|
127
|
+
<p className="mt-1 text-sm break-all text-gray-500">{userEmail}</p>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{error && <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>}
|
|
131
|
+
|
|
132
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
133
|
+
<div>
|
|
134
|
+
<label className="block text-sm font-medium text-gray-700">Nouveau mot de passe</label>
|
|
135
|
+
<div className="relative mt-1">
|
|
136
|
+
<input
|
|
137
|
+
type={showPassword ? 'text' : 'password'}
|
|
138
|
+
required
|
|
139
|
+
minLength={6}
|
|
140
|
+
value={password}
|
|
141
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
142
|
+
className="block w-full rounded-lg border border-gray-300 px-4 py-3 pr-10 text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
143
|
+
placeholder="••••••••"
|
|
144
|
+
/>
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={() => setShowPassword((prev) => !prev)}
|
|
148
|
+
className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400 hover:text-gray-600"
|
|
149
|
+
aria-label={showPassword ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
|
|
150
|
+
>
|
|
151
|
+
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
<p className="mt-1 text-xs text-gray-500">Minimum 6 caractères</p>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div>
|
|
158
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
159
|
+
Confirmer le mot de passe
|
|
160
|
+
</label>
|
|
161
|
+
<div className="relative mt-1">
|
|
162
|
+
<input
|
|
163
|
+
type={showConfirmPassword ? 'text' : 'password'}
|
|
164
|
+
required
|
|
165
|
+
minLength={6}
|
|
166
|
+
value={confirmPassword}
|
|
167
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
168
|
+
className="block w-full rounded-lg border border-gray-300 px-4 py-3 pr-10 text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
169
|
+
placeholder="••••••••"
|
|
170
|
+
/>
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
onClick={() => setShowConfirmPassword((prev) => !prev)}
|
|
174
|
+
className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400 hover:text-gray-600"
|
|
175
|
+
aria-label={
|
|
176
|
+
showConfirmPassword
|
|
177
|
+
? 'Masquer la confirmation du mot de passe'
|
|
178
|
+
: 'Afficher la confirmation du mot de passe'
|
|
179
|
+
}
|
|
180
|
+
>
|
|
181
|
+
{showConfirmPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<button
|
|
187
|
+
type="submit"
|
|
188
|
+
disabled={loading}
|
|
189
|
+
className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
190
|
+
>
|
|
191
|
+
{loading ? 'Réinitialisation...' : 'Réinitialiser le mot de passe'}
|
|
192
|
+
</button>
|
|
193
|
+
</form>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default function ResetPasswordCompletePage() {
|
|
200
|
+
return (
|
|
201
|
+
<Suspense
|
|
202
|
+
fallback={
|
|
203
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
204
|
+
<div className="w-full max-w-md rounded-2xl bg-white p-8 text-center shadow-xl">
|
|
205
|
+
<p className="text-gray-600">Chargement...</p>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
}
|
|
209
|
+
>
|
|
210
|
+
<ResetPasswordCompleteContent />
|
|
211
|
+
</Suspense>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
|
|
7
|
+
export default function ResetPasswordPage() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const [email, setEmail] = useState('');
|
|
10
|
+
const [error, setError] = useState('');
|
|
11
|
+
const [success, setSuccess] = useState(false);
|
|
12
|
+
const [loading, setLoading] = useState(false);
|
|
13
|
+
|
|
14
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
setError('');
|
|
17
|
+
setLoading(true);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch('/api/reset-password/request', {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({ email }),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(data.error || "Erreur lors de l'envoi du code");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setSuccess(true);
|
|
33
|
+
} catch (err: any) {
|
|
34
|
+
setError(err.message || "Erreur lors de l'envoi du code");
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (success) {
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
43
|
+
<div className="w-full max-w-md">
|
|
44
|
+
<div className="rounded-2xl bg-white p-6 shadow-xl sm:p-8">
|
|
45
|
+
<div className="mb-6 text-center sm:mb-8">
|
|
46
|
+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-green-100 sm:h-16 sm:w-16">
|
|
47
|
+
<svg
|
|
48
|
+
className="h-6 w-6 text-green-600 sm:h-8 sm:w-8"
|
|
49
|
+
fill="none"
|
|
50
|
+
stroke="currentColor"
|
|
51
|
+
viewBox="0 0 24 24"
|
|
52
|
+
>
|
|
53
|
+
<path
|
|
54
|
+
strokeLinecap="round"
|
|
55
|
+
strokeLinejoin="round"
|
|
56
|
+
strokeWidth={2}
|
|
57
|
+
d="M5 13l4 4L19 7"
|
|
58
|
+
/>
|
|
59
|
+
</svg>
|
|
60
|
+
</div>
|
|
61
|
+
<h1 className="text-xl font-bold text-gray-900 sm:text-2xl">Code envoyé</h1>
|
|
62
|
+
<p className="mt-2 text-sm break-all text-gray-600">
|
|
63
|
+
Un code à 6 chiffres a été envoyé à <strong>{email}</strong>
|
|
64
|
+
</p>
|
|
65
|
+
<p className="mt-2 text-sm text-gray-500">
|
|
66
|
+
Vérifiez votre boîte de réception et votre dossier spam.
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<button
|
|
71
|
+
onClick={() =>
|
|
72
|
+
router.push(`/reset-password/verify?email=${encodeURIComponent(email)}`)
|
|
73
|
+
}
|
|
74
|
+
className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-indigo-700"
|
|
75
|
+
>
|
|
76
|
+
Continuer
|
|
77
|
+
</button>
|
|
78
|
+
|
|
79
|
+
<Link
|
|
80
|
+
href="/signin"
|
|
81
|
+
className="mt-4 block text-center text-sm text-indigo-600 hover:text-indigo-700"
|
|
82
|
+
>
|
|
83
|
+
Retour à la connexion
|
|
84
|
+
</Link>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
93
|
+
<div className="w-full max-w-md">
|
|
94
|
+
<div className="rounded-2xl bg-white p-6 shadow-xl sm:p-8">
|
|
95
|
+
{/* Header */}
|
|
96
|
+
<div className="mb-6 text-center sm:mb-8">
|
|
97
|
+
<h1 className="text-2xl font-bold text-gray-900 sm:text-3xl">
|
|
98
|
+
Réinitialiser le mot de passe
|
|
99
|
+
</h1>
|
|
100
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
101
|
+
Entrez votre adresse email pour recevoir un code de réinitialisation
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Error Message */}
|
|
106
|
+
{error && (
|
|
107
|
+
<div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{/* Form */}
|
|
111
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
112
|
+
<div>
|
|
113
|
+
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
|
114
|
+
Email
|
|
115
|
+
</label>
|
|
116
|
+
<input
|
|
117
|
+
id="email"
|
|
118
|
+
type="email"
|
|
119
|
+
required
|
|
120
|
+
value={email}
|
|
121
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
122
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
123
|
+
placeholder="vous@exemple.com"
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<button
|
|
128
|
+
type="submit"
|
|
129
|
+
disabled={loading}
|
|
130
|
+
className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
131
|
+
>
|
|
132
|
+
{loading ? 'Envoi...' : 'Envoyer le code'}
|
|
133
|
+
</button>
|
|
134
|
+
</form>
|
|
135
|
+
|
|
136
|
+
<Link
|
|
137
|
+
href="/signin"
|
|
138
|
+
className="mt-4 block text-center text-sm text-indigo-600 hover:text-indigo-700"
|
|
139
|
+
>
|
|
140
|
+
Retour à la connexion
|
|
141
|
+
</Link>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|