nextjs-auth-module 1.1.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 +147 -0
- package/bin/create-auth-app.js +46 -0
- package/bin/init-db.js +128 -0
- package/bin/setup-auth.js +156 -0
- package/package.json +56 -0
- package/sql/schema.sql +144 -0
- package/src/app/api/forgot-password/route.js +79 -0
- package/src/app/api/login/route.js +53 -0
- package/src/app/api/register/route.js +52 -0
- package/src/app/api/reset-password/route.js +91 -0
- package/src/app/forgot-password/page.jsx +126 -0
- package/src/app/login/page.jsx +255 -0
- package/src/app/register/page.jsx +340 -0
- package/src/app/reset-password/page.jsx +179 -0
- package/src/context/AuthContext.jsx +153 -0
- package/src/index.js +5 -0
- package/src/lib/auth.js +42 -0
- package/src/lib/db.js +7 -0
- package/src/lib/email.js +106 -0
- package/src/scripts/init-db.js +47 -0
- package/templates/.env.example +10 -0
- package/templates/layout.jsx +14 -0
- package/templates/page.jsx +289 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useAuth } from '@/context/AuthContext';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { useRouter } from 'next/navigation';
|
|
7
|
+
import { EyeIcon, EyeSlashIcon, UserIcon, EnvelopeIcon, LockClosedIcon, CheckBadgeIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
|
|
8
|
+
import { CheckCircleIcon } from '@heroicons/react/24/solid';
|
|
9
|
+
|
|
10
|
+
export default function RegisterPage() {
|
|
11
|
+
const [formData, setFormData] = useState({
|
|
12
|
+
name: '',
|
|
13
|
+
email: '',
|
|
14
|
+
password: '',
|
|
15
|
+
confirmPassword: '',
|
|
16
|
+
});
|
|
17
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
18
|
+
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
19
|
+
const [error, setError] = useState('');
|
|
20
|
+
const [loading, setLoading] = useState(false);
|
|
21
|
+
const [agreeToTerms, setAgreeToTerms] = useState(false);
|
|
22
|
+
const { register } = useAuth();
|
|
23
|
+
const router = useRouter();
|
|
24
|
+
|
|
25
|
+
// Password strength checker
|
|
26
|
+
const getPasswordStrength = () => {
|
|
27
|
+
const password = formData.password;
|
|
28
|
+
let strength = 0;
|
|
29
|
+
if (password.length >= 6) strength++;
|
|
30
|
+
if (password.match(/[a-z]+/)) strength++;
|
|
31
|
+
if (password.match(/[A-Z]+/)) strength++;
|
|
32
|
+
if (password.match(/[0-9]+/)) strength++;
|
|
33
|
+
if (password.match(/[$@#&!]+/)) strength++;
|
|
34
|
+
return strength;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const passwordStrength = getPasswordStrength();
|
|
38
|
+
const getStrengthText = () => {
|
|
39
|
+
if (passwordStrength <= 2) return { text: 'Weak', color: 'text-red-500', bg: 'bg-red-100' };
|
|
40
|
+
if (passwordStrength <= 3) return { text: 'Medium', color: 'text-yellow-500', bg: 'bg-yellow-100' };
|
|
41
|
+
if (passwordStrength <= 4) return { text: 'Strong', color: 'text-green-500', bg: 'bg-green-100' };
|
|
42
|
+
return { text: 'Very Strong', color: 'text-indigo-500', bg: 'bg-indigo-100' };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleChange = (e) => {
|
|
46
|
+
setFormData({
|
|
47
|
+
...formData,
|
|
48
|
+
[e.target.name]: e.target.value,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleSubmit = async (e) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
setError('');
|
|
55
|
+
|
|
56
|
+
if (!agreeToTerms) {
|
|
57
|
+
setError('Please agree to the terms and conditions');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (formData.password !== formData.confirmPassword) {
|
|
62
|
+
setError('Passwords do not match');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (formData.password.length < 6) {
|
|
67
|
+
setError('Password must be at least 6 characters');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setLoading(true);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await register(formData.name, formData.email, formData.password);
|
|
75
|
+
router.push('/');
|
|
76
|
+
} catch (err) {
|
|
77
|
+
setError(err.message);
|
|
78
|
+
} finally {
|
|
79
|
+
setLoading(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-indigo-50 flex items-center justify-center px-4 py-8 relative overflow-hidden">
|
|
85
|
+
{/* Animated Background */}
|
|
86
|
+
<div className="absolute inset-0 overflow-hidden">
|
|
87
|
+
<div className="absolute -top-40 -right-32 w-80 h-80 bg-indigo-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob"></div>
|
|
88
|
+
<div className="absolute -bottom-40 -left-32 w-80 h-80 bg-purple-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-2000"></div>
|
|
89
|
+
<div className="absolute top-40 left-1/2 w-80 h-80 bg-pink-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-4000"></div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div className="max-w-md w-full relative z-10">
|
|
93
|
+
{/* Logo/Brand */}
|
|
94
|
+
<div className="text-center mb-8">
|
|
95
|
+
{/* <div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-indigo-600 via-purple-600 to-pink-600 rounded-2xl mb-5 shadow-2xl transform hover:scale-105 transition-transform duration-300">
|
|
96
|
+
<span className="text-white text-3xl font-bold">⚡</span>
|
|
97
|
+
</div> */}
|
|
98
|
+
<h2 className="text-4xl font-bold text-gray-900 mb-3 bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
|
99
|
+
Create Account
|
|
100
|
+
</h2>
|
|
101
|
+
<p className="text-gray-600 text-lg">Join us and start your journey 🚀</p>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Registration Form */}
|
|
105
|
+
<div className="bg-white/90 backdrop-blur-sm rounded-3xl shadow-2xl p-8 border border-gray-100">
|
|
106
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
107
|
+
{error && (
|
|
108
|
+
<div className="bg-red-50 border-l-4 border-red-500 text-red-700 px-4 py-3 rounded-lg text-sm flex items-start gap-3">
|
|
109
|
+
<div className="text-red-500">⚠️</div>
|
|
110
|
+
<div>{error}</div>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
{/* Full Name Field */}
|
|
115
|
+
<div className="group">
|
|
116
|
+
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
|
117
|
+
Full Name
|
|
118
|
+
</label>
|
|
119
|
+
<div className="relative">
|
|
120
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
121
|
+
<UserIcon className="h-5 w-5 text-gray-400 group-focus-within:text-indigo-500 transition-colors" />
|
|
122
|
+
</div>
|
|
123
|
+
<input
|
|
124
|
+
type="text"
|
|
125
|
+
name="name"
|
|
126
|
+
value={formData.name}
|
|
127
|
+
onChange={handleChange}
|
|
128
|
+
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all text-gray-700 placeholder-gray-400"
|
|
129
|
+
placeholder="John Smith"
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Email Field */}
|
|
135
|
+
<div className="group">
|
|
136
|
+
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
|
137
|
+
Email Address
|
|
138
|
+
</label>
|
|
139
|
+
<div className="relative">
|
|
140
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
141
|
+
<EnvelopeIcon className="h-5 w-5 text-gray-400 group-focus-within:text-indigo-500 transition-colors" />
|
|
142
|
+
</div>
|
|
143
|
+
<input
|
|
144
|
+
type="email"
|
|
145
|
+
name="email"
|
|
146
|
+
value={formData.email}
|
|
147
|
+
onChange={handleChange}
|
|
148
|
+
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all text-gray-700 placeholder-gray-400"
|
|
149
|
+
placeholder="you@example.com"
|
|
150
|
+
required
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Password Field */}
|
|
156
|
+
<div className="group">
|
|
157
|
+
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
|
158
|
+
Password
|
|
159
|
+
</label>
|
|
160
|
+
<div className="relative">
|
|
161
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
162
|
+
<LockClosedIcon className="h-5 w-5 text-gray-400 group-focus-within:text-indigo-500 transition-colors" />
|
|
163
|
+
</div>
|
|
164
|
+
<input
|
|
165
|
+
type={showPassword ? 'text' : 'password'}
|
|
166
|
+
name="password"
|
|
167
|
+
value={formData.password}
|
|
168
|
+
onChange={handleChange}
|
|
169
|
+
className="w-full pl-10 pr-12 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all text-gray-700 placeholder-gray-400"
|
|
170
|
+
placeholder="Create a strong password"
|
|
171
|
+
required
|
|
172
|
+
/>
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
176
|
+
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-indigo-600 transition-colors"
|
|
177
|
+
>
|
|
178
|
+
{showPassword ? <EyeSlashIcon className="w-5 h-5" /> : <EyeIcon className="w-5 h-5" />}
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Password Strength Indicator */}
|
|
183
|
+
{formData.password && (
|
|
184
|
+
<div className="mt-2 space-y-1">
|
|
185
|
+
<div className="flex gap-1">
|
|
186
|
+
{[...Array(5)].map((_, i) => (
|
|
187
|
+
<div
|
|
188
|
+
key={i}
|
|
189
|
+
className={`h-1 flex-1 rounded-full transition-all duration-300 ${
|
|
190
|
+
i < passwordStrength
|
|
191
|
+
? passwordStrength <= 2
|
|
192
|
+
? 'bg-red-500'
|
|
193
|
+
: passwordStrength <= 3
|
|
194
|
+
? 'bg-yellow-500'
|
|
195
|
+
: 'bg-green-500'
|
|
196
|
+
: 'bg-gray-200'
|
|
197
|
+
}`}
|
|
198
|
+
/>
|
|
199
|
+
))}
|
|
200
|
+
</div>
|
|
201
|
+
<div className="flex justify-between items-center">
|
|
202
|
+
<p className={`text-xs font-medium ${getStrengthText().color}`}>
|
|
203
|
+
{getStrengthText().text} Password
|
|
204
|
+
</p>
|
|
205
|
+
<p className="text-xs text-gray-500">
|
|
206
|
+
{formData.password.length} characters
|
|
207
|
+
</p>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Confirm Password Field */}
|
|
214
|
+
<div className="group">
|
|
215
|
+
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
|
216
|
+
Confirm Password
|
|
217
|
+
</label>
|
|
218
|
+
<div className="relative">
|
|
219
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
220
|
+
<CheckBadgeIcon className="h-5 w-5 text-gray-400 group-focus-within:text-indigo-500 transition-colors" />
|
|
221
|
+
</div>
|
|
222
|
+
<input
|
|
223
|
+
type={showConfirmPassword ? 'text' : 'password'}
|
|
224
|
+
name="confirmPassword"
|
|
225
|
+
value={formData.confirmPassword}
|
|
226
|
+
onChange={handleChange}
|
|
227
|
+
className="w-full pl-10 pr-12 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all text-gray-700 placeholder-gray-400"
|
|
228
|
+
placeholder="Confirm your password"
|
|
229
|
+
required
|
|
230
|
+
/>
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
234
|
+
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-indigo-600 transition-colors"
|
|
235
|
+
>
|
|
236
|
+
{showConfirmPassword ? <EyeSlashIcon className="w-5 h-5" /> : <EyeIcon className="w-5 h-5" />}
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
{formData.confirmPassword && formData.password !== formData.confirmPassword && (
|
|
240
|
+
<p className="text-xs text-red-500 mt-1">Passwords do not match</p>
|
|
241
|
+
)}
|
|
242
|
+
{formData.confirmPassword && formData.password === formData.confirmPassword && formData.password && (
|
|
243
|
+
<p className="text-xs text-green-500 mt-1 flex items-center gap-1">
|
|
244
|
+
<CheckCircleIcon className="w-3 h-3" /> Passwords match
|
|
245
|
+
</p>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{/* Terms and Conditions */}
|
|
250
|
+
<div className="flex items-start gap-3">
|
|
251
|
+
<input
|
|
252
|
+
type="checkbox"
|
|
253
|
+
id="terms"
|
|
254
|
+
checked={agreeToTerms}
|
|
255
|
+
onChange={(e) => setAgreeToTerms(e.target.checked)}
|
|
256
|
+
className="mt-1 w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"
|
|
257
|
+
/>
|
|
258
|
+
<label htmlFor="terms" className="text-sm text-gray-600">
|
|
259
|
+
I agree to the{' '}
|
|
260
|
+
<Link href="/terms" className="text-indigo-600 hover:text-indigo-700 font-medium">
|
|
261
|
+
Terms of Service
|
|
262
|
+
</Link>{' '}
|
|
263
|
+
and{' '}
|
|
264
|
+
<Link href="/privacy" className="text-indigo-600 hover:text-indigo-700 font-medium">
|
|
265
|
+
Privacy Policy
|
|
266
|
+
</Link>
|
|
267
|
+
</label>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
{/* Submit Button */}
|
|
271
|
+
<button
|
|
272
|
+
type="submit"
|
|
273
|
+
disabled={loading}
|
|
274
|
+
className="group relative w-full py-3.5 bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 text-white rounded-xl font-semibold hover:from-indigo-700 hover:via-purple-700 hover:to-pink-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed overflow-hidden"
|
|
275
|
+
>
|
|
276
|
+
<span className="relative z-10 flex items-center justify-center gap-2">
|
|
277
|
+
{loading ? (
|
|
278
|
+
<>
|
|
279
|
+
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
|
|
280
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
|
281
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
282
|
+
</svg>
|
|
283
|
+
Creating account...
|
|
284
|
+
</>
|
|
285
|
+
) : (
|
|
286
|
+
<>
|
|
287
|
+
Create Account
|
|
288
|
+
<ArrowRightIcon className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
|
289
|
+
</>
|
|
290
|
+
)}
|
|
291
|
+
</span>
|
|
292
|
+
</button>
|
|
293
|
+
</form>
|
|
294
|
+
|
|
295
|
+
{/* Divider */}
|
|
296
|
+
<div className="relative my-6">
|
|
297
|
+
<div className="absolute inset-0 flex items-center">
|
|
298
|
+
<div className="w-full border-t border-gray-200"></div>
|
|
299
|
+
</div>
|
|
300
|
+
<div className="relative flex justify-center text-sm">
|
|
301
|
+
<span className="px-4 bg-white text-gray-500">Or</span>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
{/* Sign In Link */}
|
|
306
|
+
<div className="text-center">
|
|
307
|
+
<p className="text-sm text-gray-600">
|
|
308
|
+
Already have an account?{' '}
|
|
309
|
+
<Link
|
|
310
|
+
href="/login"
|
|
311
|
+
className="text-indigo-600 hover:text-indigo-700 font-semibold hover:underline transition-all"
|
|
312
|
+
>
|
|
313
|
+
Sign in
|
|
314
|
+
</Link>
|
|
315
|
+
</p>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<style jsx>{`
|
|
322
|
+
@keyframes blob {
|
|
323
|
+
0% { transform: translate(0px, 0px) scale(1); }
|
|
324
|
+
33% { transform: translate(30px, -50px) scale(1.1); }
|
|
325
|
+
66% { transform: translate(-20px, 20px) scale(0.9); }
|
|
326
|
+
100% { transform: translate(0px, 0px) scale(1); }
|
|
327
|
+
}
|
|
328
|
+
.animate-blob {
|
|
329
|
+
animation: blob 7s infinite;
|
|
330
|
+
}
|
|
331
|
+
.animation-delay-2000 {
|
|
332
|
+
animation-delay: 2s;
|
|
333
|
+
}
|
|
334
|
+
.animation-delay-4000 {
|
|
335
|
+
animation-delay: 4s;
|
|
336
|
+
}
|
|
337
|
+
`}</style>
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, Suspense } from 'react';
|
|
4
|
+
import { useSearchParams, useRouter } from 'next/navigation';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
|
7
|
+
|
|
8
|
+
function ResetPasswordForm() {
|
|
9
|
+
const searchParams = useSearchParams();
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
const token = searchParams.get('token');
|
|
12
|
+
|
|
13
|
+
const [password, setPassword] = useState('');
|
|
14
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
15
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
16
|
+
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
const [message, setMessage] = useState('');
|
|
19
|
+
const [error, setError] = useState('');
|
|
20
|
+
|
|
21
|
+
const handleSubmit = async (e) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setError('');
|
|
24
|
+
setMessage('');
|
|
25
|
+
|
|
26
|
+
if (password !== confirmPassword) {
|
|
27
|
+
setError('Passwords do not match');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (password.length < 6) {
|
|
32
|
+
setError('Password must be at least 6 characters');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setLoading(true);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch('/api/reset-password', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({ token, newPassword: password }),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const data = await response.json();
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(data.error || 'Failed to reset password');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setMessage(data.message);
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
router.push('/login');
|
|
54
|
+
}, 3000);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
setError(err.message);
|
|
57
|
+
} finally {
|
|
58
|
+
setLoading(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (!token) {
|
|
63
|
+
return (
|
|
64
|
+
<div className="text-center">
|
|
65
|
+
<div className="bg-red-50 border border-red-200 rounded-lg p-6">
|
|
66
|
+
<h3 className="text-lg font-semibold text-red-800 mb-2">Invalid Reset Link</h3>
|
|
67
|
+
<p className="text-red-600 mb-4">No reset token provided. Please request a new password reset.</p>
|
|
68
|
+
<Link
|
|
69
|
+
href="/forgot-password"
|
|
70
|
+
className="inline-block px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
|
71
|
+
>
|
|
72
|
+
Request New Reset Link
|
|
73
|
+
</Link>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
81
|
+
{error && (
|
|
82
|
+
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg text-sm">
|
|
83
|
+
{error}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
{message && (
|
|
88
|
+
<div className="bg-green-50 border border-green-200 text-green-600 px-4 py-3 rounded-lg text-sm">
|
|
89
|
+
{message}
|
|
90
|
+
<div className="mt-2 text-sm">Redirecting to login...</div>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<div>
|
|
95
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
96
|
+
New Password
|
|
97
|
+
</label>
|
|
98
|
+
<div className="relative">
|
|
99
|
+
<input
|
|
100
|
+
type={showPassword ? 'text' : 'password'}
|
|
101
|
+
value={password}
|
|
102
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
103
|
+
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all pr-12"
|
|
104
|
+
placeholder="Enter new password"
|
|
105
|
+
required
|
|
106
|
+
disabled={loading}
|
|
107
|
+
/>
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
111
|
+
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
|
112
|
+
>
|
|
113
|
+
{showPassword ? <EyeSlashIcon className="w-5 h-5" /> : <EyeIcon className="w-5 h-5" />}
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div>
|
|
119
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
120
|
+
Confirm New Password
|
|
121
|
+
</label>
|
|
122
|
+
<div className="relative">
|
|
123
|
+
<input
|
|
124
|
+
type={showConfirmPassword ? 'text' : 'password'}
|
|
125
|
+
value={confirmPassword}
|
|
126
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
127
|
+
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all pr-12"
|
|
128
|
+
placeholder="Confirm new password"
|
|
129
|
+
required
|
|
130
|
+
disabled={loading}
|
|
131
|
+
/>
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
135
|
+
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
|
136
|
+
>
|
|
137
|
+
{showConfirmPassword ? <EyeSlashIcon className="w-5 h-5" /> : <EyeIcon className="w-5 h-5" />}
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<button
|
|
143
|
+
type="submit"
|
|
144
|
+
disabled={loading}
|
|
145
|
+
className="w-full py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-lg font-medium hover:from-blue-700 hover:to-purple-700 transition-all duration-200 shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
146
|
+
>
|
|
147
|
+
{loading ? 'Resetting Password...' : 'Reset Password'}
|
|
148
|
+
</button>
|
|
149
|
+
</form>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export default function ResetPasswordPage() {
|
|
154
|
+
return (
|
|
155
|
+
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 flex items-center justify-center px-6">
|
|
156
|
+
<div className="max-w-md w-full">
|
|
157
|
+
<div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-8">
|
|
158
|
+
<div className="text-center mb-8">
|
|
159
|
+
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-blue-600 to-purple-600 rounded-2xl mb-4 shadow-lg">
|
|
160
|
+
<span className="text-white text-2xl">🔐</span>
|
|
161
|
+
</div>
|
|
162
|
+
<h2 className="text-3xl font-bold text-gray-900 mb-2">Set New Password</h2>
|
|
163
|
+
<p className="text-gray-600">Create a new secure password for your account</p>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<Suspense fallback={<div className="text-center">Loading...</div>}>
|
|
167
|
+
<ResetPasswordForm />
|
|
168
|
+
</Suspense>
|
|
169
|
+
|
|
170
|
+
<div className="mt-6 text-center">
|
|
171
|
+
<Link href="/login" className="text-sm text-blue-600 hover:text-blue-700">
|
|
172
|
+
← Back to Login
|
|
173
|
+
</Link>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
const AuthContext = createContext();
|
|
6
|
+
|
|
7
|
+
export const useAuth = () => {
|
|
8
|
+
const context = useContext(AuthContext);
|
|
9
|
+
if (!context) {
|
|
10
|
+
throw new Error('useAuth must be used within AuthProvider');
|
|
11
|
+
}
|
|
12
|
+
return context;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const AuthProvider = ({ children }) => {
|
|
16
|
+
const [user, setUser] = useState(null);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const token = localStorage.getItem('token');
|
|
21
|
+
const userData = localStorage.getItem('user');
|
|
22
|
+
|
|
23
|
+
if (token && userData) {
|
|
24
|
+
try {
|
|
25
|
+
setUser(JSON.parse(userData));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('Error parsing user data:', error);
|
|
28
|
+
localStorage.removeItem('user');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
setLoading(false);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const login = async (email, password) => {
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch('/api/login', {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ email, password }),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new Error(data.error || 'Login failed');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
localStorage.setItem('token', data.token);
|
|
49
|
+
localStorage.setItem('user', JSON.stringify(data.user));
|
|
50
|
+
setUser(data.user);
|
|
51
|
+
return data;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Login error:', error);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const register = async (name, email, password) => {
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch('/api/register', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ name, email, password }),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(data.error || 'Registration failed');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
localStorage.setItem('token', data.token);
|
|
73
|
+
localStorage.setItem('user', JSON.stringify(data.user));
|
|
74
|
+
setUser(data.user);
|
|
75
|
+
return data;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Registration error:', error);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const forgotPassword = async (email) => {
|
|
83
|
+
try {
|
|
84
|
+
console.log('Calling forgot password API for:', email);
|
|
85
|
+
|
|
86
|
+
const response = await fetch('/api/forgot-password', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({ email }),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
console.log('Response status:', response.status);
|
|
95
|
+
|
|
96
|
+
const data = await response.json();
|
|
97
|
+
console.log('Response data:', data);
|
|
98
|
+
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new Error(data.error || 'Request failed');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return data;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Forgot password error:', error);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const resetPassword = async (token, newPassword) => {
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch('/api/reset-password', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'Content-Type': 'application/json' },
|
|
115
|
+
body: JSON.stringify({ token, newPassword }),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const data = await response.json();
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error(data.error || 'Password reset failed');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return data;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error('Reset password error:', error);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const logout = () => {
|
|
132
|
+
localStorage.removeItem('token');
|
|
133
|
+
localStorage.removeItem('user');
|
|
134
|
+
setUser(null);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const value = {
|
|
138
|
+
user,
|
|
139
|
+
loading,
|
|
140
|
+
login,
|
|
141
|
+
register,
|
|
142
|
+
forgotPassword,
|
|
143
|
+
resetPassword,
|
|
144
|
+
logout,
|
|
145
|
+
isAuthenticated: !!user,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<AuthContext.Provider value={value}>
|
|
150
|
+
{children}
|
|
151
|
+
</AuthContext.Provider>
|
|
152
|
+
);
|
|
153
|
+
};
|