import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { useState, useEffect, useMemo } from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../ui/dialog"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "../ui/form"; import { LuCheck, LuX, LuEye, LuEyeOff, LuExternalLink } from "react-icons/lu"; import { useTranslation } from "react-i18next"; import { useDocDomain } from "@/hooks/use-doc-domain"; import useSWR from "swr"; import { formatSecondsToDuration } from "@/utils/dateUtil"; import ActivityIndicator from "../indicators/activity-indicator"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; type SetPasswordProps = { show: boolean; onSave: (password: string, oldPassword?: string) => void; onCancel: () => void; initialError?: string | null; username?: string; isLoading?: boolean; }; export default function SetPasswordDialog({ show, onSave, onCancel, initialError, username, isLoading = false, }: SetPasswordProps) { const { t } = useTranslation(["views/settings", "common"]); const { getLocaleDocUrl } = useDocDomain(); const { data: config } = useSWR("config"); const refreshSeconds: number | undefined = config?.auth?.refresh_time ?? undefined; const refreshTimeLabel = refreshSeconds ? formatSecondsToDuration(refreshSeconds) : "30 minutes"; // visibility toggles for password fields const [showOldPassword, setShowOldPassword] = useState(false); const [showPasswordVisible, setShowPasswordVisible] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); // Create form schema with conditional old password requirement const formSchema = useMemo(() => { const baseSchema = { password: z .string() .min(8, t("users.dialog.form.password.requirements.length")) .regex(/[A-Z]/, t("users.dialog.form.password.requirements.uppercase")) .regex(/\d/, t("users.dialog.form.password.requirements.digit")) .regex( /[!@#$%^&*(),.?":{}|<>]/, t("users.dialog.form.password.requirements.special"), ), confirmPassword: z.string(), }; if (username) { return z .object({ oldPassword: z .string() .min(1, t("users.dialog.passwordSetting.currentPasswordRequired")), ...baseSchema, }) .refine((data) => data.password === data.confirmPassword, { message: t("users.dialog.passwordSetting.doNotMatch"), path: ["confirmPassword"], }); } else { return z .object(baseSchema) .refine((data) => data.password === data.confirmPassword, { message: t("users.dialog.passwordSetting.doNotMatch"), path: ["confirmPassword"], }); } }, [username, t]); type FormValues = z.infer; const defaultValues = username ? { oldPassword: "", password: "", confirmPassword: "", } : { password: "", confirmPassword: "", }; const form = useForm({ resolver: zodResolver(formSchema), mode: "onChange", defaultValues: defaultValues as FormValues, }); const password = form.watch("password"); const confirmPassword = form.watch("confirmPassword"); // Password strength calculation const passwordStrength = useMemo(() => { if (!password) return 0; let strength = 0; if (password.length >= 8) strength += 1; if (/\d/.test(password)) strength += 1; if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength += 1; if (/[A-Z]/.test(password)) strength += 1; return strength; }, [password]); const requirements = useMemo( () => ({ length: password?.length >= 8, uppercase: /[A-Z]/.test(password || ""), digit: /\d/.test(password || ""), special: /[!@#$%^&*(),.?":{}|<>]/.test(password || ""), }), [password], ); // Reset form and visibility toggles when dialog opens/closes useEffect(() => { if (show) { form.reset(); setShowOldPassword(false); setShowPasswordVisible(false); setShowConfirmPassword(false); } }, [show, form]); // Handle backend errors useEffect(() => { if (show && initialError) { const errorMsg = String(initialError); // Check if the error is about incorrect current password if ( errorMsg.toLowerCase().includes("current password is incorrect") || errorMsg.toLowerCase().includes("current password incorrect") ) { if (username) { form.setError("oldPassword" as keyof FormValues, { type: "manual", message: t("users.dialog.passwordSetting.incorrectCurrentPassword"), }); } } else { // For other errors, show as form-level error form.setError("root", { type: "manual", message: errorMsg, }); } } }, [show, initialError, form, t, username]); const onSubmit = async (values: FormValues) => { const oldPassword = "oldPassword" in values ? ( values as { oldPassword: string; password: string; confirmPassword: string; } ).oldPassword : undefined; onSave(values.password, oldPassword); }; const getStrengthLabel = () => { if (!password) return ""; if (passwordStrength <= 1) return t("users.dialog.form.password.strength.weak"); if (passwordStrength === 2) return t("users.dialog.form.password.strength.medium"); if (passwordStrength === 3) return t("users.dialog.form.password.strength.strong"); return t("users.dialog.form.password.strength.veryStrong"); }; const getStrengthColor = () => { if (!password) return "bg-gray-200"; if (passwordStrength <= 1) return "bg-red-500"; if (passwordStrength === 2) return "bg-yellow-500"; if (passwordStrength === 3) return "bg-green-500"; return "bg-green-600"; }; return ( {username ? t("users.dialog.passwordSetting.updatePassword", { username, ns: "views/settings", }) : t("users.dialog.passwordSetting.setPassword")} {t("users.dialog.passwordSetting.desc")}

{t("users.dialog.passwordSetting.multiDeviceWarning", { refresh_time: refreshTimeLabel, ns: "views/settings", })}

{t("readTheDocumentation", { ns: "common" })}

{username && ( ( {t("users.dialog.form.currentPassword.title")}
)} /> )} ( {t("users.dialog.form.newPassword.title")}
{password && (

{t("users.dialog.form.password.strength.title")} {getStrengthLabel()}

{t("users.dialog.form.password.requirements.title")}

  • {requirements.length ? ( ) : ( )} {t( "users.dialog.form.password.requirements.length", )}
  • {requirements.uppercase ? ( ) : ( )} {t( "users.dialog.form.password.requirements.uppercase", )}
  • {requirements.digit ? ( ) : ( )} {t( "users.dialog.form.password.requirements.digit", )}
  • {requirements.special ? ( ) : ( )} {t( "users.dialog.form.password.requirements.special", )}
)} )} /> ( {t("users.dialog.form.password.confirm.title")}
{password && confirmPassword && password === confirmPassword && (
{t("users.dialog.form.password.match")}
)}
)} /> {form.formState.errors.root && (
{form.formState.errors.root.message}
)}
); }