diff --git a/frigate/api/auth.py b/frigate/api/auth.py index e345d645c..d913173d0 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -55,7 +55,6 @@ def require_admin_by_default(): "/auth", "/auth/first_time_login", "/login", - "/auth/verify", "/logout", # Authenticated user endpoints (allow_any_authenticated) "/profile", @@ -753,30 +752,6 @@ def login(request: Request, body: AppPostLoginBody): return JSONResponse(content={"message": "Login failed"}, status_code=401) -@router.post("/auth/verify", dependencies=[Depends(allow_public())]) -@limiter.limit(limit_value=rateLimiter.get_limit) -def verify(request: Request, body: AppPostLoginBody): - """Verify credentials without creating a session. - - This endpoint is used for password change verification and other - credential validation scenarios that don't require session creation. - """ - user = body.user - password = body.password - - try: - db_user: User = User.get_by_id(user) - except DoesNotExist: - return JSONResponse(content={"message": "Verification failed"}, status_code=401) - - password_hash = db_user.password_hash - if verify_password(password, password_hash): - return JSONResponse( - content={"message": "Verification successful"}, status_code=200 - ) - return JSONResponse(content={"message": "Verification failed"}, status_code=401) - - @router.get("/users", dependencies=[Depends(require_role(["admin"]))]) def get_users(): exports = ( diff --git a/web/src/components/menu/AccountSettings.tsx b/web/src/components/menu/AccountSettings.tsx index 74701031d..e5a367391 100644 --- a/web/src/components/menu/AccountSettings.tsx +++ b/web/src/components/menu/AccountSettings.tsx @@ -30,7 +30,6 @@ import axios from "axios"; import { toast } from "sonner"; import SetPasswordDialog from "../overlay/SetPasswordDialog"; import { useTranslation } from "react-i18next"; -import { verifyPassword } from "@/utils/authUtil"; type AccountSettingsProps = { className?: string; @@ -44,19 +43,16 @@ export default function AccountSettings({ className }: AccountSettingsProps) { const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); const [passwordError, setPasswordError] = useState(null); + const [isPasswordLoading, setIsPasswordLoading] = useState(false); const Container = isDesktop ? DropdownMenu : Drawer; const Trigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger; const Content = isDesktop ? DropdownMenuContent : DrawerContent; const MenuItem = isDesktop ? DropdownMenuItem : DrawerClose; - const verifyOldPassword = async (oldPassword: string): Promise => { - if (!profile?.username || profile.username === "anonymous") return false; - return verifyPassword(profile.username, oldPassword); - }; - const handlePasswordSave = async (password: string, oldPassword?: string) => { if (!profile?.username || profile.username === "anonymous") return; + setIsPasswordLoading(true); axios .put(`users/${profile.username}/password`, { password, @@ -66,6 +62,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) { if (response.status === 200) { setPasswordDialogOpen(false); setPasswordError(null); + setIsPasswordLoading(false); toast.success(t("users.toast.success.updatePassword"), { position: "top-center", }); @@ -77,16 +74,9 @@ export default function AccountSettings({ className }: AccountSettingsProps) { error.response?.data?.detail || "Unknown error"; - setPasswordDialogOpen(false); - setPasswordError(null); - toast.error( - t("users.toast.error.setPasswordFailed", { - errorMessage, - }), - { - position: "top-center", - }, - ); + // Keep dialog open and show error + setPasswordError(errorMessage); + setIsPasswordLoading(false); }); }; @@ -172,9 +162,9 @@ export default function AccountSettings({ className }: AccountSettingsProps) { setPasswordDialogOpen(false); setPasswordError(null); }} - onVerifyOldPassword={verifyOldPassword} initialError={passwordError} username={profile?.username} + isLoading={isPasswordLoading} /> ); diff --git a/web/src/components/menu/GeneralSettings.tsx b/web/src/components/menu/GeneralSettings.tsx index 93376650f..1788bce84 100644 --- a/web/src/components/menu/GeneralSettings.tsx +++ b/web/src/components/menu/GeneralSettings.tsx @@ -66,7 +66,6 @@ import { supportedLanguageKeys } from "@/lib/const"; import { useDocDomain } from "@/hooks/use-doc-domain"; import { MdCategory } from "react-icons/md"; -import { verifyPassword } from "@/utils/authUtil"; type GeneralSettingsProps = { className?: string; @@ -118,14 +117,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { const Portal = isDesktop ? DropdownMenuPortal : DialogPortal; const [passwordError, setPasswordError] = useState(null); - - const verifyOldPassword = async (oldPassword: string): Promise => { - if (!profile?.username || profile.username === "anonymous") return false; - return verifyPassword(profile.username, oldPassword); - }; + const [isPasswordLoading, setIsPasswordLoading] = useState(false); const handlePasswordSave = async (password: string, oldPassword?: string) => { if (!profile?.username || profile.username === "anonymous") return; + setIsPasswordLoading(true); axios .put(`users/${profile.username}/password`, { password, @@ -135,6 +131,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { if (response.status === 200) { setPasswordDialogOpen(false); setPasswordError(null); + setIsPasswordLoading(false); toast.success( t("users.toast.success.updatePassword", { ns: "views/settings", @@ -151,17 +148,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { error.response?.data?.detail || "Unknown error"; - setPasswordDialogOpen(false); - setPasswordError(null); - toast.error( - t("users.toast.error.setPasswordFailed", { - ns: "views/settings", - errorMessage, - }), - { - position: "top-center", - }, - ); + // Keep dialog open and show error + setPasswordError(errorMessage); + setIsPasswordLoading(false); }); }; @@ -573,9 +562,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { setPasswordDialogOpen(false); setPasswordError(null); }} - onVerifyOldPassword={verifyOldPassword} initialError={passwordError} username={profile?.username} + isLoading={isPasswordLoading} /> ); diff --git a/web/src/components/overlay/SetPasswordDialog.tsx b/web/src/components/overlay/SetPasswordDialog.tsx index cceef91f0..b66a5fa97 100644 --- a/web/src/components/overlay/SetPasswordDialog.tsx +++ b/web/src/components/overlay/SetPasswordDialog.tsx @@ -22,18 +22,18 @@ type SetPasswordProps = { show: boolean; onSave: (password: string, oldPassword?: string) => void; onCancel: () => void; - onVerifyOldPassword?: (oldPassword: string) => Promise; initialError?: string | null; username?: string; + isLoading?: boolean; }; export default function SetPasswordDialog({ show, onSave, onCancel, - onVerifyOldPassword, initialError, username, + isLoading = false, }: SetPasswordProps) { const { t } = useTranslation(["views/settings", "common"]); const { getLocaleDocUrl } = useDocDomain(); @@ -49,8 +49,6 @@ export default function SetPasswordDialog({ const [confirmPassword, setConfirmPassword] = useState(""); const [passwordStrength, setPasswordStrength] = useState(0); const [error, setError] = useState(null); - const [isValidatingOldPassword, setIsValidatingOldPassword] = - useState(false); // visibility toggles for password fields const [showOldPassword, setShowOldPassword] = useState(false); @@ -58,6 +56,7 @@ export default function SetPasswordDialog({ useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [hasInitialized, setHasInitialized] = useState(false); // Password strength requirements @@ -68,14 +67,23 @@ export default function SetPasswordDialog({ special: /[!@#$%^&*(),.?":{}|<>]/.test(password), }; - // Reset state when dialog opens/closes - useEffect(() => { if (show) { - setOldPassword(""); - setPassword(""); - setConfirmPassword(""); - setError(initialError || null); + if (!hasInitialized) { + setOldPassword(""); + setPassword(""); + setConfirmPassword(""); + setError(null); + setHasInitialized(true); + } + } else { + setHasInitialized(false); + } + }, [show, hasInitialized]); + + useEffect(() => { + if (show && initialError) { + setError(initialError); } }, [show, initialError]); @@ -133,24 +141,6 @@ export default function SetPasswordDialog({ return; } - // Verify old password if callback is provided and old password is provided - if (username && oldPassword && onVerifyOldPassword) { - setIsValidatingOldPassword(true); - try { - const isValid = await onVerifyOldPassword(oldPassword); - if (!isValid) { - setError(t("users.dialog.passwordSetting.incorrectCurrentPassword")); - setIsValidatingOldPassword(false); - return; - } - } catch (err) { - setError(t("users.dialog.passwordSetting.passwordVerificationFailed")); - setIsValidatingOldPassword(false); - return; - } - setIsValidatingOldPassword(false); - } - onSave(password, oldPassword || undefined); }; @@ -465,6 +455,7 @@ export default function SetPasswordDialog({ aria-label={t("button.cancel", { ns: "common" })} onClick={onCancel} type="button" + disabled={isLoading} > {t("button.cancel", { ns: "common" })} @@ -474,7 +465,7 @@ export default function SetPasswordDialog({ className="flex flex-1" onClick={handleSave} disabled={ - isValidatingOldPassword || + isLoading || !password || password !== confirmPassword || (username && !oldPassword) || @@ -484,7 +475,7 @@ export default function SetPasswordDialog({ !requirements.special } > - {isValidatingOldPassword ? ( + {isLoading ? (
{t("button.saving", { ns: "common" })} diff --git a/web/src/utils/authUtil.ts b/web/src/utils/authUtil.ts deleted file mode 100644 index 06f4ab95e..000000000 --- a/web/src/utils/authUtil.ts +++ /dev/null @@ -1,24 +0,0 @@ -import axios from "axios"; - -/** - * Verifies a user's password without creating a session. - * This is used for password change verification. - * - * @param username - The username to verify - * @param password - The password to verify - * @returns true if credentials are valid, false otherwise - */ -export async function verifyPassword( - username: string, - password: string, -): Promise { - try { - const response = await axios.post("auth/verify", { - user: username, - password, - }); - return response.status === 200; - } catch (error) { - return false; - } -} diff --git a/web/src/views/settings/AuthenticationView.tsx b/web/src/views/settings/AuthenticationView.tsx index e69090a6f..225de37f8 100644 --- a/web/src/views/settings/AuthenticationView.tsx +++ b/web/src/views/settings/AuthenticationView.tsx @@ -37,7 +37,6 @@ import { useTranslation } from "react-i18next"; import DeleteRoleDialog from "@/components/overlay/DeleteRoleDialog"; import { Separator } from "@/components/ui/separator"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; -import { verifyPassword } from "@/utils/authUtil"; type AuthenticationViewProps = { section?: "users" | "roles"; @@ -59,6 +58,7 @@ export default function AuthenticationView({ const [showEditRole, setShowEditRole] = useState(false); const [showDeleteRole, setShowDeleteRole] = useState(false); const [passwordError, setPasswordError] = useState(null); + const [isPasswordLoading, setIsPasswordLoading] = useState(false); const [selectedUser, setSelectedUser] = useState(); const [selectedUserRole, setSelectedUserRole] = useState(); @@ -71,22 +71,16 @@ export default function AuthenticationView({ document.title = t("documentTitle.authentication"); }, [t]); - const onVerifyOldPassword = useCallback( - async (oldPassword: string): Promise => { - if (!selectedUser) return false; - return verifyPassword(selectedUser, oldPassword); - }, - [selectedUser], - ); - const onSavePassword = useCallback( (user: string, password: string, oldPassword?: string) => { + setIsPasswordLoading(true); axios .put(`users/${user}/password`, { password, old_password: oldPassword }) .then((response) => { if (response.status === 200) { setShowSetPassword(false); setPasswordError(null); + setIsPasswordLoading(false); toast.success(t("users.toast.success.updatePassword"), { position: "top-center", }); @@ -98,17 +92,9 @@ export default function AuthenticationView({ error.response?.data?.detail || "Unknown error"; - // Close dialog and show toast for any errors - setShowSetPassword(false); - setPasswordError(null); - toast.error( - t("users.toast.error.setPasswordFailed", { - errorMessage, - }), - { - position: "top-center", - }, - ); + // Keep dialog open and show error + setPasswordError(errorMessage); + setIsPasswordLoading(false); }); }, [t], @@ -582,11 +568,11 @@ export default function AuthenticationView({ setShowSetPassword(false); setPasswordError(null); }} - onVerifyOldPassword={onVerifyOldPassword} initialError={passwordError} onSave={(password, oldPassword) => onSavePassword(selectedUser!, password, oldPassword) } + isLoading={isPasswordLoading} />