mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-11 09:37:37 +03:00
remove password verification endpoint
Just send old_password + new password in one request, let the backend handle verification in a single operation
This commit is contained in:
parent
7bbf287698
commit
f5e52179d4
@ -55,7 +55,6 @@ def require_admin_by_default():
|
|||||||
"/auth",
|
"/auth",
|
||||||
"/auth/first_time_login",
|
"/auth/first_time_login",
|
||||||
"/login",
|
"/login",
|
||||||
"/auth/verify",
|
|
||||||
"/logout",
|
"/logout",
|
||||||
# Authenticated user endpoints (allow_any_authenticated)
|
# Authenticated user endpoints (allow_any_authenticated)
|
||||||
"/profile",
|
"/profile",
|
||||||
@ -753,30 +752,6 @@ def login(request: Request, body: AppPostLoginBody):
|
|||||||
return JSONResponse(content={"message": "Login failed"}, status_code=401)
|
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"]))])
|
@router.get("/users", dependencies=[Depends(require_role(["admin"]))])
|
||||||
def get_users():
|
def get_users():
|
||||||
exports = (
|
exports = (
|
||||||
|
|||||||
@ -30,7 +30,6 @@ import axios from "axios";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import SetPasswordDialog from "../overlay/SetPasswordDialog";
|
import SetPasswordDialog from "../overlay/SetPasswordDialog";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { verifyPassword } from "@/utils/authUtil";
|
|
||||||
|
|
||||||
type AccountSettingsProps = {
|
type AccountSettingsProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -44,19 +43,16 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
|||||||
|
|
||||||
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
||||||
const [passwordError, setPasswordError] = useState<string | null>(null);
|
const [passwordError, setPasswordError] = useState<string | null>(null);
|
||||||
|
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
||||||
|
|
||||||
const Container = isDesktop ? DropdownMenu : Drawer;
|
const Container = isDesktop ? DropdownMenu : Drawer;
|
||||||
const Trigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;
|
const Trigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;
|
||||||
const Content = isDesktop ? DropdownMenuContent : DrawerContent;
|
const Content = isDesktop ? DropdownMenuContent : DrawerContent;
|
||||||
const MenuItem = isDesktop ? DropdownMenuItem : DrawerClose;
|
const MenuItem = isDesktop ? DropdownMenuItem : DrawerClose;
|
||||||
|
|
||||||
const verifyOldPassword = async (oldPassword: string): Promise<boolean> => {
|
|
||||||
if (!profile?.username || profile.username === "anonymous") return false;
|
|
||||||
return verifyPassword(profile.username, oldPassword);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePasswordSave = async (password: string, oldPassword?: string) => {
|
const handlePasswordSave = async (password: string, oldPassword?: string) => {
|
||||||
if (!profile?.username || profile.username === "anonymous") return;
|
if (!profile?.username || profile.username === "anonymous") return;
|
||||||
|
setIsPasswordLoading(true);
|
||||||
axios
|
axios
|
||||||
.put(`users/${profile.username}/password`, {
|
.put(`users/${profile.username}/password`, {
|
||||||
password,
|
password,
|
||||||
@ -66,6 +62,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
|||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
setPasswordDialogOpen(false);
|
setPasswordDialogOpen(false);
|
||||||
setPasswordError(null);
|
setPasswordError(null);
|
||||||
|
setIsPasswordLoading(false);
|
||||||
toast.success(t("users.toast.success.updatePassword"), {
|
toast.success(t("users.toast.success.updatePassword"), {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
@ -77,16 +74,9 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
|||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
|
|
||||||
setPasswordDialogOpen(false);
|
// Keep dialog open and show error
|
||||||
setPasswordError(null);
|
setPasswordError(errorMessage);
|
||||||
toast.error(
|
setIsPasswordLoading(false);
|
||||||
t("users.toast.error.setPasswordFailed", {
|
|
||||||
errorMessage,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
position: "top-center",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -172,9 +162,9 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
|||||||
setPasswordDialogOpen(false);
|
setPasswordDialogOpen(false);
|
||||||
setPasswordError(null);
|
setPasswordError(null);
|
||||||
}}
|
}}
|
||||||
onVerifyOldPassword={verifyOldPassword}
|
|
||||||
initialError={passwordError}
|
initialError={passwordError}
|
||||||
username={profile?.username}
|
username={profile?.username}
|
||||||
|
isLoading={isPasswordLoading}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -66,7 +66,6 @@ import { supportedLanguageKeys } from "@/lib/const";
|
|||||||
|
|
||||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||||
import { MdCategory } from "react-icons/md";
|
import { MdCategory } from "react-icons/md";
|
||||||
import { verifyPassword } from "@/utils/authUtil";
|
|
||||||
|
|
||||||
type GeneralSettingsProps = {
|
type GeneralSettingsProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -118,14 +117,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
const Portal = isDesktop ? DropdownMenuPortal : DialogPortal;
|
const Portal = isDesktop ? DropdownMenuPortal : DialogPortal;
|
||||||
|
|
||||||
const [passwordError, setPasswordError] = useState<string | null>(null);
|
const [passwordError, setPasswordError] = useState<string | null>(null);
|
||||||
|
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
||||||
const verifyOldPassword = async (oldPassword: string): Promise<boolean> => {
|
|
||||||
if (!profile?.username || profile.username === "anonymous") return false;
|
|
||||||
return verifyPassword(profile.username, oldPassword);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePasswordSave = async (password: string, oldPassword?: string) => {
|
const handlePasswordSave = async (password: string, oldPassword?: string) => {
|
||||||
if (!profile?.username || profile.username === "anonymous") return;
|
if (!profile?.username || profile.username === "anonymous") return;
|
||||||
|
setIsPasswordLoading(true);
|
||||||
axios
|
axios
|
||||||
.put(`users/${profile.username}/password`, {
|
.put(`users/${profile.username}/password`, {
|
||||||
password,
|
password,
|
||||||
@ -135,6 +131,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
setPasswordDialogOpen(false);
|
setPasswordDialogOpen(false);
|
||||||
setPasswordError(null);
|
setPasswordError(null);
|
||||||
|
setIsPasswordLoading(false);
|
||||||
toast.success(
|
toast.success(
|
||||||
t("users.toast.success.updatePassword", {
|
t("users.toast.success.updatePassword", {
|
||||||
ns: "views/settings",
|
ns: "views/settings",
|
||||||
@ -151,17 +148,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
|
|
||||||
setPasswordDialogOpen(false);
|
// Keep dialog open and show error
|
||||||
setPasswordError(null);
|
setPasswordError(errorMessage);
|
||||||
toast.error(
|
setIsPasswordLoading(false);
|
||||||
t("users.toast.error.setPasswordFailed", {
|
|
||||||
ns: "views/settings",
|
|
||||||
errorMessage,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
position: "top-center",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -573,9 +562,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
setPasswordDialogOpen(false);
|
setPasswordDialogOpen(false);
|
||||||
setPasswordError(null);
|
setPasswordError(null);
|
||||||
}}
|
}}
|
||||||
onVerifyOldPassword={verifyOldPassword}
|
|
||||||
initialError={passwordError}
|
initialError={passwordError}
|
||||||
username={profile?.username}
|
username={profile?.username}
|
||||||
|
isLoading={isPasswordLoading}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -22,18 +22,18 @@ type SetPasswordProps = {
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
onSave: (password: string, oldPassword?: string) => void;
|
onSave: (password: string, oldPassword?: string) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onVerifyOldPassword?: (oldPassword: string) => Promise<boolean>;
|
|
||||||
initialError?: string | null;
|
initialError?: string | null;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SetPasswordDialog({
|
export default function SetPasswordDialog({
|
||||||
show,
|
show,
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
onVerifyOldPassword,
|
|
||||||
initialError,
|
initialError,
|
||||||
username,
|
username,
|
||||||
|
isLoading = false,
|
||||||
}: SetPasswordProps) {
|
}: SetPasswordProps) {
|
||||||
const { t } = useTranslation(["views/settings", "common"]);
|
const { t } = useTranslation(["views/settings", "common"]);
|
||||||
const { getLocaleDocUrl } = useDocDomain();
|
const { getLocaleDocUrl } = useDocDomain();
|
||||||
@ -49,8 +49,6 @@ export default function SetPasswordDialog({
|
|||||||
const [confirmPassword, setConfirmPassword] = useState<string>("");
|
const [confirmPassword, setConfirmPassword] = useState<string>("");
|
||||||
const [passwordStrength, setPasswordStrength] = useState<number>(0);
|
const [passwordStrength, setPasswordStrength] = useState<number>(0);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isValidatingOldPassword, setIsValidatingOldPassword] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
// visibility toggles for password fields
|
// visibility toggles for password fields
|
||||||
const [showOldPassword, setShowOldPassword] = useState<boolean>(false);
|
const [showOldPassword, setShowOldPassword] = useState<boolean>(false);
|
||||||
@ -58,6 +56,7 @@ export default function SetPasswordDialog({
|
|||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
const [showConfirmPassword, setShowConfirmPassword] =
|
const [showConfirmPassword, setShowConfirmPassword] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
|
const [hasInitialized, setHasInitialized] = useState<boolean>(false);
|
||||||
|
|
||||||
// Password strength requirements
|
// Password strength requirements
|
||||||
|
|
||||||
@ -68,14 +67,23 @@ export default function SetPasswordDialog({
|
|||||||
special: /[!@#$%^&*(),.?":{}|<>]/.test(password),
|
special: /[!@#$%^&*(),.?":{}|<>]/.test(password),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset state when dialog opens/closes
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show) {
|
if (show) {
|
||||||
|
if (!hasInitialized) {
|
||||||
setOldPassword("");
|
setOldPassword("");
|
||||||
setPassword("");
|
setPassword("");
|
||||||
setConfirmPassword("");
|
setConfirmPassword("");
|
||||||
setError(initialError || null);
|
setError(null);
|
||||||
|
setHasInitialized(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasInitialized(false);
|
||||||
|
}
|
||||||
|
}, [show, hasInitialized]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show && initialError) {
|
||||||
|
setError(initialError);
|
||||||
}
|
}
|
||||||
}, [show, initialError]);
|
}, [show, initialError]);
|
||||||
|
|
||||||
@ -133,24 +141,6 @@ export default function SetPasswordDialog({
|
|||||||
return;
|
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);
|
onSave(password, oldPassword || undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -465,6 +455,7 @@ export default function SetPasswordDialog({
|
|||||||
aria-label={t("button.cancel", { ns: "common" })}
|
aria-label={t("button.cancel", { ns: "common" })}
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{t("button.cancel", { ns: "common" })}
|
{t("button.cancel", { ns: "common" })}
|
||||||
</Button>
|
</Button>
|
||||||
@ -474,7 +465,7 @@ export default function SetPasswordDialog({
|
|||||||
className="flex flex-1"
|
className="flex flex-1"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={
|
disabled={
|
||||||
isValidatingOldPassword ||
|
isLoading ||
|
||||||
!password ||
|
!password ||
|
||||||
password !== confirmPassword ||
|
password !== confirmPassword ||
|
||||||
(username && !oldPassword) ||
|
(username && !oldPassword) ||
|
||||||
@ -484,7 +475,7 @@ export default function SetPasswordDialog({
|
|||||||
!requirements.special
|
!requirements.special
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isValidatingOldPassword ? (
|
{isLoading ? (
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
<span>{t("button.saving", { ns: "common" })}</span>
|
<span>{t("button.saving", { ns: "common" })}</span>
|
||||||
|
|||||||
@ -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<boolean> {
|
|
||||||
try {
|
|
||||||
const response = await axios.post("auth/verify", {
|
|
||||||
user: username,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
return response.status === 200;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -37,7 +37,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import DeleteRoleDialog from "@/components/overlay/DeleteRoleDialog";
|
import DeleteRoleDialog from "@/components/overlay/DeleteRoleDialog";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
||||||
import { verifyPassword } from "@/utils/authUtil";
|
|
||||||
|
|
||||||
type AuthenticationViewProps = {
|
type AuthenticationViewProps = {
|
||||||
section?: "users" | "roles";
|
section?: "users" | "roles";
|
||||||
@ -59,6 +58,7 @@ export default function AuthenticationView({
|
|||||||
const [showEditRole, setShowEditRole] = useState(false);
|
const [showEditRole, setShowEditRole] = useState(false);
|
||||||
const [showDeleteRole, setShowDeleteRole] = useState(false);
|
const [showDeleteRole, setShowDeleteRole] = useState(false);
|
||||||
const [passwordError, setPasswordError] = useState<string | null>(null);
|
const [passwordError, setPasswordError] = useState<string | null>(null);
|
||||||
|
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
||||||
|
|
||||||
const [selectedUser, setSelectedUser] = useState<string>();
|
const [selectedUser, setSelectedUser] = useState<string>();
|
||||||
const [selectedUserRole, setSelectedUserRole] = useState<string>();
|
const [selectedUserRole, setSelectedUserRole] = useState<string>();
|
||||||
@ -71,22 +71,16 @@ export default function AuthenticationView({
|
|||||||
document.title = t("documentTitle.authentication");
|
document.title = t("documentTitle.authentication");
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const onVerifyOldPassword = useCallback(
|
|
||||||
async (oldPassword: string): Promise<boolean> => {
|
|
||||||
if (!selectedUser) return false;
|
|
||||||
return verifyPassword(selectedUser, oldPassword);
|
|
||||||
},
|
|
||||||
[selectedUser],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSavePassword = useCallback(
|
const onSavePassword = useCallback(
|
||||||
(user: string, password: string, oldPassword?: string) => {
|
(user: string, password: string, oldPassword?: string) => {
|
||||||
|
setIsPasswordLoading(true);
|
||||||
axios
|
axios
|
||||||
.put(`users/${user}/password`, { password, old_password: oldPassword })
|
.put(`users/${user}/password`, { password, old_password: oldPassword })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
setShowSetPassword(false);
|
setShowSetPassword(false);
|
||||||
setPasswordError(null);
|
setPasswordError(null);
|
||||||
|
setIsPasswordLoading(false);
|
||||||
toast.success(t("users.toast.success.updatePassword"), {
|
toast.success(t("users.toast.success.updatePassword"), {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
@ -98,17 +92,9 @@ export default function AuthenticationView({
|
|||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
|
|
||||||
// Close dialog and show toast for any errors
|
// Keep dialog open and show error
|
||||||
setShowSetPassword(false);
|
setPasswordError(errorMessage);
|
||||||
setPasswordError(null);
|
setIsPasswordLoading(false);
|
||||||
toast.error(
|
|
||||||
t("users.toast.error.setPasswordFailed", {
|
|
||||||
errorMessage,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
position: "top-center",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[t],
|
[t],
|
||||||
@ -582,11 +568,11 @@ export default function AuthenticationView({
|
|||||||
setShowSetPassword(false);
|
setShowSetPassword(false);
|
||||||
setPasswordError(null);
|
setPasswordError(null);
|
||||||
}}
|
}}
|
||||||
onVerifyOldPassword={onVerifyOldPassword}
|
|
||||||
initialError={passwordError}
|
initialError={passwordError}
|
||||||
onSave={(password, oldPassword) =>
|
onSave={(password, oldPassword) =>
|
||||||
onSavePassword(selectedUser!, password, oldPassword)
|
onSavePassword(selectedUser!, password, oldPassword)
|
||||||
}
|
}
|
||||||
|
isLoading={isPasswordLoading}
|
||||||
/>
|
/>
|
||||||
<DeleteUserDialog
|
<DeleteUserDialog
|
||||||
show={showDelete}
|
show={showDelete}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user