@checkmate-monitor/auth-frontend 0.1.0 → 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 +33 -0
- package/package.json +1 -1
- package/src/components/ChangePasswordPage.tsx +259 -0
- package/src/components/ForgotPasswordPage.tsx +2 -1
- package/src/components/RegisterPage.tsx +2 -1
- package/src/components/ResetPasswordPage.tsx +2 -1
- package/src/hooks/usePermissions.ts +2 -1
- package/src/index.tsx +33 -9
- package/src/lib/auth-client.ts +53 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
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
|
+
|
|
19
|
+
## 0.1.1
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- 0f8cc7d: Add runtime configuration API for Docker deployments
|
|
24
|
+
|
|
25
|
+
- Backend: Add `/api/config` endpoint serving `BASE_URL` at runtime
|
|
26
|
+
- Backend: Update CORS to use `BASE_URL` and auto-allow Vite dev server
|
|
27
|
+
- Backend: `INTERNAL_URL` now defaults to `localhost:3000` (no BASE_URL fallback)
|
|
28
|
+
- Frontend API: Add `RuntimeConfigProvider` context for runtime config
|
|
29
|
+
- Frontend: Use `RuntimeConfigProvider` from `frontend-api`
|
|
30
|
+
- Auth Frontend: Add `useAuthClient()` hook using runtime config
|
|
31
|
+
|
|
32
|
+
- Updated dependencies [0f8cc7d]
|
|
33
|
+
- @checkmate-monitor/frontend-api@0.0.3
|
|
34
|
+
- @checkmate-monitor/ui@0.1.1
|
|
35
|
+
|
|
3
36
|
## 0.1.0
|
|
4
37
|
|
|
5
38
|
### Minor 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
|
+
};
|
|
@@ -14,12 +14,13 @@ import {
|
|
|
14
14
|
CardContent,
|
|
15
15
|
CardFooter,
|
|
16
16
|
} from "@checkmate-monitor/ui";
|
|
17
|
-
import {
|
|
17
|
+
import { useAuthClient } from "../lib/auth-client";
|
|
18
18
|
|
|
19
19
|
export const ForgotPasswordPage = () => {
|
|
20
20
|
const [email, setEmail] = useState("");
|
|
21
21
|
const [loading, setLoading] = useState(false);
|
|
22
22
|
const [submitted, setSubmitted] = useState(false);
|
|
23
|
+
const authClient = useAuthClient();
|
|
23
24
|
|
|
24
25
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
25
26
|
e.preventDefault();
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "@checkmate-monitor/ui";
|
|
28
28
|
import { useEnabledStrategies } from "../hooks/useEnabledStrategies";
|
|
29
29
|
import { SocialProviderButton } from "./SocialProviderButton";
|
|
30
|
-
import {
|
|
30
|
+
import { useAuthClient } from "../lib/auth-client";
|
|
31
31
|
|
|
32
32
|
export const RegisterPage = () => {
|
|
33
33
|
const [name, setName] = useState("");
|
|
@@ -42,6 +42,7 @@ export const RegisterPage = () => {
|
|
|
42
42
|
const { strategies, loading: strategiesLoading } = useEnabledStrategies();
|
|
43
43
|
const [registrationAllowed, setRegistrationAllowed] = useState<boolean>(true);
|
|
44
44
|
const [checkingRegistration, setCheckingRegistration] = useState(true);
|
|
45
|
+
const authClient = useAuthClient();
|
|
45
46
|
|
|
46
47
|
// Validate password on change
|
|
47
48
|
useEffect(() => {
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
AlertTitle,
|
|
20
20
|
AlertDescription,
|
|
21
21
|
} from "@checkmate-monitor/ui";
|
|
22
|
-
import {
|
|
22
|
+
import { useAuthClient } from "../lib/auth-client";
|
|
23
23
|
|
|
24
24
|
export const ResetPasswordPage = () => {
|
|
25
25
|
const [searchParams] = useSearchParams();
|
|
@@ -32,6 +32,7 @@ export const ResetPasswordPage = () => {
|
|
|
32
32
|
const [error, setError] = useState<string>();
|
|
33
33
|
const [success, setSuccess] = useState(false);
|
|
34
34
|
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
|
35
|
+
const authClient = useAuthClient();
|
|
35
36
|
|
|
36
37
|
// Validate password on change
|
|
37
38
|
useEffect(() => {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { useAuthClient } from "../lib/auth-client";
|
|
3
3
|
import { rpcApiRef, useApi } from "@checkmate-monitor/frontend-api";
|
|
4
4
|
import { AuthApi } from "@checkmate-monitor/auth-common";
|
|
5
5
|
|
|
6
6
|
export const usePermissions = () => {
|
|
7
|
+
const authClient = useAuthClient();
|
|
7
8
|
const { data: session } = authClient.useSession();
|
|
8
9
|
const [permissions, setPermissions] = useState<string[]>([]);
|
|
9
10
|
const [loading, setLoading] = useState(true);
|
package/src/index.tsx
CHANGED
|
@@ -16,14 +16,15 @@ 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
|
-
import {
|
|
21
|
+
import { getAuthClientLazy } from "./lib/auth-client";
|
|
21
22
|
|
|
22
23
|
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";
|
|
@@ -93,7 +94,7 @@ class AuthPermissionApi implements PermissionApi {
|
|
|
93
94
|
*/
|
|
94
95
|
class BetterAuthApi implements AuthApi {
|
|
95
96
|
async signIn(email: string, password: string) {
|
|
96
|
-
const res = await
|
|
97
|
+
const res = await getAuthClientLazy().signIn.email({ email, password });
|
|
97
98
|
if (res.error) {
|
|
98
99
|
const error = new Error(res.error.message || res.error.statusText);
|
|
99
100
|
error.name = res.error.code || "AuthError";
|
|
@@ -118,9 +119,9 @@ class BetterAuthApi implements AuthApi {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
async signInWithSocial(provider: string) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
await
|
|
122
|
+
// Use current origin as callback URL (works in dev and production)
|
|
123
|
+
const frontendUrl = globalThis.location?.origin || "http://localhost:5173";
|
|
124
|
+
await getAuthClientLazy().signIn.social({
|
|
124
125
|
provider,
|
|
125
126
|
callbackURL: frontendUrl,
|
|
126
127
|
errorCallbackURL: `${frontendUrl}${resolveRoute(
|
|
@@ -130,7 +131,7 @@ class BetterAuthApi implements AuthApi {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
async signOut() {
|
|
133
|
-
await
|
|
134
|
+
await getAuthClientLazy().signOut({
|
|
134
135
|
fetchOptions: {
|
|
135
136
|
onSuccess: () => {
|
|
136
137
|
// Redirect to frontend root after successful logout
|
|
@@ -141,7 +142,7 @@ class BetterAuthApi implements AuthApi {
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
async getSession() {
|
|
144
|
-
const res = await
|
|
145
|
+
const res = await getAuthClientLazy().getSession();
|
|
145
146
|
if (res.error) {
|
|
146
147
|
const error = new Error(res.error.message || res.error.statusText);
|
|
147
148
|
error.name = res.error.code || "AuthError";
|
|
@@ -156,7 +157,7 @@ class BetterAuthApi implements AuthApi {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
useSession() {
|
|
159
|
-
const { data, isPending, error } =
|
|
160
|
+
const { data, isPending, error } = getAuthClientLazy().useSession();
|
|
160
161
|
return {
|
|
161
162
|
data: data as AuthSession | undefined,
|
|
162
163
|
isPending,
|
|
@@ -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,
|
package/src/lib/auth-client.ts
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
1
2
|
import { createAuthClient } from "better-auth/react";
|
|
3
|
+
import { useRuntimeConfig } from "@checkmate-monitor/frontend-api";
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// Cache for lazy-initialized client
|
|
6
|
+
let cachedClient: ReturnType<typeof createAuthClient> | undefined;
|
|
7
|
+
let configPromise: Promise<string> | undefined;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* React hook to get the auth client with proper runtime config.
|
|
11
|
+
* Uses RuntimeConfigProvider to get the base URL.
|
|
12
|
+
*/
|
|
13
|
+
export function useAuthClient() {
|
|
14
|
+
const { baseUrl } = useRuntimeConfig();
|
|
15
|
+
|
|
16
|
+
return useMemo(
|
|
17
|
+
() =>
|
|
18
|
+
createAuthClient({
|
|
19
|
+
baseURL: baseUrl,
|
|
20
|
+
basePath: "/api/auth",
|
|
21
|
+
}),
|
|
22
|
+
[baseUrl]
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Lazy-initialized auth client for class-based APIs.
|
|
28
|
+
* Fetches config from /api/config if not already cached.
|
|
29
|
+
* Use useAuthClient hook in React components instead.
|
|
30
|
+
*/
|
|
31
|
+
export function getAuthClientLazy(): ReturnType<typeof createAuthClient> {
|
|
32
|
+
if (!cachedClient) {
|
|
33
|
+
// Create with default URL initially
|
|
34
|
+
cachedClient = createAuthClient({
|
|
35
|
+
baseURL: "http://localhost:3000",
|
|
36
|
+
basePath: "/api/auth",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Fetch real config and update
|
|
40
|
+
if (!configPromise) {
|
|
41
|
+
configPromise = fetch("/api/config")
|
|
42
|
+
.then((res) => res.json())
|
|
43
|
+
.then((data: { baseUrl: string }) => data.baseUrl)
|
|
44
|
+
.catch(() => "http://localhost:3000");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
configPromise.then((baseUrl) => {
|
|
48
|
+
cachedClient = createAuthClient({
|
|
49
|
+
baseURL: baseUrl,
|
|
50
|
+
basePath: "/api/auth",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return cachedClient;
|
|
55
|
+
}
|