This commit is contained in:
LukeGus
2026-05-28 22:29:20 -05:00
parent 33dcde0827
commit 5777351145
238 changed files with 62301 additions and 98953 deletions
File diff suppressed because it is too large Load Diff
+30 -91
View File
@@ -4,7 +4,6 @@ import { VersionAlert } from "@/components/version-alert.tsx";
import { useTranslation } from "react-i18next";
import { isElectron } from "@/lib/electron";
import { checkElectronUpdate } from "@/main-axios.ts";
import { useTheme } from "@/components/theme-provider";
interface VersionCheckModalProps {
onContinue: () => void;
@@ -18,7 +17,6 @@ type ElectronWindow = Window & {
export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) {
const { t } = useTranslation();
const { theme } = useTheme();
const [versionInfo, setVersionInfo] = useState<Record<
string,
unknown
@@ -26,15 +24,6 @@ export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) {
const [versionChecking, setVersionChecking] = useState(false);
const [versionDismissed] = useState(false);
const isDarkMode =
theme === "dark" ||
theme === "dracula" ||
theme === "gentlemansChoice" ||
theme === "midnightEspresso" ||
theme === "catppuccinMocha" ||
(theme === "system" &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
const lineColor = isDarkMode ? "#151517" : "#f9f9f9";
const versionModalTitle =
versionInfo?.status === "beta"
? t("versionCheck.betaVersion")
@@ -112,24 +101,10 @@ export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) {
if (versionChecking && !versionInfo) {
return (
<div
className="fixed inset-0 flex items-center justify-center z-50"
style={{
background: "var(--bg-elevated)",
backgroundImage: `repeating-linear-gradient(
45deg,
transparent,
transparent 35px,
${lineColor} 35px,
${lineColor} 37px
)`,
}}
>
<div className="w-[420px] max-w-full p-8 flex flex-col backdrop-blur-sm bg-card/50 rounded-2xl shadow-xl border-2 border-edge overflow-y-auto thin-scrollbar my-2 animate-in fade-in zoom-in-95 duration-300">
<div className="flex items-center justify-center mb-4">
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
<p className="text-center text-muted-foreground">
<div className="fixed inset-0 flex items-center justify-center bg-background p-6 z-50">
<div className="flex flex-col gap-5 p-6 border border-border bg-card max-w-md w-full items-center">
<div className="w-5 h-5 border-2 border-primary border-t-transparent rounded-full animate-spin" />
<p className="text-sm text-muted-foreground">
{t("versionCheck.checkingUpdates")}
</p>
</div>
@@ -139,76 +114,40 @@ export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) {
if (!versionInfo || versionDismissed) {
return (
<div
className="fixed inset-0 flex items-center justify-center z-50"
style={{
background: "var(--bg-elevated)",
backgroundImage: `repeating-linear-gradient(
45deg,
transparent,
transparent 35px,
${lineColor} 35px,
${lineColor} 37px
)`,
}}
>
<div className="w-[420px] max-w-full p-8 flex flex-col backdrop-blur-sm bg-card/50 rounded-2xl shadow-xl border-2 border-edge overflow-y-auto thin-scrollbar my-2 animate-in fade-in zoom-in-95 duration-300">
<div className="mb-4">
<h2 className="text-lg font-semibold">
{t("versionCheck.checkUpdates")}
</h2>
</div>
<div className="fixed inset-0 flex items-center justify-center bg-background p-6 z-50">
<div className="flex flex-col gap-5 p-6 border border-border bg-card max-w-md w-full">
<p className="font-bold">{t("versionCheck.checkUpdates")}</p>
{versionInfo && !versionDismissed && (
<div className="mb-4">
<VersionAlert
updateInfo={versionInfo}
onDownload={handleDownloadUpdate}
/>
</div>
<VersionAlert
updateInfo={versionInfo}
onDownload={handleDownloadUpdate}
/>
)}
<div className="flex gap-2">
<Button onClick={handleContinue} className="flex-1 h-10">
{t("common.continue")}
</Button>
</div>
<Button
onClick={handleContinue}
className="w-full bg-accent-brand hover:bg-accent-brand/90 text-background font-bold"
>
{t("common.continue")}
</Button>
</div>
</div>
);
}
return (
<div
className="fixed inset-0 flex items-center justify-center z-50"
style={{
background: "var(--bg-elevated)",
backgroundImage: `repeating-linear-gradient(
45deg,
transparent,
transparent 35px,
${lineColor} 35px,
${lineColor} 37px
)`,
}}
>
<div className="w-[420px] max-w-full p-8 flex flex-col backdrop-blur-sm bg-card/50 rounded-2xl shadow-xl border-2 border-edge overflow-y-auto thin-scrollbar my-2 animate-in fade-in zoom-in-95 duration-300">
<div className="mb-4">
<h2 className="text-lg font-semibold">{versionModalTitle}</h2>
</div>
<div className="mb-4">
<VersionAlert
updateInfo={versionInfo}
onDownload={handleDownloadUpdate}
/>
</div>
<div className="flex gap-2">
<Button onClick={handleContinue} className="flex-1 h-10">
{t("common.continue")}
</Button>
</div>
<div className="fixed inset-0 flex items-center justify-center bg-background p-6 z-50">
<div className="flex flex-col gap-5 p-6 border border-border bg-card max-w-md w-full">
<p className="font-bold">{versionModalTitle}</p>
<VersionAlert
updateInfo={versionInfo}
onDownload={handleDownloadUpdate}
/>
<Button
onClick={handleContinue}
className="w-full bg-accent-brand hover:bg-accent-brand/90 text-background font-bold"
>
{t("common.continue")}
</Button>
</div>
</div>
);
-152
View File
@@ -1,152 +0,0 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/card.tsx";
import { Key } from "lucide-react";
import React, { useState } from "react";
import { changePassword } from "@/main-axios.ts";
import { Label } from "@/components/label.tsx";
import { PasswordInput } from "@/components/password-input.tsx";
import { Button } from "@/components/button.tsx";
import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
export function PasswordReset() {
const [error, setError] = useState<string | null>(null);
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
async function handleChangePassword() {
setError(null);
if (!currentPassword || !newPassword || !confirmPassword) {
setError(t("errors.requiredField"));
return;
}
if (newPassword !== confirmPassword) {
setError(t("common.passwordsDoNotMatch"));
return;
}
if (newPassword.length < 6) {
setError(t("common.passwordMinLength"));
return;
}
setLoading(true);
try {
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("profile.failedToChangePassword"),
);
} finally {
setLoading(false);
}
}
const Spinner = (
<svg
className="animate-spin mr-2 h-4 w-4 text-foreground inline-block"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
/>
</svg>
);
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Key className="w-5 h-5" />
{t("common.password")}
</CardTitle>
<CardDescription>{t("common.changeAccountPassword")}</CardDescription>
</CardHeader>
<CardContent>
<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>
);
}
-479
View File
@@ -1,479 +0,0 @@
import React, { useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/card.tsx";
import { Button } from "@/components/button.tsx";
import { Input } from "@/components/input.tsx";
import { PasswordInput } from "@/components/password-input.tsx";
import { Label } from "@/components/label.tsx";
import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/tabs.tsx";
import {
Shield,
Copy,
Download,
AlertCircle,
CheckCircle2,
} from "lucide-react";
import {
setupTOTP,
enableTOTP,
disableTOTP,
generateBackupCodes,
} from "@/main-axios.ts";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
interface TOTPSetupProps {
isEnabled: boolean;
onStatusChange?: (enabled: boolean) => void;
}
export function TOTPSetup({
isEnabled: initialEnabled,
onStatusChange,
}: TOTPSetupProps) {
const { t } = useTranslation();
const [isEnabled, setIsEnabled] = useState(initialEnabled);
const [isSettingUp, setIsSettingUp] = useState(false);
const [setupStep, setSetupStep] = useState<
"init" | "qr" | "verify" | "backup"
>("init");
const [qrCode, setQrCode] = useState("");
const [secret, setSecret] = useState("");
const [verificationCode, setVerificationCode] = useState("");
const [backupCodes, setBackupCodes] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [password, setPassword] = useState("");
const [disableCode, setDisableCode] = useState("");
const handleSetupStart = async () => {
setError(null);
setLoading(true);
try {
const response = await setupTOTP();
setQrCode(response.qr_code);
setSecret(response.secret);
setSetupStep("qr");
setIsSettingUp(true);
} catch (err: unknown) {
const error = err as { response?: { data?: { error?: string } } };
setError(error?.response?.data?.error || "Failed to start TOTP setup");
} finally {
setLoading(false);
}
};
const handleVerifyCode = async () => {
if (verificationCode.length !== 6) {
setError("Please enter a 6-digit code");
return;
}
setError(null);
setLoading(true);
try {
const response = await enableTOTP(verificationCode);
setBackupCodes(response.backup_codes);
setSetupStep("backup");
toast.success(t("auth.twoFactorEnabledSuccess"));
} catch (err: unknown) {
const error = err as { response?: { data?: { error?: string } } };
setError(error?.response?.data?.error || "Invalid verification code");
} finally {
setLoading(false);
}
};
const handleDisable = async () => {
setError(null);
setLoading(true);
try {
await disableTOTP(password || undefined, disableCode || undefined);
setIsEnabled(false);
setIsSettingUp(false);
setSetupStep("init");
setPassword("");
setDisableCode("");
onStatusChange?.(false);
toast.success(t("auth.twoFactorDisabled"));
} catch (err: unknown) {
const error = err as { response?: { data?: { error?: string } } };
setError(error?.response?.data?.error || "Failed to disable TOTP");
} finally {
setLoading(false);
}
};
const handleGenerateNewBackupCodes = async () => {
setError(null);
setLoading(true);
try {
const response = await generateBackupCodes(
password || undefined,
disableCode || undefined,
);
setBackupCodes(response.backup_codes);
toast.success(t("auth.newBackupCodesGenerated"));
} catch (err: unknown) {
const error = err as { response?: { data?: { error?: string } } };
setError(
error?.response?.data?.error || "Failed to generate backup codes",
);
} finally {
setLoading(false);
}
};
const copyToClipboard = (text: string, label: string) => {
navigator.clipboard.writeText(text);
toast.success(t("messages.copiedToClipboard", { item: label }));
};
const downloadBackupCodes = () => {
const content =
`Termix Two-Factor Authentication Backup Codes\n` +
`Generated: ${new Date().toISOString()}\n\n` +
`Keep these codes in a safe place. Each code can only be used once.\n\n` +
backupCodes.map((code, i) => `${i + 1}. ${code}`).join("\n");
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "termix-backup-codes.txt";
a.click();
URL.revokeObjectURL(url);
toast.success(t("auth.backupCodesDownloaded"));
};
const handleComplete = () => {
setIsEnabled(true);
setIsSettingUp(false);
setSetupStep("init");
setVerificationCode("");
onStatusChange?.(true);
};
if (isEnabled && !isSettingUp) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="w-5 h-5" />
{t("auth.twoFactorTitle")}
</CardTitle>
<CardDescription>{t("auth.twoFactorProtected")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert>
<CheckCircle2 className="h-4 w-4" />
<AlertTitle>{t("common.enabled")}</AlertTitle>
<AlertDescription>{t("auth.twoFactorActive")}</AlertDescription>
</Alert>
<Tabs defaultValue="disable" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="disable">{t("auth.disable2FA")}</TabsTrigger>
<TabsTrigger value="backup">{t("auth.backupCodes")}</TabsTrigger>
</TabsList>
<TabsContent value="disable" className="space-y-4">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>{t("common.warning")}</AlertTitle>
<AlertDescription>
{t("auth.disableTwoFactorWarning")}
</AlertDescription>
</Alert>
<div className="space-y-2">
<Label htmlFor="disable-password">
{t("auth.passwordOrTotpCode")}
</Label>
<PasswordInput
id="disable-password"
placeholder={t("placeholders.enterPassword")}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<p className="text-sm text-muted-foreground">{t("auth.or")}</p>
<Input
id="disable-code"
type="text"
placeholder={t("placeholders.totpCode")}
maxLength={6}
value={disableCode}
onChange={(e) =>
setDisableCode(e.target.value.replace(/\D/g, ""))
}
/>
</div>
<Button
variant="destructive"
onClick={handleDisable}
disabled={loading || (!password && !disableCode)}
>
{t("auth.disableTwoFactor")}
</Button>
</TabsContent>
<TabsContent value="backup" className="space-y-4">
<p className="text-sm text-muted-foreground">
{t("auth.generateNewBackupCodesText")}
</p>
<div className="space-y-2">
<Label htmlFor="backup-password">
{t("auth.passwordOrTotpCode")}
</Label>
<PasswordInput
id="backup-password"
placeholder={t("placeholders.enterPassword")}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<p className="text-sm text-muted-foreground">{t("auth.or")}</p>
<Input
id="backup-code"
type="text"
placeholder={t("placeholders.totpCode")}
maxLength={6}
value={disableCode}
onChange={(e) =>
setDisableCode(e.target.value.replace(/\D/g, ""))
}
/>
</div>
<Button
onClick={handleGenerateNewBackupCodes}
disabled={loading || (!password && !disableCode)}
>
{t("auth.generateNewBackupCodes")}
</Button>
{backupCodes.length > 0 && (
<div className="space-y-2 mt-4">
<div className="flex justify-between items-center">
<Label>{t("auth.yourBackupCodes")}</Label>
<Button
size="sm"
variant="outline"
onClick={downloadBackupCodes}
>
<Download className="w-4 h-4 mr-2" />
{t("auth.download")}
</Button>
</div>
<div className="grid grid-cols-2 gap-2 p-4 bg-muted rounded-lg font-mono text-sm">
{backupCodes.map((code, i) => (
<div key={i}>{code}</div>
))}
</div>
</div>
)}
</TabsContent>
</Tabs>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>{t("common.error")}</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
);
}
if (setupStep === "qr") {
return (
<Card>
<CardHeader>
<CardTitle>{t("auth.setupTwoFactorTitle")}</CardTitle>
<CardDescription>{t("auth.step1ScanQR")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-center">
<img src={qrCode} alt="TOTP QR Code" className="w-64 h-64" />
</div>
<div className="space-y-2">
<Label>{t("auth.manualEntryCode")}</Label>
<div className="flex gap-2">
<Input value={secret} readOnly className="font-mono text-sm" />
<Button
size="default"
variant="outline"
onClick={() => copyToClipboard(secret, "Secret key")}
>
<Copy className="w-4 h-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">
{t("auth.cannotScanQRText")}
</p>
</div>
<Button onClick={() => setSetupStep("verify")} className="w-full">
{t("auth.nextVerifyCode")}
</Button>
</CardContent>
</Card>
);
}
if (setupStep === "verify") {
return (
<Card>
<CardHeader>
<CardTitle>{t("auth.verifyAuthenticator")}</CardTitle>
<CardDescription>{t("auth.step2EnterCode")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="verify-code">{t("auth.verificationCode")}</Label>
<Input
id="verify-code"
type="text"
placeholder="000000"
maxLength={6}
value={verificationCode}
onChange={(e) =>
setVerificationCode(e.target.value.replace(/\D/g, ""))
}
className="text-center text-2xl tracking-widest font-mono"
/>
</div>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>{t("common.error")}</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setSetupStep("qr")}
disabled={loading}
>
{t("auth.back")}
</Button>
<Button
onClick={handleVerifyCode}
disabled={loading || verificationCode.length !== 6}
className="flex-1"
>
{loading ? t("interface.verifying") : t("auth.verifyAndEnable")}
</Button>
</div>
</CardContent>
</Card>
);
}
if (setupStep === "backup") {
return (
<Card>
<CardHeader>
<CardTitle>{t("auth.saveBackupCodesTitle")}</CardTitle>
<CardDescription>{t("auth.step3StoreCodesSecurely")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertTitle>{t("common.important")}</AlertTitle>
<AlertDescription>
{t("auth.importantBackupCodesText")}
</AlertDescription>
</Alert>
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label>Your Backup Codes</Label>
<Button size="sm" variant="outline" onClick={downloadBackupCodes}>
<Download className="w-4 h-4 mr-2" />
Download
</Button>
</div>
<div className="grid grid-cols-2 gap-2 p-4 bg-muted rounded-lg font-mono text-sm">
{backupCodes.map((code, i) => (
<div key={i} className="flex items-center gap-2">
<span className="text-muted-foreground">{i + 1}.</span>
<span>{code}</span>
</div>
))}
</div>
</div>
<Button onClick={handleComplete} className="w-full">
{t("auth.completeSetup")}
</Button>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="w-5 h-5" />
{t("auth.twoFactorTitle")}
</CardTitle>
<CardDescription className="space-y-2">
<p>{t("auth.addExtraSecurityLayer")}.</p>
<Button
variant="outline"
size="sm"
className="h-8 px-3 text-xs"
onClick={() =>
window.open("https://docs.termix.site/totp", "_blank")
}
>
{t("common.documentation")}
</Button>
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertTitle>{t("common.notEnabled")}</AlertTitle>
<AlertDescription>{t("auth.notEnabledText")}</AlertDescription>
</Alert>
<Button
onClick={handleSetupStart}
disabled={loading}
className="w-full h-11 text-base"
>
{loading ? t("common.settingUp") : t("auth.enableTwoFactorButton")}
</Button>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
);
}
File diff suppressed because it is too large Load Diff