@checkstack/auth-frontend 0.0.2
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/CHANGELOG.md +207 -0
- package/e2e/login.e2e.ts +63 -0
- package/package.json +34 -0
- package/playwright-report/data/774b616fd991c36e57f6aa95d67906b877dff5d1.md +20 -0
- package/playwright-report/data/d37ef869a8ef03c489f7ca3b80d67da69614c383.png +3 -0
- package/playwright-report/index.html +85 -0
- package/playwright.config.ts +5 -0
- package/src/api.ts +78 -0
- package/src/components/ApplicationsTab.tsx +452 -0
- package/src/components/AuthErrorPage.tsx +94 -0
- package/src/components/AuthSettingsPage.tsx +249 -0
- package/src/components/AuthStrategyCard.tsx +77 -0
- package/src/components/ChangePasswordPage.tsx +259 -0
- package/src/components/CreateUserDialog.tsx +156 -0
- package/src/components/ForgotPasswordPage.tsx +131 -0
- package/src/components/LoginPage.tsx +330 -0
- package/src/components/RegisterPage.tsx +350 -0
- package/src/components/ResetPasswordPage.tsx +262 -0
- package/src/components/RoleDialog.tsx +284 -0
- package/src/components/RolesTab.tsx +219 -0
- package/src/components/SocialProviderButton.tsx +30 -0
- package/src/components/StrategiesTab.tsx +276 -0
- package/src/components/UsersTab.tsx +234 -0
- package/src/hooks/useEnabledStrategies.ts +54 -0
- package/src/hooks/usePermissions.ts +43 -0
- package/src/index.test.tsx +95 -0
- package/src/index.tsx +271 -0
- package/src/lib/auth-client.ts +55 -0
- package/test-results/login-Login-Page-should-show-login-form-elements-chromium/test-failed-1.png +3 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Link, useNavigate } from "react-router-dom";
|
|
3
|
+
import { AlertCircle } from "lucide-react";
|
|
4
|
+
import { useApi, rpcApiRef } from "@checkstack/frontend-api";
|
|
5
|
+
import { authApiRef } from "../api";
|
|
6
|
+
import {
|
|
7
|
+
AuthApi,
|
|
8
|
+
authRoutes,
|
|
9
|
+
passwordSchema,
|
|
10
|
+
} from "@checkstack/auth-common";
|
|
11
|
+
import { resolveRoute } from "@checkstack/common";
|
|
12
|
+
import {
|
|
13
|
+
Button,
|
|
14
|
+
Input,
|
|
15
|
+
Label,
|
|
16
|
+
Card,
|
|
17
|
+
CardHeader,
|
|
18
|
+
CardTitle,
|
|
19
|
+
CardDescription,
|
|
20
|
+
CardContent,
|
|
21
|
+
CardFooter,
|
|
22
|
+
InfoBanner,
|
|
23
|
+
InfoBannerIcon,
|
|
24
|
+
InfoBannerContent,
|
|
25
|
+
InfoBannerTitle,
|
|
26
|
+
InfoBannerDescription,
|
|
27
|
+
} from "@checkstack/ui";
|
|
28
|
+
import { useEnabledStrategies } from "../hooks/useEnabledStrategies";
|
|
29
|
+
import { SocialProviderButton } from "./SocialProviderButton";
|
|
30
|
+
import { useAuthClient } from "../lib/auth-client";
|
|
31
|
+
|
|
32
|
+
export const RegisterPage = () => {
|
|
33
|
+
const [name, setName] = useState("");
|
|
34
|
+
const [email, setEmail] = useState("");
|
|
35
|
+
const [password, setPassword] = useState("");
|
|
36
|
+
const [loading, setLoading] = useState(false);
|
|
37
|
+
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
|
38
|
+
const navigate = useNavigate();
|
|
39
|
+
const authApi = useApi(authApiRef);
|
|
40
|
+
const rpcApi = useApi(rpcApiRef);
|
|
41
|
+
const authRpcClient = rpcApi.forPlugin(AuthApi);
|
|
42
|
+
const { strategies, loading: strategiesLoading } = useEnabledStrategies();
|
|
43
|
+
const [registrationAllowed, setRegistrationAllowed] = useState<boolean>(true);
|
|
44
|
+
const [checkingRegistration, setCheckingRegistration] = useState(true);
|
|
45
|
+
const authClient = useAuthClient();
|
|
46
|
+
|
|
47
|
+
// Validate password on change
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (password) {
|
|
50
|
+
const result = passwordSchema.safeParse(password);
|
|
51
|
+
if (result.success) {
|
|
52
|
+
setValidationErrors([]);
|
|
53
|
+
} else {
|
|
54
|
+
setValidationErrors(result.error.issues.map((issue) => issue.message));
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
setValidationErrors([]);
|
|
58
|
+
}
|
|
59
|
+
}, [password]);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
authRpcClient
|
|
63
|
+
.getRegistrationStatus()
|
|
64
|
+
.then(({ allowRegistration }) => {
|
|
65
|
+
setRegistrationAllowed(allowRegistration);
|
|
66
|
+
})
|
|
67
|
+
.catch((error: Error) => {
|
|
68
|
+
console.error("Failed to check registration status:", error);
|
|
69
|
+
// Default to allowed on error to avoid blocking
|
|
70
|
+
setRegistrationAllowed(true);
|
|
71
|
+
})
|
|
72
|
+
.finally(() => setCheckingRegistration(false));
|
|
73
|
+
}, [authRpcClient]);
|
|
74
|
+
|
|
75
|
+
const handleCredentialRegister = async (e: React.FormEvent) => {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
|
|
78
|
+
// Validate password before submitting
|
|
79
|
+
const result = passwordSchema.safeParse(password);
|
|
80
|
+
if (!result.success) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setLoading(true);
|
|
85
|
+
try {
|
|
86
|
+
const res = await authClient.signUp.email({ name, email, password });
|
|
87
|
+
if (res.error) {
|
|
88
|
+
console.error("Registration failed:", res.error);
|
|
89
|
+
} else {
|
|
90
|
+
navigate("/");
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error("Registration failed:", error);
|
|
94
|
+
} finally {
|
|
95
|
+
setLoading(false);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleSocialRegister = async (provider: string) => {
|
|
100
|
+
try {
|
|
101
|
+
await authApi.signInWithSocial(provider);
|
|
102
|
+
// Navigation will happen automatically after OAuth redirect
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error("Social registration failed:", error);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const credentialStrategy = strategies.find((s) => s.type === "credential");
|
|
109
|
+
const socialStrategies = strategies.filter((s) => s.type === "social");
|
|
110
|
+
const hasCredential = !!credentialStrategy;
|
|
111
|
+
const hasSocial = socialStrategies.length > 0;
|
|
112
|
+
|
|
113
|
+
// Loading state
|
|
114
|
+
if (strategiesLoading || checkingRegistration) {
|
|
115
|
+
return (
|
|
116
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
117
|
+
<Card className="w-full max-w-md">
|
|
118
|
+
<CardContent className="pt-6">
|
|
119
|
+
<div className="space-y-4">
|
|
120
|
+
<div className="h-4 bg-muted animate-pulse rounded" />
|
|
121
|
+
<div className="h-10 bg-muted animate-pulse rounded" />
|
|
122
|
+
<div className="h-10 bg-muted animate-pulse rounded" />
|
|
123
|
+
<div className="h-10 bg-muted animate-pulse rounded" />
|
|
124
|
+
</div>
|
|
125
|
+
</CardContent>
|
|
126
|
+
</Card>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Registration is disabled
|
|
132
|
+
if (!registrationAllowed) {
|
|
133
|
+
return (
|
|
134
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
135
|
+
<Card className="w-full max-w-md">
|
|
136
|
+
<CardHeader className="flex flex-col space-y-1 items-center">
|
|
137
|
+
<CardTitle>Registration Disabled</CardTitle>
|
|
138
|
+
</CardHeader>
|
|
139
|
+
<CardContent>
|
|
140
|
+
<InfoBanner variant="warning">
|
|
141
|
+
<InfoBannerIcon>
|
|
142
|
+
<AlertCircle className="h-4 w-4" />
|
|
143
|
+
</InfoBannerIcon>
|
|
144
|
+
<InfoBannerContent>
|
|
145
|
+
<InfoBannerTitle>
|
|
146
|
+
Registration is Currently Disabled
|
|
147
|
+
</InfoBannerTitle>
|
|
148
|
+
<InfoBannerDescription>
|
|
149
|
+
New user registration has been disabled by the system
|
|
150
|
+
administrator. If you already have an account, please{" "}
|
|
151
|
+
<Link
|
|
152
|
+
to={resolveRoute(authRoutes.routes.login)}
|
|
153
|
+
className="underline text-primary hover:text-primary/90 font-medium"
|
|
154
|
+
>
|
|
155
|
+
sign in
|
|
156
|
+
</Link>
|
|
157
|
+
. Otherwise, please contact your administrator for assistance.
|
|
158
|
+
</InfoBannerDescription>
|
|
159
|
+
</InfoBannerContent>
|
|
160
|
+
</InfoBanner>
|
|
161
|
+
</CardContent>
|
|
162
|
+
</Card>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// No strategies enabled
|
|
168
|
+
if (strategies.length === 0) {
|
|
169
|
+
return (
|
|
170
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
171
|
+
<Card className="w-full max-w-md">
|
|
172
|
+
<CardHeader className="flex flex-col space-y-1 items-center">
|
|
173
|
+
<CardTitle>Registration Unavailable</CardTitle>
|
|
174
|
+
</CardHeader>
|
|
175
|
+
<CardContent>
|
|
176
|
+
<InfoBanner variant="warning">
|
|
177
|
+
<InfoBannerIcon>
|
|
178
|
+
<AlertCircle className="h-4 w-4" />
|
|
179
|
+
</InfoBannerIcon>
|
|
180
|
+
<InfoBannerContent>
|
|
181
|
+
<InfoBannerTitle>
|
|
182
|
+
No authentication methods enabled
|
|
183
|
+
</InfoBannerTitle>
|
|
184
|
+
<InfoBannerDescription>
|
|
185
|
+
Please contact your system administrator to enable
|
|
186
|
+
authentication methods.
|
|
187
|
+
</InfoBannerDescription>
|
|
188
|
+
</InfoBannerContent>
|
|
189
|
+
</InfoBanner>
|
|
190
|
+
</CardContent>
|
|
191
|
+
</Card>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check if any strategy requires manual registration
|
|
197
|
+
const requiresRegistration = strategies.some(
|
|
198
|
+
(s) => s.requiresManualRegistration
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// If no strategy requires manual registration, inform the user
|
|
202
|
+
if (!requiresRegistration) {
|
|
203
|
+
return (
|
|
204
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
205
|
+
<Card className="w-full max-w-md">
|
|
206
|
+
<CardHeader className="flex flex-col space-y-1 items-center">
|
|
207
|
+
<CardTitle>Registration Not Required</CardTitle>
|
|
208
|
+
</CardHeader>
|
|
209
|
+
<CardContent>
|
|
210
|
+
<InfoBanner>
|
|
211
|
+
<InfoBannerIcon>
|
|
212
|
+
<AlertCircle className="h-4 w-4" />
|
|
213
|
+
</InfoBannerIcon>
|
|
214
|
+
<InfoBannerContent>
|
|
215
|
+
<InfoBannerTitle>Automatic Account Creation</InfoBannerTitle>
|
|
216
|
+
<InfoBannerDescription>
|
|
217
|
+
Accounts are automatically created when you sign in with one
|
|
218
|
+
of the available authentication methods. Please proceed to the{" "}
|
|
219
|
+
<Link
|
|
220
|
+
to="/auth/login"
|
|
221
|
+
className="underline text-primary hover:text-primary/90 font-medium"
|
|
222
|
+
>
|
|
223
|
+
login page
|
|
224
|
+
</Link>
|
|
225
|
+
.
|
|
226
|
+
</InfoBannerDescription>
|
|
227
|
+
</InfoBannerContent>
|
|
228
|
+
</InfoBanner>
|
|
229
|
+
</CardContent>
|
|
230
|
+
</Card>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
237
|
+
<Card className="w-full max-w-md">
|
|
238
|
+
<CardHeader className="flex flex-col space-y-1 items-center">
|
|
239
|
+
<CardTitle>Create your account</CardTitle>
|
|
240
|
+
<CardDescription>
|
|
241
|
+
{hasCredential && hasSocial
|
|
242
|
+
? "Choose your preferred registration method"
|
|
243
|
+
: hasCredential
|
|
244
|
+
? "Enter your details to get started"
|
|
245
|
+
: "Continue to create your account"}
|
|
246
|
+
</CardDescription>
|
|
247
|
+
</CardHeader>
|
|
248
|
+
<CardContent>
|
|
249
|
+
<div className="space-y-4">
|
|
250
|
+
{/* Credential Registration Form */}
|
|
251
|
+
{hasCredential && (
|
|
252
|
+
<form className="space-y-4" onSubmit={handleCredentialRegister}>
|
|
253
|
+
<div className="space-y-2">
|
|
254
|
+
<Label htmlFor="name">Name</Label>
|
|
255
|
+
<Input
|
|
256
|
+
id="name"
|
|
257
|
+
placeholder="John Doe"
|
|
258
|
+
type="text"
|
|
259
|
+
required
|
|
260
|
+
value={name}
|
|
261
|
+
onChange={(e) => setName(e.target.value)}
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
<div className="space-y-2">
|
|
265
|
+
<Label htmlFor="email">Email</Label>
|
|
266
|
+
<Input
|
|
267
|
+
id="email"
|
|
268
|
+
placeholder="name@example.com"
|
|
269
|
+
type="email"
|
|
270
|
+
required
|
|
271
|
+
value={email}
|
|
272
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
<div className="space-y-2">
|
|
276
|
+
<Label htmlFor="password">Password</Label>
|
|
277
|
+
<Input
|
|
278
|
+
id="password"
|
|
279
|
+
required
|
|
280
|
+
type="password"
|
|
281
|
+
value={password}
|
|
282
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
283
|
+
/>
|
|
284
|
+
{validationErrors.length > 0 && (
|
|
285
|
+
<ul className="text-sm text-muted-foreground list-disc pl-5 space-y-1">
|
|
286
|
+
{validationErrors.map((validationError, i) => (
|
|
287
|
+
<li key={i} className="text-destructive">
|
|
288
|
+
{validationError}
|
|
289
|
+
</li>
|
|
290
|
+
))}
|
|
291
|
+
</ul>
|
|
292
|
+
)}
|
|
293
|
+
<p className="text-xs text-muted-foreground">
|
|
294
|
+
At least 8 characters with uppercase, lowercase, and number
|
|
295
|
+
</p>
|
|
296
|
+
</div>
|
|
297
|
+
<Button
|
|
298
|
+
type="submit"
|
|
299
|
+
className="w-full"
|
|
300
|
+
disabled={loading || validationErrors.length > 0}
|
|
301
|
+
>
|
|
302
|
+
{loading ? "Creating Account..." : "Create Account"}
|
|
303
|
+
</Button>
|
|
304
|
+
</form>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{/* Separator */}
|
|
308
|
+
{hasCredential && hasSocial && (
|
|
309
|
+
<div className="relative">
|
|
310
|
+
<div className="absolute inset-0 flex items-center">
|
|
311
|
+
<span className="w-full border-t border-border" />
|
|
312
|
+
</div>
|
|
313
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
314
|
+
<span className="bg-card px-2 text-muted-foreground">
|
|
315
|
+
Or continue with
|
|
316
|
+
</span>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
|
|
321
|
+
{/* Social Provider Buttons */}
|
|
322
|
+
{hasSocial && (
|
|
323
|
+
<div className="space-y-2">
|
|
324
|
+
{socialStrategies.map((strategy) => (
|
|
325
|
+
<SocialProviderButton
|
|
326
|
+
key={strategy.id}
|
|
327
|
+
displayName={strategy.displayName}
|
|
328
|
+
icon={strategy.icon}
|
|
329
|
+
onClick={() => handleSocialRegister(strategy.id)}
|
|
330
|
+
/>
|
|
331
|
+
))}
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
</CardContent>
|
|
336
|
+
<CardFooter className="flex justify-center border-t border-border mt-4 pt-4">
|
|
337
|
+
<div className="text-sm">
|
|
338
|
+
Already have an account?{" "}
|
|
339
|
+
<Link
|
|
340
|
+
to={resolveRoute(authRoutes.routes.login)}
|
|
341
|
+
className="underline text-primary hover:text-primary/90 font-medium"
|
|
342
|
+
>
|
|
343
|
+
Sign in
|
|
344
|
+
</Link>
|
|
345
|
+
</div>
|
|
346
|
+
</CardFooter>
|
|
347
|
+
</Card>
|
|
348
|
+
</div>
|
|
349
|
+
);
|
|
350
|
+
};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Link, useSearchParams, useNavigate } from "react-router-dom";
|
|
3
|
+
import { Lock, ArrowLeft, CheckCircle, AlertCircle } from "lucide-react";
|
|
4
|
+
import { authRoutes, passwordSchema } from "@checkstack/auth-common";
|
|
5
|
+
import { resolveRoute } from "@checkstack/common";
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
Input,
|
|
9
|
+
Label,
|
|
10
|
+
Card,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
CardDescription,
|
|
14
|
+
CardContent,
|
|
15
|
+
CardFooter,
|
|
16
|
+
Alert,
|
|
17
|
+
AlertIcon,
|
|
18
|
+
AlertContent,
|
|
19
|
+
AlertTitle,
|
|
20
|
+
AlertDescription,
|
|
21
|
+
} from "@checkstack/ui";
|
|
22
|
+
import { useAuthClient } from "../lib/auth-client";
|
|
23
|
+
|
|
24
|
+
export const ResetPasswordPage = () => {
|
|
25
|
+
const [searchParams] = useSearchParams();
|
|
26
|
+
const navigate = useNavigate();
|
|
27
|
+
const token = searchParams.get("token");
|
|
28
|
+
|
|
29
|
+
const [password, setPassword] = useState("");
|
|
30
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
31
|
+
const [loading, setLoading] = useState(false);
|
|
32
|
+
const [error, setError] = useState<string>();
|
|
33
|
+
const [success, setSuccess] = useState(false);
|
|
34
|
+
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
|
35
|
+
const authClient = useAuthClient();
|
|
36
|
+
|
|
37
|
+
// Validate password on change
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (password) {
|
|
40
|
+
const result = passwordSchema.safeParse(password);
|
|
41
|
+
if (result.success) {
|
|
42
|
+
setValidationErrors([]);
|
|
43
|
+
} else {
|
|
44
|
+
setValidationErrors(result.error.issues.map((issue) => issue.message));
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
setValidationErrors([]);
|
|
48
|
+
}
|
|
49
|
+
}, [password]);
|
|
50
|
+
|
|
51
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
setError(undefined);
|
|
54
|
+
|
|
55
|
+
// Frontend validation
|
|
56
|
+
if (password !== confirmPassword) {
|
|
57
|
+
setError("Passwords do not match");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = passwordSchema.safeParse(password);
|
|
62
|
+
if (!result.success) {
|
|
63
|
+
setError(result.error.issues[0].message);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!token) {
|
|
68
|
+
setError("Invalid or missing reset token");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setLoading(true);
|
|
73
|
+
try {
|
|
74
|
+
const response = await authClient.resetPassword({
|
|
75
|
+
newPassword: password,
|
|
76
|
+
token,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (response.error) {
|
|
80
|
+
setError(response.error.message ?? "Failed to reset password");
|
|
81
|
+
} else {
|
|
82
|
+
setSuccess(true);
|
|
83
|
+
}
|
|
84
|
+
} catch (error_) {
|
|
85
|
+
setError(
|
|
86
|
+
error_ instanceof Error ? error_.message : "Failed to reset password"
|
|
87
|
+
);
|
|
88
|
+
} finally {
|
|
89
|
+
setLoading(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// No token - show error
|
|
94
|
+
if (!token) {
|
|
95
|
+
return (
|
|
96
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
97
|
+
<Card className="w-full max-w-md">
|
|
98
|
+
<CardHeader className="space-y-1 text-center">
|
|
99
|
+
<div className="mx-auto w-12 h-12 rounded-full bg-destructive/10 flex items-center justify-center mb-2">
|
|
100
|
+
<AlertCircle className="h-6 w-6 text-destructive" />
|
|
101
|
+
</div>
|
|
102
|
+
<CardTitle className="text-2xl font-bold">Invalid Link</CardTitle>
|
|
103
|
+
<CardDescription>
|
|
104
|
+
This password reset link is invalid or has expired. Please request
|
|
105
|
+
a new one.
|
|
106
|
+
</CardDescription>
|
|
107
|
+
</CardHeader>
|
|
108
|
+
<CardFooter className="flex flex-col gap-4">
|
|
109
|
+
<Button
|
|
110
|
+
variant="primary"
|
|
111
|
+
className="w-full"
|
|
112
|
+
onClick={() =>
|
|
113
|
+
navigate(resolveRoute(authRoutes.routes.forgotPassword))
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
Request New Link
|
|
117
|
+
</Button>
|
|
118
|
+
<Link
|
|
119
|
+
to={resolveRoute(authRoutes.routes.login)}
|
|
120
|
+
className="text-sm text-primary hover:underline flex items-center justify-center gap-1"
|
|
121
|
+
>
|
|
122
|
+
<ArrowLeft className="h-4 w-4" />
|
|
123
|
+
Back to Login
|
|
124
|
+
</Link>
|
|
125
|
+
</CardFooter>
|
|
126
|
+
</Card>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Success state
|
|
132
|
+
if (success) {
|
|
133
|
+
return (
|
|
134
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
135
|
+
<Card className="w-full max-w-md">
|
|
136
|
+
<CardHeader className="space-y-1 text-center">
|
|
137
|
+
<div className="mx-auto w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mb-2">
|
|
138
|
+
<CheckCircle className="h-6 w-6 text-primary" />
|
|
139
|
+
</div>
|
|
140
|
+
<CardTitle className="text-2xl font-bold">
|
|
141
|
+
Password Reset Successfully
|
|
142
|
+
</CardTitle>
|
|
143
|
+
<CardDescription>
|
|
144
|
+
Your password has been reset. You can now log in with your new
|
|
145
|
+
password.
|
|
146
|
+
</CardDescription>
|
|
147
|
+
</CardHeader>
|
|
148
|
+
<CardFooter>
|
|
149
|
+
<Button
|
|
150
|
+
className="w-full"
|
|
151
|
+
onClick={() => navigate(resolveRoute(authRoutes.routes.login))}
|
|
152
|
+
>
|
|
153
|
+
Go to Login
|
|
154
|
+
</Button>
|
|
155
|
+
</CardFooter>
|
|
156
|
+
</Card>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
163
|
+
<Card className="w-full max-w-md">
|
|
164
|
+
<CardHeader className="space-y-1">
|
|
165
|
+
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
|
|
166
|
+
<CardDescription>Enter your new password below.</CardDescription>
|
|
167
|
+
</CardHeader>
|
|
168
|
+
<form onSubmit={handleSubmit}>
|
|
169
|
+
<CardContent className="space-y-4">
|
|
170
|
+
{error && (
|
|
171
|
+
<Alert variant="error">
|
|
172
|
+
<AlertIcon>
|
|
173
|
+
<AlertCircle className="h-4 w-4" />
|
|
174
|
+
</AlertIcon>
|
|
175
|
+
<AlertContent>
|
|
176
|
+
<AlertTitle>Error</AlertTitle>
|
|
177
|
+
<AlertDescription>{error}</AlertDescription>
|
|
178
|
+
</AlertContent>
|
|
179
|
+
</Alert>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
<div className="space-y-2">
|
|
183
|
+
<Label htmlFor="password">New Password</Label>
|
|
184
|
+
<div className="relative">
|
|
185
|
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
186
|
+
<Input
|
|
187
|
+
id="password"
|
|
188
|
+
type="password"
|
|
189
|
+
placeholder="Enter new password"
|
|
190
|
+
value={password}
|
|
191
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
192
|
+
className="pl-10"
|
|
193
|
+
required
|
|
194
|
+
autoFocus
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
{validationErrors.length > 0 && (
|
|
198
|
+
<ul className="text-sm text-muted-foreground list-disc pl-5 space-y-1">
|
|
199
|
+
{validationErrors.map((validationError, i) => (
|
|
200
|
+
<li key={i} className="text-destructive">
|
|
201
|
+
{validationError}
|
|
202
|
+
</li>
|
|
203
|
+
))}
|
|
204
|
+
</ul>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div className="space-y-2">
|
|
209
|
+
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
|
210
|
+
<div className="relative">
|
|
211
|
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
212
|
+
<Input
|
|
213
|
+
id="confirmPassword"
|
|
214
|
+
type="password"
|
|
215
|
+
placeholder="Confirm new password"
|
|
216
|
+
value={confirmPassword}
|
|
217
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
218
|
+
className="pl-10"
|
|
219
|
+
required
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
{confirmPassword && password !== confirmPassword && (
|
|
223
|
+
<p className="text-sm text-destructive">
|
|
224
|
+
Passwords do not match
|
|
225
|
+
</p>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<div className="text-xs text-muted-foreground">
|
|
230
|
+
Password must be at least 8 characters and contain:
|
|
231
|
+
<ul className="list-disc pl-5 mt-1">
|
|
232
|
+
<li>At least one uppercase letter</li>
|
|
233
|
+
<li>At least one lowercase letter</li>
|
|
234
|
+
<li>At least one number</li>
|
|
235
|
+
</ul>
|
|
236
|
+
</div>
|
|
237
|
+
</CardContent>
|
|
238
|
+
<CardFooter className="flex flex-col gap-4">
|
|
239
|
+
<Button
|
|
240
|
+
type="submit"
|
|
241
|
+
className="w-full"
|
|
242
|
+
disabled={
|
|
243
|
+
loading ||
|
|
244
|
+
validationErrors.length > 0 ||
|
|
245
|
+
password !== confirmPassword
|
|
246
|
+
}
|
|
247
|
+
>
|
|
248
|
+
{loading ? "Resetting..." : "Reset Password"}
|
|
249
|
+
</Button>
|
|
250
|
+
<Link
|
|
251
|
+
to={resolveRoute(authRoutes.routes.login)}
|
|
252
|
+
className="text-sm text-primary hover:underline flex items-center justify-center gap-1"
|
|
253
|
+
>
|
|
254
|
+
<ArrowLeft className="h-4 w-4" />
|
|
255
|
+
Back to Login
|
|
256
|
+
</Link>
|
|
257
|
+
</CardFooter>
|
|
258
|
+
</form>
|
|
259
|
+
</Card>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
};
|