diff --git a/src/components/settings/pages/security-settings-content.tsx b/src/components/settings/pages/security-settings-content.tsx index 05718669..30699b95 100644 --- a/src/components/settings/pages/security-settings-content.tsx +++ b/src/components/settings/pages/security-settings-content.tsx @@ -1,3 +1,244 @@ +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() { - return <>Security coming soon...; -} + const [passwordSuccess, setPasswordSuccess] = useState(false) + const [sessionMessage, setSessionMessage] = useState<{ + type: "success" | "error"; + text: string; + } | null>(null); + const [sessionLoading, setSessionLoading] = useState(false); + + const { + handleSubmit, + setError, + control, + reset, + formState: { errors, isSubmitting, isDirty }, + } = useForm({ + resolver: zodResolver(ChangePasswordSchema), + mode: "onSubmit", + reValidateMode: "onChange", + defaultValues: { + currentPassword: "", + newPassword: "", + confirmPassword: "", + }, + }); + + const onSubmit = async (data: ChangePasswordSchemaType) => { + const { error } = await authClient.changePassword({ + currentPassword: data.currentPassword, + newPassword: data.newPassword, + revokeOtherSessions: false, + }); + + if (error) { + setError("root", { + type: "custom", + message: getBetterAuthErrorMessage(error?.code), + }); + return; + } + + reset(); + setPasswordSuccess(true); + }; + + const handleRevokeAllSessions = async () => { + try { + 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 { + setSessionLoading(false); + } + }; + + const isPasswordSuccess = (errors.root as any)?.type === "success"; + + return ( +
+
+

+ Manage your account security and sessions +

+
+ + + + + + + Change Password + + + Update your password to keep your account secure + + + +
+ {errors.root?.message && ( + + + Couldn't update password + {errors.root.message} + + )} + + {passwordSuccess && !isDirty && ( + + + Success + Password changed successfully. + + )} + + + + + + + +
+
+ + + + + + Session Management + + + Manage your active sessions across all devices + + + + {sessionMessage && ( + + {sessionMessage.type === "error" ? ( + + ) : ( + + )} + {sessionMessage.text} + + )} + +
+
+

Logout from all devices

+

+ End all sessions except the current one +

+
+ +
+
+
+ + {/* Logout */} + + + + + Logout + + Sign out of your account on this device + + + + + +
+ ); +} \ No newline at end of file