fix: Password reset issues, ODIC admin auth not filling, and electron x64 build issues

This commit is contained in:
LukeGus
2025-10-30 17:12:10 -05:00
parent d86c404972
commit cf431e59ac
11 changed files with 346 additions and 316 deletions

View File

@@ -34,7 +34,7 @@ import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useConfirmation } from "@/hooks/use-confirmation.ts";
import {
getOIDCConfig,
getAdminOIDCConfig,
getRegistrationAllowed,
getPasswordLoginAllowed,
getUserList,
@@ -125,7 +125,7 @@ export function AdminSettings({
}
}
getOIDCConfig()
getAdminOIDCConfig()
.then((res) => {
if (res) setOidcConfig(res);
})

View File

@@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button.tsx";
import { Input } from "@/components/ui/input.tsx";
import { PasswordInput } from "@/components/ui/password-input.tsx";
import { Label } from "@/components/ui/label.tsx";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
import { useTranslation } from "react-i18next";
import { LanguageSwitcher } from "@/ui/Desktop/User/LanguageSwitcher.tsx";
import { toast } from "sonner";
@@ -858,6 +859,12 @@ export function Auth({
<>
{resetStep === "initiate" && (
<>
<Alert variant="destructive" className="mb-4">
<AlertTitle>{t("common.warning")}</AlertTitle>
<AlertDescription>
{t("auth.dataLossWarning")}
</AlertDescription>
</Alert>
<div className="text-center text-muted-foreground mb-4">
<p>{t("auth.resetCodeDesc")}</p>
</div>

View File

@@ -7,13 +7,8 @@ import {
} from "@/components/ui/card.tsx";
import { Key } from "lucide-react";
import React, { useState } from "react";
import {
completePasswordReset,
initiatePasswordReset,
verifyPasswordResetCode,
} from "@/ui/main-axios.ts";
import { changePassword } from "@/ui/main-axios.ts";
import { Label } from "@/components/ui/label.tsx";
import { Input } from "@/components/ui/input.tsx";
import { PasswordInput } from "@/components/ui/password-input.tsx";
import { Button } from "@/components/ui/button.tsx";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
@@ -31,98 +26,42 @@ interface PasswordResetProps {
export function PasswordReset({ userInfo }: PasswordResetProps) {
const [error, setError] = useState<string | null>(null);
const [resetStep, setResetStep] = useState<
"initiate" | "verify" | "newPassword"
>("initiate");
const [resetCode, setResetCode] = useState("");
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [tempToken, setTempToken] = useState("");
const [resetLoading, setResetLoading] = useState(false);
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
async function handleInitiatePasswordReset() {
async function handleChangePassword() {
setError(null);
setResetLoading(true);
try {
await initiatePasswordReset(userInfo.username);
setResetStep("verify");
setError(null);
} catch (err: unknown) {
const error = err as {
message?: string;
response?: { data?: { error?: string } };
};
setError(
error?.response?.data?.error ||
error?.message ||
t("common.failedToInitiatePasswordReset"),
);
} finally {
setResetLoading(false);
if (!currentPassword || !newPassword || !confirmPassword) {
setError(t("errors.requiredField"));
return;
}
}
function resetPasswordState() {
setResetStep("initiate");
setResetCode("");
setNewPassword("");
setConfirmPassword("");
setTempToken("");
setError(null);
}
async function handleVerifyResetCode() {
setError(null);
setResetLoading(true);
try {
const response = await verifyPasswordResetCode(
userInfo.username,
resetCode,
);
setTempToken(response.tempToken);
setResetStep("newPassword");
setError(null);
} catch (err: unknown) {
const error = err as { response?: { data?: { error?: string } } };
setError(
error?.response?.data?.error || t("common.failedToVerifyResetCode"),
);
} finally {
setResetLoading(false);
}
}
async function handleCompletePasswordReset() {
setError(null);
setResetLoading(true);
if (newPassword !== confirmPassword) {
setError(t("common.passwordsDoNotMatch"));
setResetLoading(false);
return;
}
if (newPassword.length < 6) {
setError(t("common.passwordMinLength"));
setResetLoading(false);
return;
}
setLoading(true);
try {
await completePasswordReset(userInfo.username, tempToken, newPassword);
toast.success(t("common.passwordResetSuccess"));
resetPasswordState();
await changePassword(currentPassword, newPassword);
toast.success(t("profile.passwordChangedSuccess"));
window.location.reload();
} catch (err: unknown) {
const error = err as { response?: { data?: { error?: string } } };
setError(
error?.response?.data?.error ||
t("common.failedToCompletePasswordReset"),
error?.response?.data?.error || t("profile.failedToChangePassword"),
);
} finally {
setResetLoading(false);
setLoading(false);
}
}
@@ -158,147 +97,64 @@ export function PasswordReset({ userInfo }: PasswordResetProps) {
<CardDescription>{t("common.changeAccountPassword")}</CardDescription>
</CardHeader>
<CardContent>
<>
{resetStep === "initiate" && (
<>
<Alert variant="destructive" className="mb-4">
<AlertTitle>Warning: Data Loss</AlertTitle>
<AlertDescription>
Resetting your password will delete all your saved SSH hosts,
credentials, and encrypted data. This action cannot be undone.
Only use this if you have forgotten your password.
</AlertDescription>
</Alert>
<div className="flex flex-col gap-4">
<Button
type="button"
className="w-full h-11 text-base"
disabled={resetLoading || !userInfo.username.trim()}
onClick={handleInitiatePasswordReset}
>
{resetLoading ? Spinner : t("common.sendResetCode")}
</Button>
</div>
</>
)}
{resetStep === "verify" && (
<>
<div className="text-center text-muted-foreground mb-4">
<p>
{t("common.enterSixDigitCode")}{" "}
<strong>{userInfo.username}</strong>
</p>
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="reset-code">{t("common.resetCode")}</Label>
<Input
id="reset-code"
type="text"
required
maxLength={6}
className="h-11 text-base text-center text-lg tracking-widest"
value={resetCode}
onChange={(e) =>
setResetCode(e.target.value.replace(/\D/g, ""))
}
disabled={resetLoading}
placeholder={t("placeholders.enterCode")}
/>
</div>
<Button
type="button"
className="w-full h-11 text-base font-semibold"
disabled={resetLoading || resetCode.length !== 6}
onClick={handleVerifyResetCode}
>
{resetLoading ? Spinner : t("common.verifyCode")}
</Button>
<Button
type="button"
variant="outline"
className="w-full h-11 text-base font-semibold"
disabled={resetLoading}
onClick={() => {
setResetStep("initiate");
setResetCode("");
}}
>
Back
</Button>
</div>
</>
)}
{resetStep === "newPassword" && (
<>
<div className="text-center text-muted-foreground mb-4">
<p>
{t("common.enterNewPassword")}{" "}
<strong>{userInfo.username}</strong>
</p>
</div>
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-2">
<Label htmlFor="new-password">
{t("common.newPassword")}
</Label>
<PasswordInput
id="new-password"
required
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
disabled={resetLoading}
autoComplete="new-password"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="confirm-password">
{t("common.confirmPassword")}
</Label>
<PasswordInput
id="confirm-password"
required
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
disabled={resetLoading}
autoComplete="new-password"
/>
</div>
<Button
type="button"
className="w-full h-11 text-base font-semibold"
disabled={resetLoading || !newPassword || !confirmPassword}
onClick={handleCompletePasswordReset}
>
{resetLoading ? Spinner : t("common.resetPassword")}
</Button>
<Button
type="button"
variant="outline"
className="w-full h-11 text-base font-semibold"
disabled={resetLoading}
onClick={() => {
setResetStep("verify");
setNewPassword("");
setConfirmPassword("");
}}
>
Back
</Button>
</div>
</>
)}
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-2">
<Label htmlFor="current-password">
{t("profile.currentPassword")}
</Label>
<PasswordInput
id="current-password"
required
className="h-11 text-base"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
disabled={loading}
autoComplete="current-password"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="new-password">{t("common.newPassword")}</Label>
<PasswordInput
id="new-password"
required
className="h-11 text-base"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
disabled={loading}
autoComplete="new-password"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="confirm-password">
{t("common.confirmPassword")}
</Label>
<PasswordInput
id="confirm-password"
required
className="h-11 text-base"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
disabled={loading}
autoComplete="new-password"
/>
</div>
<Button
type="button"
className="w-full h-11 text-base font-semibold mt-2"
disabled={
loading || !currentPassword || !newPassword || !confirmPassword
}
onClick={handleChangePassword}
>
{loading ? Spinner : t("profile.changePassword")}
</Button>
{error && (
<Alert variant="destructive" className="mt-4">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
</>
</div>
</CardContent>
</Card>
);

View File

@@ -740,6 +740,12 @@ export function Auth({
<>
{resetStep === "initiate" && (
<>
<Alert variant="destructive" className="mb-4">
<AlertTitle>{t("common.warning")}</AlertTitle>
<AlertDescription>
{t("auth.dataLossWarning")}
</AlertDescription>
</Alert>
<div className="text-center text-muted-foreground mb-4">
<p>{t("auth.resetCodeDesc")}</p>
</div>

View File

@@ -1755,6 +1755,15 @@ export async function getOIDCConfig(): Promise<Record<string, unknown>> {
}
}
export async function getAdminOIDCConfig(): Promise<Record<string, unknown>> {
try {
const response = await authApi.get("/users/oidc-config/admin");
return response.data;
} catch (error) {
handleApiError(error, "fetch admin OIDC config");
}
}
export async function getSetupRequired(): Promise<{ setup_required: boolean }> {
try {
const response = await authApi.get("/users/setup-required");
@@ -1816,6 +1825,18 @@ export async function completePasswordReset(
}
}
export async function changePassword(oldPassword: string, newPassword: string) {
try {
const response = await authApi.post("/users/change-password", {
oldPassword,
newPassword,
});
return response.data;
} catch (error) {
handleApiError(error, "change password");
}
}
export async function getOIDCAuthorizeUrl(): Promise<OIDCAuthorize> {
try {
const response = await authApi.get("/users/oidc/authorize");