@checkmate-monitor/auth-frontend 0.1.1 → 0.2.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 +16 -0
- package/package.json +1 -1
- package/src/components/ChangePasswordPage.tsx +259 -0
- package/src/index.tsx +25 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @checkmate-monitor/auth-frontend
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e26c08e: Add password change functionality for credential-authenticated users
|
|
8
|
+
|
|
9
|
+
- Add `changePassword` route to auth-common
|
|
10
|
+
- Create `ChangePasswordPage.tsx` component with password validation, current password verification, and session revocation option
|
|
11
|
+
- Add "Change Password" menu item in User Menu
|
|
12
|
+
- Reuses patterns from existing password reset flow for consistency
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [e26c08e]
|
|
17
|
+
- @checkmate-monitor/auth-common@0.2.0
|
|
18
|
+
|
|
3
19
|
## 0.1.1
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { Lock, ArrowLeft, CheckCircle, AlertCircle, Key } from "lucide-react";
|
|
4
|
+
import { passwordSchema } from "@checkmate-monitor/auth-common";
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
Input,
|
|
8
|
+
Label,
|
|
9
|
+
Card,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
CardDescription,
|
|
13
|
+
CardContent,
|
|
14
|
+
CardFooter,
|
|
15
|
+
Alert,
|
|
16
|
+
AlertIcon,
|
|
17
|
+
AlertContent,
|
|
18
|
+
AlertTitle,
|
|
19
|
+
AlertDescription,
|
|
20
|
+
Checkbox,
|
|
21
|
+
} from "@checkmate-monitor/ui";
|
|
22
|
+
import { useAuthClient } from "../lib/auth-client";
|
|
23
|
+
|
|
24
|
+
export const ChangePasswordPage = () => {
|
|
25
|
+
const navigate = useNavigate();
|
|
26
|
+
|
|
27
|
+
const [currentPassword, setCurrentPassword] = useState("");
|
|
28
|
+
const [newPassword, setNewPassword] = useState("");
|
|
29
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
30
|
+
const [revokeOtherSessions, setRevokeOtherSessions] = useState(true);
|
|
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 new password on change
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (newPassword) {
|
|
40
|
+
const result = passwordSchema.safeParse(newPassword);
|
|
41
|
+
if (result.success) {
|
|
42
|
+
setValidationErrors([]);
|
|
43
|
+
} else {
|
|
44
|
+
setValidationErrors(result.error.issues.map((issue) => issue.message));
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
setValidationErrors([]);
|
|
48
|
+
}
|
|
49
|
+
}, [newPassword]);
|
|
50
|
+
|
|
51
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
setError(undefined);
|
|
54
|
+
|
|
55
|
+
// Frontend validation
|
|
56
|
+
if (newPassword !== confirmPassword) {
|
|
57
|
+
setError("New passwords do not match");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = passwordSchema.safeParse(newPassword);
|
|
62
|
+
if (!result.success) {
|
|
63
|
+
setError(result.error.issues[0].message);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!currentPassword) {
|
|
68
|
+
setError("Current password is required");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setLoading(true);
|
|
73
|
+
try {
|
|
74
|
+
const response = await authClient.changePassword({
|
|
75
|
+
currentPassword,
|
|
76
|
+
newPassword,
|
|
77
|
+
revokeOtherSessions,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (response.error) {
|
|
81
|
+
setError(response.error.message ?? "Failed to change password");
|
|
82
|
+
} else {
|
|
83
|
+
setSuccess(true);
|
|
84
|
+
}
|
|
85
|
+
} catch (error_) {
|
|
86
|
+
setError(
|
|
87
|
+
error_ instanceof Error ? error_.message : "Failed to change password"
|
|
88
|
+
);
|
|
89
|
+
} finally {
|
|
90
|
+
setLoading(false);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Success state
|
|
95
|
+
if (success) {
|
|
96
|
+
return (
|
|
97
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
98
|
+
<Card className="w-full max-w-md">
|
|
99
|
+
<CardHeader className="space-y-1 text-center">
|
|
100
|
+
<div className="mx-auto w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mb-2">
|
|
101
|
+
<CheckCircle className="h-6 w-6 text-primary" />
|
|
102
|
+
</div>
|
|
103
|
+
<CardTitle className="text-2xl font-bold">
|
|
104
|
+
Password Changed Successfully
|
|
105
|
+
</CardTitle>
|
|
106
|
+
<CardDescription>
|
|
107
|
+
Your password has been updated.
|
|
108
|
+
{revokeOtherSessions &&
|
|
109
|
+
" All other sessions have been signed out."}
|
|
110
|
+
</CardDescription>
|
|
111
|
+
</CardHeader>
|
|
112
|
+
<CardFooter>
|
|
113
|
+
<Button className="w-full" onClick={() => navigate("/")}>
|
|
114
|
+
Go to Dashboard
|
|
115
|
+
</Button>
|
|
116
|
+
</CardFooter>
|
|
117
|
+
</Card>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
124
|
+
<Card className="w-full max-w-md">
|
|
125
|
+
<CardHeader className="space-y-1">
|
|
126
|
+
<CardTitle className="text-2xl font-bold">Change Password</CardTitle>
|
|
127
|
+
<CardDescription>
|
|
128
|
+
Enter your current password and choose a new password.
|
|
129
|
+
</CardDescription>
|
|
130
|
+
</CardHeader>
|
|
131
|
+
<form onSubmit={handleSubmit}>
|
|
132
|
+
<CardContent className="space-y-4">
|
|
133
|
+
{error && (
|
|
134
|
+
<Alert variant="error">
|
|
135
|
+
<AlertIcon>
|
|
136
|
+
<AlertCircle className="h-4 w-4" />
|
|
137
|
+
</AlertIcon>
|
|
138
|
+
<AlertContent>
|
|
139
|
+
<AlertTitle>Error</AlertTitle>
|
|
140
|
+
<AlertDescription>{error}</AlertDescription>
|
|
141
|
+
</AlertContent>
|
|
142
|
+
</Alert>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
<div className="space-y-2">
|
|
146
|
+
<Label htmlFor="currentPassword">Current Password</Label>
|
|
147
|
+
<div className="relative">
|
|
148
|
+
<Key className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
149
|
+
<Input
|
|
150
|
+
id="currentPassword"
|
|
151
|
+
type="password"
|
|
152
|
+
placeholder="Enter current password"
|
|
153
|
+
value={currentPassword}
|
|
154
|
+
onChange={(e) => setCurrentPassword(e.target.value)}
|
|
155
|
+
className="pl-10"
|
|
156
|
+
required
|
|
157
|
+
autoFocus
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div className="space-y-2">
|
|
163
|
+
<Label htmlFor="newPassword">New Password</Label>
|
|
164
|
+
<div className="relative">
|
|
165
|
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
166
|
+
<Input
|
|
167
|
+
id="newPassword"
|
|
168
|
+
type="password"
|
|
169
|
+
placeholder="Enter new password"
|
|
170
|
+
value={newPassword}
|
|
171
|
+
onChange={(e) => setNewPassword(e.target.value)}
|
|
172
|
+
className="pl-10"
|
|
173
|
+
required
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
{validationErrors.length > 0 && (
|
|
177
|
+
<ul className="text-sm text-muted-foreground list-disc pl-5 space-y-1">
|
|
178
|
+
{validationErrors.map((validationError, i) => (
|
|
179
|
+
<li key={i} className="text-destructive">
|
|
180
|
+
{validationError}
|
|
181
|
+
</li>
|
|
182
|
+
))}
|
|
183
|
+
</ul>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div className="space-y-2">
|
|
188
|
+
<Label htmlFor="confirmPassword">Confirm New Password</Label>
|
|
189
|
+
<div className="relative">
|
|
190
|
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
191
|
+
<Input
|
|
192
|
+
id="confirmPassword"
|
|
193
|
+
type="password"
|
|
194
|
+
placeholder="Confirm new password"
|
|
195
|
+
value={confirmPassword}
|
|
196
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
197
|
+
className="pl-10"
|
|
198
|
+
required
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
{confirmPassword && newPassword !== confirmPassword && (
|
|
202
|
+
<p className="text-sm text-destructive">
|
|
203
|
+
Passwords do not match
|
|
204
|
+
</p>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div className="text-xs text-muted-foreground">
|
|
209
|
+
Password must be at least 8 characters and contain:
|
|
210
|
+
<ul className="list-disc pl-5 mt-1">
|
|
211
|
+
<li>At least one uppercase letter</li>
|
|
212
|
+
<li>At least one lowercase letter</li>
|
|
213
|
+
<li>At least one number</li>
|
|
214
|
+
</ul>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div className="flex items-center space-x-2">
|
|
218
|
+
<Checkbox
|
|
219
|
+
id="revokeOtherSessions"
|
|
220
|
+
checked={revokeOtherSessions}
|
|
221
|
+
onCheckedChange={(checked) =>
|
|
222
|
+
setRevokeOtherSessions(checked === true)
|
|
223
|
+
}
|
|
224
|
+
/>
|
|
225
|
+
<Label
|
|
226
|
+
htmlFor="revokeOtherSessions"
|
|
227
|
+
className="text-sm font-normal"
|
|
228
|
+
>
|
|
229
|
+
Sign out of all other sessions
|
|
230
|
+
</Label>
|
|
231
|
+
</div>
|
|
232
|
+
</CardContent>
|
|
233
|
+
<CardFooter className="flex flex-col gap-4">
|
|
234
|
+
<Button
|
|
235
|
+
type="submit"
|
|
236
|
+
className="w-full"
|
|
237
|
+
disabled={
|
|
238
|
+
loading ||
|
|
239
|
+
validationErrors.length > 0 ||
|
|
240
|
+
newPassword !== confirmPassword ||
|
|
241
|
+
!currentPassword
|
|
242
|
+
}
|
|
243
|
+
>
|
|
244
|
+
{loading ? "Changing..." : "Change Password"}
|
|
245
|
+
</Button>
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
onClick={() => navigate(-1)}
|
|
249
|
+
className="text-sm text-primary hover:underline flex items-center justify-center gap-1"
|
|
250
|
+
>
|
|
251
|
+
<ArrowLeft className="h-4 w-4" />
|
|
252
|
+
Go Back
|
|
253
|
+
</button>
|
|
254
|
+
</CardFooter>
|
|
255
|
+
</form>
|
|
256
|
+
</Card>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
};
|
package/src/index.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import { RegisterPage } from "./components/RegisterPage";
|
|
|
16
16
|
import { AuthErrorPage } from "./components/AuthErrorPage";
|
|
17
17
|
import { ForgotPasswordPage } from "./components/ForgotPasswordPage";
|
|
18
18
|
import { ResetPasswordPage } from "./components/ResetPasswordPage";
|
|
19
|
+
import { ChangePasswordPage } from "./components/ChangePasswordPage";
|
|
19
20
|
import { authApiRef, AuthApi, AuthSession } from "./api";
|
|
20
21
|
import { getAuthClientLazy } from "./lib/auth-client";
|
|
21
22
|
|
|
@@ -23,7 +24,7 @@ import { usePermissions } from "./hooks/usePermissions";
|
|
|
23
24
|
|
|
24
25
|
import { PermissionAction } from "@checkmate-monitor/common";
|
|
25
26
|
import { useNavigate } from "react-router-dom";
|
|
26
|
-
import { Settings2 } from "lucide-react";
|
|
27
|
+
import { Settings2, Key } from "lucide-react";
|
|
27
28
|
import { DropdownMenuItem } from "@checkmate-monitor/ui";
|
|
28
29
|
import { useApi } from "@checkmate-monitor/frontend-api";
|
|
29
30
|
import { AuthSettingsPage } from "./components/AuthSettingsPage";
|
|
@@ -202,6 +203,10 @@ export const authPlugin = createFrontendPlugin({
|
|
|
202
203
|
route: authRoutes.routes.resetPassword,
|
|
203
204
|
element: <ResetPasswordPage />,
|
|
204
205
|
},
|
|
206
|
+
{
|
|
207
|
+
route: authRoutes.routes.changePassword,
|
|
208
|
+
element: <ChangePasswordPage />,
|
|
209
|
+
},
|
|
205
210
|
],
|
|
206
211
|
extensions: [
|
|
207
212
|
{
|
|
@@ -232,6 +237,25 @@ export const authPlugin = createFrontendPlugin({
|
|
|
232
237
|
);
|
|
233
238
|
},
|
|
234
239
|
},
|
|
240
|
+
{
|
|
241
|
+
id: "auth.user-menu.change-password",
|
|
242
|
+
slot: UserMenuItemsSlot,
|
|
243
|
+
component: () => {
|
|
244
|
+
const navigate = useNavigate();
|
|
245
|
+
// Only show for credential-authenticated users
|
|
246
|
+
// The changePassword API requires current password, so only credential users can use it
|
|
247
|
+
return (
|
|
248
|
+
<DropdownMenuItem
|
|
249
|
+
onClick={() =>
|
|
250
|
+
navigate(resolveRoute(authRoutes.routes.changePassword))
|
|
251
|
+
}
|
|
252
|
+
icon={<Key className="h-4 w-4" />}
|
|
253
|
+
>
|
|
254
|
+
Change Password
|
|
255
|
+
</DropdownMenuItem>
|
|
256
|
+
);
|
|
257
|
+
},
|
|
258
|
+
},
|
|
235
259
|
{
|
|
236
260
|
id: "auth.user-menu.logout",
|
|
237
261
|
slot: UserMenuItemsBottomSlot,
|