@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkmate-monitor/auth-frontend",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "exports": {
@@ -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,