@checkstack/auth-frontend 0.3.1 → 0.4.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/CHANGELOG.md +11 -0
- package/package.json +1 -1
- package/src/components/OnboardingCheck.tsx +33 -0
- package/src/components/OnboardingPage.tsx +291 -0
- package/src/components/ProfilePage.tsx +223 -0
- package/src/index.tsx +23 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @checkstack/auth-frontend
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- df6ac7b: Added onboarding flow and user profile
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [df6ac7b]
|
|
12
|
+
- @checkstack/auth-common@0.4.0
|
|
13
|
+
|
|
3
14
|
## 0.3.1
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AuthApi } from "@checkstack/auth-common";
|
|
2
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { useLocation, useNavigate } from "react-router-dom";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Onboarding guard that redirects to onboarding page if no users exist.
|
|
8
|
+
* Skips check if already on onboarding page.
|
|
9
|
+
*/
|
|
10
|
+
export function OnboardingCheck() {
|
|
11
|
+
const navigate = useNavigate();
|
|
12
|
+
const location = useLocation();
|
|
13
|
+
const authApi = usePluginClient(AuthApi);
|
|
14
|
+
|
|
15
|
+
const { data, isLoading } = authApi.getOnboardingStatus.useQuery();
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (isLoading) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Skip check if already on onboarding page
|
|
23
|
+
if (location.pathname === "/auth/onboarding") {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (data?.needsOnboarding) {
|
|
28
|
+
navigate("/auth/onboarding", { replace: true });
|
|
29
|
+
}
|
|
30
|
+
}, [isLoading, data, location.pathname, navigate]);
|
|
31
|
+
|
|
32
|
+
return <></>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { User, Lock, Mail, CheckCircle, AlertCircle } from "lucide-react";
|
|
4
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
5
|
+
import { AuthApi, authRoutes, passwordSchema } from "@checkstack/auth-common";
|
|
6
|
+
import { resolveRoute } from "@checkstack/common";
|
|
7
|
+
import {
|
|
8
|
+
Button,
|
|
9
|
+
Input,
|
|
10
|
+
Label,
|
|
11
|
+
Card,
|
|
12
|
+
CardHeader,
|
|
13
|
+
CardTitle,
|
|
14
|
+
CardDescription,
|
|
15
|
+
CardContent,
|
|
16
|
+
CardFooter,
|
|
17
|
+
Alert,
|
|
18
|
+
AlertIcon,
|
|
19
|
+
AlertContent,
|
|
20
|
+
AlertTitle,
|
|
21
|
+
AlertDescription,
|
|
22
|
+
} from "@checkstack/ui";
|
|
23
|
+
import { useAuthClient } from "../lib/auth-client";
|
|
24
|
+
|
|
25
|
+
export const OnboardingPage = () => {
|
|
26
|
+
const navigate = useNavigate();
|
|
27
|
+
const [name, setName] = useState("");
|
|
28
|
+
const [email, setEmail] = useState("");
|
|
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
|
+
|
|
36
|
+
const authClient = usePluginClient(AuthApi);
|
|
37
|
+
const completeOnboardingMutation =
|
|
38
|
+
authClient.completeOnboarding.useMutation();
|
|
39
|
+
const betterAuthClient = useAuthClient();
|
|
40
|
+
|
|
41
|
+
// Check if onboarding is needed
|
|
42
|
+
const { data: onboardingStatus, isLoading: checkingStatus } =
|
|
43
|
+
authClient.getOnboardingStatus.useQuery({});
|
|
44
|
+
|
|
45
|
+
// Redirect if onboarding not needed
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (
|
|
48
|
+
!checkingStatus &&
|
|
49
|
+
onboardingStatus &&
|
|
50
|
+
!onboardingStatus.needsOnboarding
|
|
51
|
+
) {
|
|
52
|
+
navigate(resolveRoute(authRoutes.routes.login));
|
|
53
|
+
}
|
|
54
|
+
}, [checkingStatus, onboardingStatus, navigate]);
|
|
55
|
+
|
|
56
|
+
// Validate password on change
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (password) {
|
|
59
|
+
const result = passwordSchema.safeParse(password);
|
|
60
|
+
if (result.success) {
|
|
61
|
+
setValidationErrors([]);
|
|
62
|
+
} else {
|
|
63
|
+
setValidationErrors(result.error.issues.map((issue) => issue.message));
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
setValidationErrors([]);
|
|
67
|
+
}
|
|
68
|
+
}, [password]);
|
|
69
|
+
|
|
70
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
setError(undefined);
|
|
73
|
+
|
|
74
|
+
// Validate password match
|
|
75
|
+
if (password !== confirmPassword) {
|
|
76
|
+
setError("Passwords do not match");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Validate password strength
|
|
81
|
+
const result = passwordSchema.safeParse(password);
|
|
82
|
+
if (!result.success) {
|
|
83
|
+
setError(result.error.issues[0].message);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setLoading(true);
|
|
88
|
+
try {
|
|
89
|
+
const response = await completeOnboardingMutation.mutateAsync({
|
|
90
|
+
name,
|
|
91
|
+
email,
|
|
92
|
+
password,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (response.success) {
|
|
96
|
+
// Auto-login the user
|
|
97
|
+
const loginRes = await betterAuthClient.signIn.email({
|
|
98
|
+
email,
|
|
99
|
+
password,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (loginRes.error) {
|
|
103
|
+
setError("Account created but login failed. Please login manually.");
|
|
104
|
+
} else {
|
|
105
|
+
setSuccess(true);
|
|
106
|
+
// Redirect to dashboard
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
globalThis.location.href = "/";
|
|
109
|
+
}, 1500);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (error_) {
|
|
113
|
+
const message =
|
|
114
|
+
error_ instanceof Error ? error_.message : "Failed to complete setup";
|
|
115
|
+
setError(message);
|
|
116
|
+
} finally {
|
|
117
|
+
setLoading(false);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Loading state
|
|
122
|
+
if (checkingStatus) {
|
|
123
|
+
return (
|
|
124
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
125
|
+
<Card className="w-full max-w-md">
|
|
126
|
+
<CardContent className="pt-6">
|
|
127
|
+
<div className="space-y-4">
|
|
128
|
+
<div className="h-4 bg-muted animate-pulse rounded" />
|
|
129
|
+
<div className="h-10 bg-muted animate-pulse rounded" />
|
|
130
|
+
<div className="h-10 bg-muted animate-pulse rounded" />
|
|
131
|
+
</div>
|
|
132
|
+
</CardContent>
|
|
133
|
+
</Card>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Success state
|
|
139
|
+
if (success) {
|
|
140
|
+
return (
|
|
141
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
142
|
+
<Card className="w-full max-w-md">
|
|
143
|
+
<CardHeader className="space-y-1 text-center">
|
|
144
|
+
<div className="mx-auto w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mb-2">
|
|
145
|
+
<CheckCircle className="h-6 w-6 text-primary" />
|
|
146
|
+
</div>
|
|
147
|
+
<CardTitle className="text-2xl font-bold">
|
|
148
|
+
Setup Complete!
|
|
149
|
+
</CardTitle>
|
|
150
|
+
<CardDescription>
|
|
151
|
+
Your admin account has been created. Redirecting to dashboard...
|
|
152
|
+
</CardDescription>
|
|
153
|
+
</CardHeader>
|
|
154
|
+
</Card>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
161
|
+
<Card className="w-full max-w-md">
|
|
162
|
+
<CardHeader className="space-y-1 text-center">
|
|
163
|
+
<CardTitle className="text-2xl font-bold">
|
|
164
|
+
Welcome to Checkstack
|
|
165
|
+
</CardTitle>
|
|
166
|
+
<CardDescription>
|
|
167
|
+
Create your administrator account to get started
|
|
168
|
+
</CardDescription>
|
|
169
|
+
</CardHeader>
|
|
170
|
+
<form onSubmit={handleSubmit}>
|
|
171
|
+
<CardContent className="space-y-4">
|
|
172
|
+
{error && (
|
|
173
|
+
<Alert variant="error">
|
|
174
|
+
<AlertIcon>
|
|
175
|
+
<AlertCircle className="h-4 w-4" />
|
|
176
|
+
</AlertIcon>
|
|
177
|
+
<AlertContent>
|
|
178
|
+
<AlertTitle>Error</AlertTitle>
|
|
179
|
+
<AlertDescription>{error}</AlertDescription>
|
|
180
|
+
</AlertContent>
|
|
181
|
+
</Alert>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
<div className="space-y-2">
|
|
185
|
+
<Label htmlFor="name">Name</Label>
|
|
186
|
+
<div className="relative">
|
|
187
|
+
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
188
|
+
<Input
|
|
189
|
+
id="name"
|
|
190
|
+
type="text"
|
|
191
|
+
placeholder="Your name"
|
|
192
|
+
value={name}
|
|
193
|
+
onChange={(e) => setName(e.target.value)}
|
|
194
|
+
className="pl-10"
|
|
195
|
+
required
|
|
196
|
+
autoFocus
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div className="space-y-2">
|
|
202
|
+
<Label htmlFor="email">Email</Label>
|
|
203
|
+
<div className="relative">
|
|
204
|
+
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
205
|
+
<Input
|
|
206
|
+
id="email"
|
|
207
|
+
type="email"
|
|
208
|
+
placeholder="admin@example.com"
|
|
209
|
+
value={email}
|
|
210
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
211
|
+
className="pl-10"
|
|
212
|
+
required
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div className="space-y-2">
|
|
218
|
+
<Label htmlFor="password">Password</Label>
|
|
219
|
+
<div className="relative">
|
|
220
|
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
221
|
+
<Input
|
|
222
|
+
id="password"
|
|
223
|
+
type="password"
|
|
224
|
+
placeholder="Create a strong password"
|
|
225
|
+
value={password}
|
|
226
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
227
|
+
className="pl-10"
|
|
228
|
+
required
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
{validationErrors.length > 0 && (
|
|
232
|
+
<ul className="text-sm text-muted-foreground list-disc pl-5 space-y-1">
|
|
233
|
+
{validationErrors.map((validationError, i) => (
|
|
234
|
+
<li key={i} className="text-destructive">
|
|
235
|
+
{validationError}
|
|
236
|
+
</li>
|
|
237
|
+
))}
|
|
238
|
+
</ul>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<div className="space-y-2">
|
|
243
|
+
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
|
244
|
+
<div className="relative">
|
|
245
|
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
246
|
+
<Input
|
|
247
|
+
id="confirmPassword"
|
|
248
|
+
type="password"
|
|
249
|
+
placeholder="Confirm your password"
|
|
250
|
+
value={confirmPassword}
|
|
251
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
252
|
+
className="pl-10"
|
|
253
|
+
required
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
{confirmPassword && password !== confirmPassword && (
|
|
257
|
+
<p className="text-sm text-destructive">
|
|
258
|
+
Passwords do not match
|
|
259
|
+
</p>
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div className="text-xs text-muted-foreground">
|
|
264
|
+
Password must be at least 8 characters and contain:
|
|
265
|
+
<ul className="list-disc pl-5 mt-1">
|
|
266
|
+
<li>At least one uppercase letter</li>
|
|
267
|
+
<li>At least one lowercase letter</li>
|
|
268
|
+
<li>At least one number</li>
|
|
269
|
+
</ul>
|
|
270
|
+
</div>
|
|
271
|
+
</CardContent>
|
|
272
|
+
<CardFooter>
|
|
273
|
+
<Button
|
|
274
|
+
type="submit"
|
|
275
|
+
className="w-full"
|
|
276
|
+
disabled={
|
|
277
|
+
loading ||
|
|
278
|
+
validationErrors.length > 0 ||
|
|
279
|
+
password !== confirmPassword ||
|
|
280
|
+
!name ||
|
|
281
|
+
!email
|
|
282
|
+
}
|
|
283
|
+
>
|
|
284
|
+
{loading ? "Creating Account..." : "Complete Setup"}
|
|
285
|
+
</Button>
|
|
286
|
+
</CardFooter>
|
|
287
|
+
</form>
|
|
288
|
+
</Card>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { useNavigate, Link } from "react-router-dom";
|
|
3
|
+
import {
|
|
4
|
+
User,
|
|
5
|
+
Mail,
|
|
6
|
+
Key,
|
|
7
|
+
ArrowLeft,
|
|
8
|
+
CheckCircle,
|
|
9
|
+
AlertCircle,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
12
|
+
import { AuthApi, authRoutes } from "@checkstack/auth-common";
|
|
13
|
+
import { resolveRoute } from "@checkstack/common";
|
|
14
|
+
import {
|
|
15
|
+
Button,
|
|
16
|
+
Input,
|
|
17
|
+
Label,
|
|
18
|
+
Card,
|
|
19
|
+
CardHeader,
|
|
20
|
+
CardTitle,
|
|
21
|
+
CardDescription,
|
|
22
|
+
CardContent,
|
|
23
|
+
CardFooter,
|
|
24
|
+
Alert,
|
|
25
|
+
AlertIcon,
|
|
26
|
+
AlertContent,
|
|
27
|
+
AlertTitle,
|
|
28
|
+
AlertDescription,
|
|
29
|
+
} from "@checkstack/ui";
|
|
30
|
+
|
|
31
|
+
export const ProfilePage = () => {
|
|
32
|
+
const navigate = useNavigate();
|
|
33
|
+
const [name, setName] = useState("");
|
|
34
|
+
const [email, setEmail] = useState("");
|
|
35
|
+
const [originalName, setOriginalName] = useState("");
|
|
36
|
+
const [originalEmail, setOriginalEmail] = useState("");
|
|
37
|
+
const [loading, setLoading] = useState(false);
|
|
38
|
+
const [error, setError] = useState<string>();
|
|
39
|
+
const [success, setSuccess] = useState(false);
|
|
40
|
+
const [hasCredentialAccount, setHasCredentialAccount] = useState(false);
|
|
41
|
+
|
|
42
|
+
const authClient = usePluginClient(AuthApi);
|
|
43
|
+
|
|
44
|
+
// Fetch current user profile
|
|
45
|
+
const { data: profile, isLoading: loadingProfile } =
|
|
46
|
+
authClient.getCurrentUserProfile.useQuery({});
|
|
47
|
+
|
|
48
|
+
// Update mutation
|
|
49
|
+
const updateMutation = authClient.updateCurrentUser.useMutation({
|
|
50
|
+
onSuccess: () => {
|
|
51
|
+
setSuccess(true);
|
|
52
|
+
setOriginalName(name);
|
|
53
|
+
setOriginalEmail(email);
|
|
54
|
+
setTimeout(() => setSuccess(false), 3000);
|
|
55
|
+
},
|
|
56
|
+
onError: (err) => {
|
|
57
|
+
setError(err.message);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Populate form when profile loads
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (profile) {
|
|
64
|
+
setName(profile.name);
|
|
65
|
+
setEmail(profile.email);
|
|
66
|
+
setOriginalName(profile.name);
|
|
67
|
+
setOriginalEmail(profile.email);
|
|
68
|
+
setHasCredentialAccount(profile.hasCredentialAccount);
|
|
69
|
+
}
|
|
70
|
+
}, [profile]);
|
|
71
|
+
|
|
72
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
setError(undefined);
|
|
75
|
+
setLoading(true);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const updates: { name?: string; email?: string } = {};
|
|
79
|
+
if (name !== originalName) updates.name = name;
|
|
80
|
+
if (email !== originalEmail && hasCredentialAccount)
|
|
81
|
+
updates.email = email;
|
|
82
|
+
|
|
83
|
+
// Only call if there are changes
|
|
84
|
+
if (Object.keys(updates).length > 0) {
|
|
85
|
+
await updateMutation.mutateAsync(updates);
|
|
86
|
+
} else {
|
|
87
|
+
setSuccess(true);
|
|
88
|
+
setTimeout(() => setSuccess(false), 3000);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Error handled by mutation
|
|
92
|
+
} finally {
|
|
93
|
+
setLoading(false);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const hasChanges =
|
|
98
|
+
name !== originalName || (hasCredentialAccount && email !== originalEmail);
|
|
99
|
+
|
|
100
|
+
// Loading state
|
|
101
|
+
if (loadingProfile) {
|
|
102
|
+
return (
|
|
103
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
104
|
+
<Card className="w-full max-w-md">
|
|
105
|
+
<CardContent className="pt-6">
|
|
106
|
+
<div className="space-y-4">
|
|
107
|
+
<div className="h-4 bg-muted animate-pulse rounded" />
|
|
108
|
+
<div className="h-10 bg-muted animate-pulse rounded" />
|
|
109
|
+
<div className="h-10 bg-muted animate-pulse rounded" />
|
|
110
|
+
</div>
|
|
111
|
+
</CardContent>
|
|
112
|
+
</Card>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
119
|
+
<Card className="w-full max-w-md">
|
|
120
|
+
<CardHeader className="space-y-1">
|
|
121
|
+
<CardTitle className="text-2xl font-bold">Profile</CardTitle>
|
|
122
|
+
<CardDescription>Manage your account settings</CardDescription>
|
|
123
|
+
</CardHeader>
|
|
124
|
+
<form onSubmit={handleSubmit}>
|
|
125
|
+
<CardContent className="space-y-4">
|
|
126
|
+
{error && (
|
|
127
|
+
<Alert variant="error">
|
|
128
|
+
<AlertIcon>
|
|
129
|
+
<AlertCircle className="h-4 w-4" />
|
|
130
|
+
</AlertIcon>
|
|
131
|
+
<AlertContent>
|
|
132
|
+
<AlertTitle>Error</AlertTitle>
|
|
133
|
+
<AlertDescription>{error}</AlertDescription>
|
|
134
|
+
</AlertContent>
|
|
135
|
+
</Alert>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{success && (
|
|
139
|
+
<Alert variant="success">
|
|
140
|
+
<AlertIcon>
|
|
141
|
+
<CheckCircle className="h-4 w-4" />
|
|
142
|
+
</AlertIcon>
|
|
143
|
+
<AlertContent>
|
|
144
|
+
<AlertTitle>Success</AlertTitle>
|
|
145
|
+
<AlertDescription>
|
|
146
|
+
Profile updated successfully
|
|
147
|
+
</AlertDescription>
|
|
148
|
+
</AlertContent>
|
|
149
|
+
</Alert>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
<div className="space-y-2">
|
|
153
|
+
<Label htmlFor="name">Name</Label>
|
|
154
|
+
<div className="relative">
|
|
155
|
+
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
156
|
+
<Input
|
|
157
|
+
id="name"
|
|
158
|
+
type="text"
|
|
159
|
+
placeholder="Your name"
|
|
160
|
+
value={name}
|
|
161
|
+
onChange={(e) => setName(e.target.value)}
|
|
162
|
+
className="pl-10"
|
|
163
|
+
required
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div className="space-y-2">
|
|
169
|
+
<Label htmlFor="email">Email</Label>
|
|
170
|
+
<div className="relative">
|
|
171
|
+
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
172
|
+
<Input
|
|
173
|
+
id="email"
|
|
174
|
+
type="email"
|
|
175
|
+
placeholder="your@email.com"
|
|
176
|
+
value={email}
|
|
177
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
178
|
+
className="pl-10"
|
|
179
|
+
disabled={!hasCredentialAccount}
|
|
180
|
+
required
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
{!hasCredentialAccount && (
|
|
184
|
+
<p className="text-xs text-muted-foreground">
|
|
185
|
+
Email is managed by your social login provider
|
|
186
|
+
</p>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{hasCredentialAccount && (
|
|
191
|
+
<div className="pt-2">
|
|
192
|
+
<Link
|
|
193
|
+
to={resolveRoute(authRoutes.routes.changePassword)}
|
|
194
|
+
className="inline-flex items-center gap-2 text-sm text-primary hover:underline"
|
|
195
|
+
>
|
|
196
|
+
<Key className="h-4 w-4" />
|
|
197
|
+
Change Password
|
|
198
|
+
</Link>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
</CardContent>
|
|
202
|
+
<CardFooter className="flex flex-col gap-4">
|
|
203
|
+
<Button
|
|
204
|
+
type="submit"
|
|
205
|
+
className="w-full"
|
|
206
|
+
disabled={loading || !hasChanges}
|
|
207
|
+
>
|
|
208
|
+
{loading ? "Saving..." : "Save Changes"}
|
|
209
|
+
</Button>
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
onClick={() => navigate(-1)}
|
|
213
|
+
className="text-sm text-primary hover:underline flex items-center justify-center gap-1"
|
|
214
|
+
>
|
|
215
|
+
<ArrowLeft className="h-4 w-4" />
|
|
216
|
+
Go Back
|
|
217
|
+
</button>
|
|
218
|
+
</CardFooter>
|
|
219
|
+
</form>
|
|
220
|
+
</Card>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
};
|
package/src/index.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
NavbarRightSlot,
|
|
9
9
|
UserMenuItemsSlot,
|
|
10
10
|
UserMenuItemsBottomSlot,
|
|
11
|
+
NavbarLeftSlot,
|
|
11
12
|
} from "@checkstack/frontend-api";
|
|
12
13
|
import {
|
|
13
14
|
LoginPage,
|
|
@@ -19,6 +20,8 @@ import { AuthErrorPage } from "./components/AuthErrorPage";
|
|
|
19
20
|
import { ForgotPasswordPage } from "./components/ForgotPasswordPage";
|
|
20
21
|
import { ResetPasswordPage } from "./components/ResetPasswordPage";
|
|
21
22
|
import { ChangePasswordPage } from "./components/ChangePasswordPage";
|
|
23
|
+
import { OnboardingPage } from "./components/OnboardingPage";
|
|
24
|
+
import { ProfilePage } from "./components/ProfilePage";
|
|
22
25
|
import { authApiRef, AuthApi, AuthSession } from "./api";
|
|
23
26
|
import { getAuthClientLazy } from "./lib/auth-client";
|
|
24
27
|
|
|
@@ -26,7 +29,7 @@ import { useAccessRules } from "./hooks/useAccessRules";
|
|
|
26
29
|
|
|
27
30
|
import type { AccessRule } from "@checkstack/common";
|
|
28
31
|
import { useNavigate } from "react-router-dom";
|
|
29
|
-
import { Settings2,
|
|
32
|
+
import { Settings2, User } from "lucide-react";
|
|
30
33
|
import { DropdownMenuItem } from "@checkstack/ui";
|
|
31
34
|
import { UserMenuItemsContext } from "@checkstack/frontend-api";
|
|
32
35
|
import { AuthSettingsPage } from "./components/AuthSettingsPage";
|
|
@@ -36,6 +39,7 @@ import {
|
|
|
36
39
|
pluginMetadata,
|
|
37
40
|
} from "@checkstack/auth-common";
|
|
38
41
|
import { resolveRoute } from "@checkstack/common";
|
|
42
|
+
import { OnboardingCheck } from "./components/OnboardingCheck";
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
45
|
* Unified access API implementation.
|
|
@@ -108,7 +112,7 @@ class BetterAuthApi implements AuthApi {
|
|
|
108
112
|
provider,
|
|
109
113
|
callbackURL: frontendUrl,
|
|
110
114
|
errorCallbackURL: `${frontendUrl}${resolveRoute(
|
|
111
|
-
authRoutes.routes.error
|
|
115
|
+
authRoutes.routes.error,
|
|
112
116
|
)}`,
|
|
113
117
|
});
|
|
114
118
|
}
|
|
@@ -194,6 +198,14 @@ export const authPlugin = createFrontendPlugin({
|
|
|
194
198
|
route: authRoutes.routes.changePassword,
|
|
195
199
|
element: <ChangePasswordPage />,
|
|
196
200
|
},
|
|
201
|
+
{
|
|
202
|
+
route: authRoutes.routes.profile,
|
|
203
|
+
element: <ProfilePage />,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
route: authRoutes.routes.onboarding,
|
|
207
|
+
element: <OnboardingPage />,
|
|
208
|
+
},
|
|
197
209
|
],
|
|
198
210
|
extensions: [
|
|
199
211
|
{
|
|
@@ -222,22 +234,16 @@ export const authPlugin = createFrontendPlugin({
|
|
|
222
234
|
},
|
|
223
235
|
}),
|
|
224
236
|
createSlotExtension(UserMenuItemsSlot, {
|
|
225
|
-
id: "auth.user-menu.
|
|
226
|
-
component: (
|
|
237
|
+
id: "auth.user-menu.profile",
|
|
238
|
+
component: () => {
|
|
227
239
|
const navigate = useNavigate();
|
|
228
240
|
|
|
229
|
-
// Only show for credential-authenticated users
|
|
230
|
-
// The changePassword API requires current password, so only credential users can use it
|
|
231
|
-
if (!hasCredentialAccount) return <React.Fragment />;
|
|
232
|
-
|
|
233
241
|
return (
|
|
234
242
|
<DropdownMenuItem
|
|
235
|
-
onClick={() =>
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
icon={<Key className="h-4 w-4" />}
|
|
243
|
+
onClick={() => navigate(resolveRoute(authRoutes.routes.profile))}
|
|
244
|
+
icon={<User className="h-4 w-4" />}
|
|
239
245
|
>
|
|
240
|
-
|
|
246
|
+
Profile
|
|
241
247
|
</DropdownMenuItem>
|
|
242
248
|
);
|
|
243
249
|
},
|
|
@@ -246,5 +252,9 @@ export const authPlugin = createFrontendPlugin({
|
|
|
246
252
|
id: "auth.user-menu.logout",
|
|
247
253
|
component: LogoutMenuItem,
|
|
248
254
|
}),
|
|
255
|
+
createSlotExtension(NavbarLeftSlot, {
|
|
256
|
+
id: "auth.onboarding-guard",
|
|
257
|
+
component: OnboardingCheck,
|
|
258
|
+
}),
|
|
249
259
|
],
|
|
250
260
|
});
|