import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { useState, useEffect } from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../ui/dialog"; import { Label } from "../ui/label"; 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"; 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"; const [oldPassword, setOldPassword] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [passwordStrength, setPasswordStrength] = useState(0); const [error, setError] = useState(null); // visibility toggles for password fields const [showOldPassword, setShowOldPassword] = useState(false); const [showPasswordVisible, setShowPasswordVisible] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [hasInitialized, setHasInitialized] = useState(false); // Password strength requirements const requirements = { length: password.length >= 8, uppercase: /[A-Z]/.test(password), digit: /\d/.test(password), special: /[!@#$%^&*(),.?":{}|<>]/.test(password), }; useEffect(() => { if (show) { if (!hasInitialized) { setOldPassword(""); setPassword(""); setConfirmPassword(""); setError(null); setHasInitialized(true); } } else { setHasInitialized(false); } }, [show, hasInitialized]); useEffect(() => { if (show && initialError) { setError(initialError); } }, [show, initialError]); // Password strength calculation useEffect(() => { if (!password) { setPasswordStrength(0); return; } let strength = 0; if (requirements.length) strength += 1; if (requirements.digit) strength += 1; if (requirements.special) strength += 1; if (requirements.uppercase) strength += 1; setPasswordStrength(strength); // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [password]); const handleSave = async () => { if (!password) { setError(t("users.dialog.passwordSetting.cannotBeEmpty")); return; } // Validate all requirements if (!requirements.length) { setError(t("users.dialog.form.password.requirements.length")); return; } if (!requirements.uppercase) { setError(t("users.dialog.form.password.requirements.uppercase")); return; } if (!requirements.digit) { setError(t("users.dialog.form.password.requirements.digit")); return; } if (!requirements.special) { setError(t("users.dialog.form.password.requirements.special")); return; } if (password !== confirmPassword) { setError(t("users.dialog.passwordSetting.doNotMatch")); return; } // Require old password when changing own password (username is provided) if (username && !oldPassword) { setError(t("users.dialog.passwordSetting.currentPasswordRequired")); return; } onSave(password, oldPassword || undefined); }; 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 && (
{ setOldPassword(event.target.value); setError(null); }} placeholder={t( "users.dialog.form.currentPassword.placeholder", )} />
)}
{ setPassword(event.target.value); setError(null); }} placeholder={t("users.dialog.form.newPassword.placeholder")} autoFocus />
{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")}
)}
{ setConfirmPassword(event.target.value); setError(null); }} placeholder={t( "users.dialog.form.newPassword.confirm.placeholder", )} />
{/* Password match indicator */} {password && confirmPassword && (
{password === confirmPassword ? ( <> {t("users.dialog.form.password.match")} ) : ( <> {t("users.dialog.form.password.notMatch")} )}
)}
{error && (
{error}
)}
); }