From a64e6865dc716439bd4b05f1bfb8489e9912d449 Mon Sep 17 00:00:00 2001 From: Alexey Dudarev <24899236+d-alex171@users.noreply.github.com> Date: Sat, 24 Jan 2026 11:28:05 -0800 Subject: [PATCH 1/4] added change password dialog --- .../pages/security-settings-content.tsx | 199 +++++++++++++++++- 1 file changed, 197 insertions(+), 2 deletions(-) diff --git a/src/components/settings/pages/security-settings-content.tsx b/src/components/settings/pages/security-settings-content.tsx index 05718669..b6e45b26 100644 --- a/src/components/settings/pages/security-settings-content.tsx +++ b/src/components/settings/pages/security-settings-content.tsx @@ -1,3 +1,198 @@ +import React, { useState } from 'react'; +import { createAuthClient } from 'better-auth/client'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; +import { AlertCircle, LogOut, Shield, Lock } from 'lucide-react'; +import { Alert, AlertDescription } from '@/components/ui/alert'; + export function SecuritySettingsContent() { - return <>Security coming soon...; -} + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + const authClient = createAuthClient() + + const handleLogout = async () => { + try { + setLoading(true); + await authClient.signOut(); + setMessage({ type: 'success', text: 'Successfully logged out' }); + // Redirect to login page + window.location.href = '/login'; + } catch (error) { + setMessage({ type: 'error', text: 'Failed to logout. Please try again.' }); + } finally { + setLoading(false); + } + }; + + const handleRevokeAllSessions = async () => { + try { + setLoading(true); + await authClient.revokeOtherSessions(); + setMessage({ type: 'success', text: 'All other sessions have been revoked' }); + } catch (error) { + setMessage({ type: 'error', text: 'Failed to revoke sessions. Please try again.' }); + } finally { + setLoading(false); + } + }; + + const handleChangePassword = async () => { + setMessage(null); + + if (newPassword !== confirmPassword) { + setMessage({ type: 'error', text: 'New passwords do not match' }); + return; + } + + if (newPassword.length < 8) { + setMessage({ type: 'error', text: 'Password must be at least 8 characters long' }); + return; + } + + try { + setLoading(true); + await authClient.changePassword({ + currentPassword, + newPassword, + revokeOtherSessions: false, + }); + setMessage({ type: 'success', text: 'Password changed successfully' }); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + } catch (error) { + setMessage({ type: 'error', text: 'Failed to change password. Please check your current password.' }); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

Security

+

+ Manage your account security and sessions +

+
+ + + {message && ( + + + {message.text} + + )} + + + + + + Change Password + + + Update your password to keep your account secure + + + +
+
+ + setCurrentPassword(e.target.value)} + disabled={loading} + /> +
+
+ + setNewPassword(e.target.value)} + disabled={loading} + /> +
+
+ + setConfirmPassword(e.target.value)} + disabled={loading} + /> +
+ +
+
+
+ + {/* Session Management Section */} + + + + + Session Management + + + Manage your active sessions across all devices + + + +
+
+

Logout from all devices

+

+ End all sessions except the current one +

+
+ +
+
+
+ + {/* Logout Section */} + + + + + Logout + + + Sign out of your account on this device + + + + + + +
+ ); +} \ No newline at end of file From e6b7dd18ba7acbf2c18ed96b1fee32a23716beb9 Mon Sep 17 00:00:00 2001 From: Alexey Dudarev <24899236+d-alex171@users.noreply.github.com> Date: Sat, 31 Jan 2026 10:39:16 -0800 Subject: [PATCH 2/4] fixed logout button --- .../pages/security-settings-content.tsx | 20 +++++-------------- src/components/settings/settings-dialog.tsx | 2 +- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/components/settings/pages/security-settings-content.tsx b/src/components/settings/pages/security-settings-content.tsx index b6e45b26..f0d63fda 100644 --- a/src/components/settings/pages/security-settings-content.tsx +++ b/src/components/settings/pages/security-settings-content.tsx @@ -7,6 +7,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Separator } from '@/components/ui/separator'; import { AlertCircle, LogOut, Shield, Lock } from 'lucide-react'; import { Alert, AlertDescription } from '@/components/ui/alert'; +import { forceLogout } from "@/lib/auth/logout"; + export function SecuritySettingsContent() { const [currentPassword, setCurrentPassword] = useState(''); @@ -18,18 +20,8 @@ export function SecuritySettingsContent() { const authClient = createAuthClient() const handleLogout = async () => { - try { - setLoading(true); - await authClient.signOut(); - setMessage({ type: 'success', text: 'Successfully logged out' }); - // Redirect to login page - window.location.href = '/login'; - } catch (error) { - setMessage({ type: 'error', text: 'Failed to logout. Please try again.' }); - } finally { - setLoading(false); - } - }; + await forceLogout(); + }; const handleRevokeAllSessions = async () => { try { @@ -75,7 +67,7 @@ export function SecuritySettingsContent() { }; return ( -
+

Security

@@ -140,7 +132,6 @@ export function SecuritySettingsContent() { - {/* Session Management Section */} @@ -170,7 +161,6 @@ export function SecuritySettingsContent() { - {/* Logout Section */} diff --git a/src/components/settings/settings-dialog.tsx b/src/components/settings/settings-dialog.tsx index 6c4a9e03..c1715091 100644 --- a/src/components/settings/settings-dialog.tsx +++ b/src/components/settings/settings-dialog.tsx @@ -56,7 +56,7 @@ export const SettingsDialog = NiceModal.create(() => {

Date: Fri, 27 Feb 2026 18:39:08 -0800 Subject: [PATCH 3/4] refactored form validation --- .../pages/security-settings-content.tsx | 256 +++++++++++------- src/components/settings/settings-dialog.tsx | 2 +- 2 files changed, 157 insertions(+), 101 deletions(-) diff --git a/src/components/settings/pages/security-settings-content.tsx b/src/components/settings/pages/security-settings-content.tsx index f0d63fda..884495dc 100644 --- a/src/components/settings/pages/security-settings-content.tsx +++ b/src/components/settings/pages/security-settings-content.tsx @@ -1,71 +1,98 @@ -import React, { useState } from 'react'; -import { createAuthClient } from 'better-auth/client'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Separator } from '@/components/ui/separator'; -import { AlertCircle, LogOut, Shield, Lock } from 'lucide-react'; -import { Alert, AlertDescription } from '@/components/ui/alert'; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { authClient } from "@/lib/auth/client"; import { forceLogout } from "@/lib/auth/logout"; +import { getBetterAuthErrorMessage } from "@/lib/auth/extensions/get-better-auth-error"; + +import { FormInputField } from "@/components/form/FormInput"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { Spinner } from "@/components/ui/spinner"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { AlertCircle, CheckCircle2, LogOut, Shield, Lock } from "lucide-react"; + +const ChangePasswordSchema = z + .object({ + currentPassword: z.string().nonempty("Please fill out this field."), + newPassword: z + .string() + .nonempty("Please fill out this field.") + .min(8, "Password must be at least 8 characters long."), + confirmPassword: z.string().nonempty("Please fill out this field."), + }) + .superRefine((val, ctx) => { + if (val.newPassword !== val.confirmPassword) { + ctx.addIssue({ + code: "custom", + path: ["confirmPassword"], + message: "Passwords don't match.", + }); + } + }); +type ChangePasswordSchemaType = z.infer; export function SecuritySettingsContent() { - const [currentPassword, setCurrentPassword] = useState(''); - const [newPassword, setNewPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [loading, setLoading] = useState(false); - const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); - - const authClient = createAuthClient() - - const handleLogout = async () => { - await forceLogout(); - }; + const [sessionMessage, setSessionMessage] = useState<{ + type: "success" | "error"; + text: string; + } | null>(null); + const [sessionLoading, setSessionLoading] = useState(false); - const handleRevokeAllSessions = async () => { - try { - setLoading(true); - await authClient.revokeOtherSessions(); - setMessage({ type: 'success', text: 'All other sessions have been revoked' }); - } catch (error) { - setMessage({ type: 'error', text: 'Failed to revoke sessions. Please try again.' }); - } finally { - setLoading(false); - } - }; + const { + handleSubmit, + setError, + control, + reset, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(ChangePasswordSchema), + mode: "onSubmit", + reValidateMode: "onChange", + defaultValues: { + currentPassword: "", + newPassword: "", + confirmPassword: "", + }, + }); - const handleChangePassword = async () => { - setMessage(null); + const onSubmit = async (data: ChangePasswordSchemaType) => { + const { error } = await authClient.changePassword({ + currentPassword: data.currentPassword, + newPassword: data.newPassword, + revokeOtherSessions: false, + }); - if (newPassword !== confirmPassword) { - setMessage({ type: 'error', text: 'New passwords do not match' }); + if (error) { + setError("root", { + type: "custom", + message: getBetterAuthErrorMessage(error?.code), + }); return; } - if (newPassword.length < 8) { - setMessage({ type: 'error', text: 'Password must be at least 8 characters long' }); - return; - } + reset(); + setError("root", { type: "success" } as any); + }; + const handleRevokeAllSessions = async () => { try { - setLoading(true); - await authClient.changePassword({ - currentPassword, - newPassword, - revokeOtherSessions: false, - }); - setMessage({ type: 'success', text: 'Password changed successfully' }); - setCurrentPassword(''); - setNewPassword(''); - setConfirmPassword(''); - } catch (error) { - setMessage({ type: 'error', text: 'Failed to change password. Please check your current password.' }); + setSessionLoading(true); + await authClient.revokeOtherSessions(); + setSessionMessage({ type: "success", text: "All other sessions have been revoked." }); + } catch { + setSessionMessage({ type: "error", text: "Failed to revoke sessions. Please try again." }); } finally { - setLoading(false); + setSessionLoading(false); } }; + const isPasswordSuccess = (errors.root as any)?.type === "success"; + return (
@@ -76,13 +103,6 @@ export function SecuritySettingsContent() {
- {message && ( - - - {message.text} - - )} - @@ -94,41 +114,64 @@ export function SecuritySettingsContent() { -
-
- - setCurrentPassword(e.target.value)} - disabled={loading} - /> -
-
- - setNewPassword(e.target.value)} - disabled={loading} - /> -
-
- - setConfirmPassword(e.target.value)} - disabled={loading} - /> -
- -
+
@@ -143,6 +186,21 @@ export function SecuritySettingsContent() { + {sessionMessage && ( + + {sessionMessage.type === "error" ? ( + + ) : ( + + )} + {sessionMessage.text} + + )} +

Logout from all devices

@@ -153,29 +211,27 @@ export function SecuritySettingsContent() {
+ {/* Logout */} Logout - - Sign out of your account on this device - + Sign out of your account on this device