@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.
@@ -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
+ };