Add setting to change network unit

This commit is contained in:
riri-314
2025-11-18 10:58:06 +01:00
parent 7d49d4f948
commit f2316fdd3a

View File

@@ -1,10 +1,16 @@
"use client" "use client";
import { useState, useEffect } from "react" import { useState, useEffect } from "react";
import { Button } from "./ui/button" import { Button } from "./ui/button";
import { Input } from "./ui/input" import { Input } from "./ui/input";
import { Label } from "./ui/label" import { Label } from "./ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card" import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "./ui/card";
import { import {
Shield, Shield,
Lock, Lock,
@@ -19,106 +25,130 @@ import {
Copy, Copy,
Eye, Eye,
EyeOff, EyeOff,
} from "lucide-react" Ruler,
import { APP_VERSION } from "./release-notes-modal" } from "lucide-react";
import { getApiUrl, fetchApi } from "../lib/api-config" import { APP_VERSION } from "./release-notes-modal";
import { TwoFactorSetup } from "./two-factor-setup" import { getApiUrl, fetchApi } from "../lib/api-config";
import { TwoFactorSetup } from "./two-factor-setup";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
interface ProxMenuxTool { interface ProxMenuxTool {
key: string key: string;
name: string name: string;
enabled: boolean enabled: boolean;
} }
export function Settings() { export function Settings() {
const [authEnabled, setAuthEnabled] = useState(false) const [authEnabled, setAuthEnabled] = useState(false);
const [totpEnabled, setTotpEnabled] = useState(false) const [totpEnabled, setTotpEnabled] = useState(false);
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false);
const [error, setError] = useState("") const [error, setError] = useState("");
const [success, setSuccess] = useState("") const [success, setSuccess] = useState("");
// Setup form state // Setup form state
const [showSetupForm, setShowSetupForm] = useState(false) const [showSetupForm, setShowSetupForm] = useState(false);
const [username, setUsername] = useState("") const [username, setUsername] = useState("");
const [password, setPassword] = useState("") const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("") const [confirmPassword, setConfirmPassword] = useState("");
// Change password form state // Change password form state
const [showChangePassword, setShowChangePassword] = useState(false) const [showChangePassword, setShowChangePassword] = useState(false);
const [currentPassword, setCurrentPassword] = useState("") const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("") const [newPassword, setNewPassword] = useState("");
const [confirmNewPassword, setConfirmNewPassword] = useState("") const [confirmNewPassword, setConfirmNewPassword] = useState("");
const [show2FASetup, setShow2FASetup] = useState(false) const [show2FASetup, setShow2FASetup] = useState(false);
const [show2FADisable, setShow2FADisable] = useState(false) const [show2FADisable, setShow2FADisable] = useState(false);
const [disable2FAPassword, setDisable2FAPassword] = useState("") const [disable2FAPassword, setDisable2FAPassword] = useState("");
const [proxmenuxTools, setProxmenuxTools] = useState<ProxMenuxTool[]>([]) const [proxmenuxTools, setProxmenuxTools] = useState<ProxMenuxTool[]>([]);
const [loadingTools, setLoadingTools] = useState(true) const [loadingTools, setLoadingTools] = useState(true);
const [expandedVersions, setExpandedVersions] = useState<Record<string, boolean>>({ const [expandedVersions, setExpandedVersions] = useState<
Record<string, boolean>
>({
[APP_VERSION]: true, // Current version expanded by default [APP_VERSION]: true, // Current version expanded by default
}) });
const [networkUnitSettings, setNetworkUnitSettings] = useState("Bytes");
const [loadingUnitSettings, setLoadingUnitSettings] = useState(true);
// API Token state management // API Token state management
const [showApiTokenSection, setShowApiTokenSection] = useState(false) const [showApiTokenSection, setShowApiTokenSection] = useState(false);
const [apiToken, setApiToken] = useState("") const [apiToken, setApiToken] = useState("");
const [apiTokenVisible, setApiTokenVisible] = useState(false) const [apiTokenVisible, setApiTokenVisible] = useState(false);
const [tokenPassword, setTokenPassword] = useState("") const [tokenPassword, setTokenPassword] = useState("");
const [tokenTotpCode, setTokenTotpCode] = useState("") const [tokenTotpCode, setTokenTotpCode] = useState("");
const [generatingToken, setGeneratingToken] = useState(false) const [generatingToken, setGeneratingToken] = useState(false);
const [tokenCopied, setTokenCopied] = useState(false) const [tokenCopied, setTokenCopied] = useState(false);
useEffect(() => { useEffect(() => {
checkAuthStatus() checkAuthStatus();
loadProxmenuxTools() loadProxmenuxTools();
}, []) getUnitsSettings();
}, []);
const changeNetworkUnit = (unit: string) => {
localStorage.setItem("proxmenux-network-unit", unit);
setNetworkUnitSettings(unit);
};
const getUnitsSettings = () => {
const networkUnit =
localStorage.getItem("proxmenux-network-unit") || "Bytes";
// wait 2 seconds to simulate loading
console.log("[v0] Loaded network unit from localStorage:", networkUnit);
setTimeout(() => {
setNetworkUnitSettings(networkUnit);
setLoadingUnitSettings(false);
}, 1000);
};
const checkAuthStatus = async () => { const checkAuthStatus = async () => {
try { try {
const response = await fetch(getApiUrl("/api/auth/status")) const response = await fetch(getApiUrl("/api/auth/status"));
const data = await response.json() const data = await response.json();
setAuthEnabled(data.auth_enabled || false) setAuthEnabled(data.auth_enabled || false);
setTotpEnabled(data.totp_enabled || false) // Get 2FA status setTotpEnabled(data.totp_enabled || false); // Get 2FA status
} catch (err) { } catch (err) {
console.error("Failed to check auth status:", err) console.error("Failed to check auth status:", err);
} }
} };
const loadProxmenuxTools = async () => { const loadProxmenuxTools = async () => {
try { try {
const response = await fetch(getApiUrl("/api/proxmenux/installed-tools")) const response = await fetch(getApiUrl("/api/proxmenux/installed-tools"));
const data = await response.json() const data = await response.json();
if (data.success) { if (data.success) {
setProxmenuxTools(data.installed_tools || []) setProxmenuxTools(data.installed_tools || []);
} }
} catch (err) { } catch (err) {
console.error("Failed to load ProxMenux tools:", err) console.error("Failed to load ProxMenux tools:", err);
} finally { } finally {
setLoadingTools(false) setLoadingTools(false);
} }
} };
const handleEnableAuth = async () => { const handleEnableAuth = async () => {
setError("") setError("");
setSuccess("") setSuccess("");
if (!username || !password) { if (!username || !password) {
setError("Please fill in all fields") setError("Please fill in all fields");
return return;
} }
if (password !== confirmPassword) { if (password !== confirmPassword) {
setError("Passwords do not match") setError("Passwords do not match");
return return;
} }
if (password.length < 6) { if (password.length < 6) {
setError("Password must be at least 6 characters") setError("Password must be at least 6 characters");
return return;
} }
setLoading(true) setLoading(true);
try { try {
const response = await fetch(getApiUrl("/api/auth/setup"), { const response = await fetch(getApiUrl("/api/auth/setup"), {
@@ -129,145 +159,155 @@ export function Settings() {
password, password,
enable_auth: true, enable_auth: true,
}), }),
}) });
const data = await response.json() const data = await response.json();
if (!response.ok) { if (!response.ok) {
throw new Error(data.error || "Failed to enable authentication") throw new Error(data.error || "Failed to enable authentication");
} }
// Save token // Save token
localStorage.setItem("proxmenux-auth-token", data.token) localStorage.setItem("proxmenux-auth-token", data.token);
localStorage.setItem("proxmenux-auth-setup-complete", "true") localStorage.setItem("proxmenux-auth-setup-complete", "true");
setSuccess("Authentication enabled successfully!") setSuccess("Authentication enabled successfully!");
setAuthEnabled(true) setAuthEnabled(true);
setShowSetupForm(false) setShowSetupForm(false);
setUsername("") setUsername("");
setPassword("") setPassword("");
setConfirmPassword("") setConfirmPassword("");
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to enable authentication") setError(
err instanceof Error ? err.message : "Failed to enable authentication"
);
} finally { } finally {
setLoading(false) setLoading(false);
} }
} };
const handleDisableAuth = async () => { const handleDisableAuth = async () => {
if ( if (
!confirm( !confirm(
"Are you sure you want to disable authentication? This will remove password protection from your dashboard.", "Are you sure you want to disable authentication? This will remove password protection from your dashboard."
) )
) { ) {
return return;
} }
setLoading(true) setLoading(true);
setError("") setError("");
setSuccess("") setSuccess("");
try { try {
const token = localStorage.getItem("proxmenux-auth-token") const token = localStorage.getItem("proxmenux-auth-token");
const response = await fetch(getApiUrl("/api/auth/disable"), { const response = await fetch(getApiUrl("/api/auth/disable"), {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
}) });
const data = await response.json() const data = await response.json();
if (!response.ok) { if (!response.ok) {
throw new Error(data.message || "Failed to disable authentication") throw new Error(data.message || "Failed to disable authentication");
} }
localStorage.removeItem("proxmenux-auth-token") localStorage.removeItem("proxmenux-auth-token");
localStorage.removeItem("proxmenux-auth-setup-complete") localStorage.removeItem("proxmenux-auth-setup-complete");
setSuccess("Authentication disabled successfully! Reloading...") setSuccess("Authentication disabled successfully! Reloading...");
setTimeout(() => { setTimeout(() => {
window.location.reload() window.location.reload();
}, 1000) }, 1000);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to disable authentication. Please try again.") setError(
err instanceof Error
? err.message
: "Failed to disable authentication. Please try again."
);
} finally { } finally {
setLoading(false) setLoading(false);
} }
} };
const handleChangePassword = async () => { const handleChangePassword = async () => {
setError("") setError("");
setSuccess("") setSuccess("");
if (!currentPassword || !newPassword) { if (!currentPassword || !newPassword) {
setError("Please fill in all fields") setError("Please fill in all fields");
return return;
} }
if (newPassword !== confirmNewPassword) { if (newPassword !== confirmNewPassword) {
setError("New passwords do not match") setError("New passwords do not match");
return return;
} }
if (newPassword.length < 6) { if (newPassword.length < 6) {
setError("Password must be at least 6 characters") setError("Password must be at least 6 characters");
return return;
} }
setLoading(true) setLoading(true);
try { try {
const response = await fetch(getApiUrl("/api/auth/change-password"), { const response = await fetch(getApiUrl("/api/auth/change-password"), {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("proxmenux-auth-token")}`, Authorization: `Bearer ${localStorage.getItem(
"proxmenux-auth-token"
)}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
current_password: currentPassword, current_password: currentPassword,
new_password: newPassword, new_password: newPassword,
}), }),
}) });
const data = await response.json() const data = await response.json();
if (!response.ok) { if (!response.ok) {
throw new Error(data.error || "Failed to change password") throw new Error(data.error || "Failed to change password");
} }
// Update token if provided // Update token if provided
if (data.token) { if (data.token) {
localStorage.setItem("proxmenux-auth-token", data.token) localStorage.setItem("proxmenux-auth-token", data.token);
} }
setSuccess("Password changed successfully!") setSuccess("Password changed successfully!");
setShowChangePassword(false) setShowChangePassword(false);
setCurrentPassword("") setCurrentPassword("");
setNewPassword("") setNewPassword("");
setConfirmNewPassword("") setConfirmNewPassword("");
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to change password") setError(
err instanceof Error ? err.message : "Failed to change password"
);
} finally { } finally {
setLoading(false) setLoading(false);
} }
} };
const handleDisable2FA = async () => { const handleDisable2FA = async () => {
setError("") setError("");
setSuccess("") setSuccess("");
if (!disable2FAPassword) { if (!disable2FAPassword) {
setError("Please enter your password") setError("Please enter your password");
return return;
} }
setLoading(true) setLoading(true);
try { try {
const token = localStorage.getItem("proxmenux-auth-token") const token = localStorage.getItem("proxmenux-auth-token");
const response = await fetch(getApiUrl("/api/auth/totp/disable"), { const response = await fetch(getApiUrl("/api/auth/totp/disable"), {
method: "POST", method: "POST",
headers: { headers: {
@@ -275,47 +315,47 @@ export function Settings() {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ password: disable2FAPassword }), body: JSON.stringify({ password: disable2FAPassword }),
}) });
const data = await response.json() const data = await response.json();
if (!response.ok) { if (!response.ok) {
throw new Error(data.message || "Failed to disable 2FA") throw new Error(data.message || "Failed to disable 2FA");
} }
setSuccess("2FA disabled successfully!") setSuccess("2FA disabled successfully!");
setTotpEnabled(false) setTotpEnabled(false);
setShow2FADisable(false) setShow2FADisable(false);
setDisable2FAPassword("") setDisable2FAPassword("");
checkAuthStatus() checkAuthStatus();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to disable 2FA") setError(err instanceof Error ? err.message : "Failed to disable 2FA");
} finally { } finally {
setLoading(false) setLoading(false);
} }
} };
const handleLogout = () => { const handleLogout = () => {
localStorage.removeItem("proxmenux-auth-token") localStorage.removeItem("proxmenux-auth-token");
localStorage.removeItem("proxmenux-auth-setup-complete") localStorage.removeItem("proxmenux-auth-setup-complete");
window.location.reload() window.location.reload();
} };
const handleGenerateApiToken = async () => { const handleGenerateApiToken = async () => {
setError("") setError("");
setSuccess("") setSuccess("");
if (!tokenPassword) { if (!tokenPassword) {
setError("Please enter your password") setError("Please enter your password");
return return;
} }
if (totpEnabled && !tokenTotpCode) { if (totpEnabled && !tokenTotpCode) {
setError("Please enter your 2FA code") setError("Please enter your 2FA code");
return return;
} }
setGeneratingToken(true) setGeneratingToken(true);
try { try {
const data = await fetchApi("/api/auth/generate-api-token", { const data = await fetchApi("/api/auth/generate-api-token", {
@@ -325,47 +365,55 @@ export function Settings() {
password: tokenPassword, password: tokenPassword,
totp_token: totpEnabled ? tokenTotpCode : undefined, totp_token: totpEnabled ? tokenTotpCode : undefined,
}), }),
}) });
if (!data.success) { if (!data.success) {
setError(data.message || data.error || "Failed to generate API token") setError(data.message || data.error || "Failed to generate API token");
return return;
} }
if (!data.token) { if (!data.token) {
setError("No token received from server") setError("No token received from server");
return return;
} }
setApiToken(data.token) setApiToken(data.token);
setSuccess("API token generated successfully! Make sure to copy it now as you won't be able to see it again.") setSuccess(
setTokenPassword("") "API token generated successfully! Make sure to copy it now as you won't be able to see it again."
setTokenTotpCode("") );
setTokenPassword("");
setTokenTotpCode("");
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to generate API token. Please try again.") setError(
err instanceof Error
? err.message
: "Failed to generate API token. Please try again."
);
} finally { } finally {
setGeneratingToken(false) setGeneratingToken(false);
} }
} };
const copyApiToken = () => { const copyApiToken = () => {
navigator.clipboard.writeText(apiToken) navigator.clipboard.writeText(apiToken);
setTokenCopied(true) setTokenCopied(true);
setTimeout(() => setTokenCopied(false), 2000) setTimeout(() => setTokenCopied(false), 2000);
} };
const toggleVersion = (version: string) => { const toggleVersion = (version: string) => {
setExpandedVersions((prev) => ({ setExpandedVersions((prev) => ({
...prev, ...prev,
[version]: !prev[version], [version]: !prev[version],
})) }));
} };
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h1 className="text-3xl font-bold">Settings</h1> <h1 className="text-3xl font-bold">Settings</h1>
<p className="text-muted-foreground mt-2">Manage your dashboard security and preferences</p> <p className="text-muted-foreground mt-2">
Manage your dashboard security and preferences
</p>
</div> </div>
{/* Authentication Settings */} {/* Authentication Settings */}
@@ -375,7 +423,9 @@ export function Settings() {
<Shield className="h-5 w-5 text-blue-500" /> <Shield className="h-5 w-5 text-blue-500" />
<CardTitle>Authentication</CardTitle> <CardTitle>Authentication</CardTitle>
</div> </div>
<CardDescription>Protect your dashboard with username and password authentication</CardDescription> <CardDescription>
Protect your dashboard with username and password authentication
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{error && ( {error && (
@@ -395,19 +445,31 @@ export function Settings() {
<div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg"> <div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div <div
className={`w-10 h-10 rounded-full flex items-center justify-center ${authEnabled ? "bg-green-500/10" : "bg-gray-500/10"}`} className={`w-10 h-10 rounded-full flex items-center justify-center ${
authEnabled ? "bg-green-500/10" : "bg-gray-500/10"
}`}
> >
<Lock className={`h-5 w-5 ${authEnabled ? "text-green-500" : "text-gray-500"}`} /> <Lock
className={`h-5 w-5 ${
authEnabled ? "text-green-500" : "text-gray-500"
}`}
/>
</div> </div>
<div> <div>
<p className="font-medium">Authentication Status</p> <p className="font-medium">Authentication Status</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{authEnabled ? "Password protection is enabled" : "No password protection"} {authEnabled
? "Password protection is enabled"
: "No password protection"}
</p> </p>
</div> </div>
</div> </div>
<div <div
className={`px-3 py-1 rounded-full text-sm font-medium ${authEnabled ? "bg-green-500/10 text-green-500" : "bg-gray-500/10 text-gray-500"}`} className={`px-3 py-1 rounded-full text-sm font-medium ${
authEnabled
? "bg-green-500/10 text-green-500"
: "bg-gray-500/10 text-gray-500"
}`}
> >
{authEnabled ? "Enabled" : "Disabled"} {authEnabled ? "Enabled" : "Disabled"}
</div> </div>
@@ -418,10 +480,14 @@ export function Settings() {
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2"> <div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" /> <Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-blue-500"> <p className="text-sm text-blue-500">
Enable authentication to protect your dashboard when accessing from non-private networks. Enable authentication to protect your dashboard when accessing
from non-private networks.
</p> </p>
</div> </div>
<Button onClick={() => setShowSetupForm(true)} className="w-full bg-blue-500 hover:bg-blue-600"> <Button
onClick={() => setShowSetupForm(true)}
className="w-full bg-blue-500 hover:bg-blue-600"
>
<Shield className="h-4 w-4 mr-2" /> <Shield className="h-4 w-4 mr-2" />
Enable Authentication Enable Authentication
</Button> </Button>
@@ -481,10 +547,19 @@ export function Settings() {
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button onClick={handleEnableAuth} className="flex-1 bg-blue-500 hover:bg-blue-600" disabled={loading}> <Button
onClick={handleEnableAuth}
className="flex-1 bg-blue-500 hover:bg-blue-600"
disabled={loading}
>
{loading ? "Enabling..." : "Enable"} {loading ? "Enabling..." : "Enable"}
</Button> </Button>
<Button onClick={() => setShowSetupForm(false)} variant="outline" className="flex-1" disabled={loading}> <Button
onClick={() => setShowSetupForm(false)}
variant="outline"
className="flex-1"
disabled={loading}
>
Cancel Cancel
</Button> </Button>
</div> </div>
@@ -493,13 +568,21 @@ export function Settings() {
{authEnabled && ( {authEnabled && (
<div className="space-y-3"> <div className="space-y-3">
<Button onClick={handleLogout} variant="outline" className="w-full bg-transparent"> <Button
onClick={handleLogout}
variant="outline"
className="w-full bg-transparent"
>
<LogOut className="h-4 w-4 mr-2" /> <LogOut className="h-4 w-4 mr-2" />
Logout Logout
</Button> </Button>
{!showChangePassword && ( {!showChangePassword && (
<Button onClick={() => setShowChangePassword(true)} variant="outline" className="w-full"> <Button
onClick={() => setShowChangePassword(true)}
variant="outline"
className="w-full"
>
<Lock className="h-4 w-4 mr-2" /> <Lock className="h-4 w-4 mr-2" />
Change Password Change Password
</Button> </Button>
@@ -542,7 +625,9 @@ export function Settings() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="confirm-new-password">Confirm New Password</Label> <Label htmlFor="confirm-new-password">
Confirm New Password
</Label>
<div className="relative"> <div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
@@ -582,15 +667,21 @@ export function Settings() {
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2"> <div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" /> <Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-400"> <div className="text-sm text-blue-400">
<p className="font-medium mb-1">Two-Factor Authentication (2FA)</p> <p className="font-medium mb-1">
Two-Factor Authentication (2FA)
</p>
<p className="text-blue-300"> <p className="text-blue-300">
Add an extra layer of security by requiring a code from your authenticator app in addition to Add an extra layer of security by requiring a code from
your password. your authenticator app in addition to your password.
</p> </p>
</div> </div>
</div> </div>
<Button onClick={() => setShow2FASetup(true)} variant="outline" className="w-full"> <Button
onClick={() => setShow2FASetup(true)}
variant="outline"
className="w-full"
>
<Shield className="h-4 w-4 mr-2" /> <Shield className="h-4 w-4 mr-2" />
Enable Two-Factor Authentication Enable Two-Factor Authentication
</Button> </Button>
@@ -601,11 +692,17 @@ export function Settings() {
<div className="space-y-3"> <div className="space-y-3">
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-3 flex items-center gap-2"> <div className="bg-green-500/10 border border-green-500/20 rounded-lg p-3 flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-green-500" /> <CheckCircle className="h-5 w-5 text-green-500" />
<p className="text-sm text-green-500 font-medium">2FA is enabled</p> <p className="text-sm text-green-500 font-medium">
2FA is enabled
</p>
</div> </div>
{!show2FADisable && ( {!show2FADisable && (
<Button onClick={() => setShow2FADisable(true)} variant="outline" className="w-full"> <Button
onClick={() => setShow2FADisable(true)}
variant="outline"
className="w-full"
>
<Shield className="h-4 w-4 mr-2" /> <Shield className="h-4 w-4 mr-2" />
Disable 2FA Disable 2FA
</Button> </Button>
@@ -613,8 +710,12 @@ export function Settings() {
{show2FADisable && ( {show2FADisable && (
<div className="space-y-4 border border-border rounded-lg p-4"> <div className="space-y-4 border border-border rounded-lg p-4">
<h3 className="font-semibold">Disable Two-Factor Authentication</h3> <h3 className="font-semibold">
<p className="text-sm text-muted-foreground">Enter your password to confirm</p> Disable Two-Factor Authentication
</h3>
<p className="text-sm text-muted-foreground">
Enter your password to confirm
</p>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="disable-2fa-password">Password</Label> <Label htmlFor="disable-2fa-password">Password</Label>
@@ -625,7 +726,9 @@ export function Settings() {
type="password" type="password"
placeholder="Enter your password" placeholder="Enter your password"
value={disable2FAPassword} value={disable2FAPassword}
onChange={(e) => setDisable2FAPassword(e.target.value)} onChange={(e) =>
setDisable2FAPassword(e.target.value)
}
className="pl-10" className="pl-10"
disabled={loading} disabled={loading}
/> />
@@ -633,14 +736,19 @@ export function Settings() {
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button onClick={handleDisable2FA} variant="destructive" className="flex-1" disabled={loading}> <Button
onClick={handleDisable2FA}
variant="destructive"
className="flex-1"
disabled={loading}
>
{loading ? "Disabling..." : "Disable 2FA"} {loading ? "Disabling..." : "Disable 2FA"}
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
setShow2FADisable(false) setShow2FADisable(false);
setDisable2FAPassword("") setDisable2FAPassword("");
setError("") setError("");
}} }}
variant="outline" variant="outline"
className="flex-1" className="flex-1"
@@ -654,7 +762,12 @@ export function Settings() {
</div> </div>
)} )}
<Button onClick={handleDisableAuth} variant="destructive" className="w-full" disabled={loading}> <Button
onClick={handleDisableAuth}
variant="destructive"
className="w-full"
disabled={loading}
>
Disable Authentication Disable Authentication
</Button> </Button>
</div> </div>
@@ -671,7 +784,8 @@ export function Settings() {
<CardTitle>API Access Tokens</CardTitle> <CardTitle>API Access Tokens</CardTitle>
</div> </div>
<CardDescription> <CardDescription>
Generate long-lived API tokens for external integrations like Homepage and Home Assistant Generate long-lived API tokens for external integrations like
Homepage and Home Assistant
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@@ -705,7 +819,10 @@ export function Settings() {
</div> </div>
{!showApiTokenSection && !apiToken && ( {!showApiTokenSection && !apiToken && (
<Button onClick={() => setShowApiTokenSection(true)} className="w-full bg-purple-500 hover:bg-purple-600"> <Button
onClick={() => setShowApiTokenSection(true)}
className="w-full bg-purple-500 hover:bg-purple-600"
>
<Key className="h-4 w-4 mr-2" /> <Key className="h-4 w-4 mr-2" />
Generate New API Token Generate New API Token
</Button> </Button>
@@ -763,10 +880,10 @@ export function Settings() {
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
setShowApiTokenSection(false) setShowApiTokenSection(false);
setTokenPassword("") setTokenPassword("");
setTokenTotpCode("") setTokenTotpCode("");
setError("") setError("");
}} }}
variant="outline" variant="outline"
className="flex-1" className="flex-1"
@@ -813,10 +930,23 @@ export function Settings() {
onClick={() => setApiTokenVisible(!apiTokenVisible)} onClick={() => setApiTokenVisible(!apiTokenVisible)}
className="h-7 w-7 p-0" className="h-7 w-7 p-0"
> >
{apiTokenVisible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} {apiTokenVisible ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button> </Button>
<Button size="sm" variant="ghost" onClick={copyApiToken} className="h-7 w-7 p-0"> <Button
<Copy className={`h-4 w-4 ${tokenCopied ? "text-green-500" : ""}`} /> size="sm"
variant="ghost"
onClick={copyApiToken}
className="h-7 w-7 p-0"
>
<Copy
className={`h-4 w-4 ${
tokenCopied ? "text-green-500" : ""
}`}
/>
</Button> </Button>
</div> </div>
</div> </div>
@@ -831,18 +961,21 @@ export function Settings() {
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm font-medium">How to use this token:</p> <p className="text-sm font-medium">How to use this token:</p>
<div className="bg-muted/50 rounded p-3 text-xs font-mono"> <div className="bg-muted/50 rounded p-3 text-xs font-mono">
<p className="text-muted-foreground mb-2"># Add to request headers:</p> <p className="text-muted-foreground mb-2">
# Add to request headers:
</p>
<p>Authorization: Bearer YOUR_TOKEN_HERE</p> <p>Authorization: Bearer YOUR_TOKEN_HERE</p>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
See the README documentation for complete integration examples with Homepage and Home Assistant. See the README documentation for complete integration
examples with Homepage and Home Assistant.
</p> </p>
</div> </div>
<Button <Button
onClick={() => { onClick={() => {
setApiToken("") setApiToken("");
setShowApiTokenSection(false) setShowApiTokenSection(false);
}} }}
variant="outline" variant="outline"
className="w-full" className="w-full"
@@ -855,6 +988,42 @@ export function Settings() {
</Card> </Card>
)} )}
{/* ProxMenux unit settings*/}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Ruler className="h-5 w-5 text-green-500" />
<CardTitle>ProxMenux unit settings</CardTitle>
</div>
<CardDescription>Change settings related to units</CardDescription>
</CardHeader>
<CardContent>
{loadingUnitSettings ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin h-8 w-8 border-4 border-green-500 border-t-transparent rounded-full" />
</div>
) : (
<div className="text-foreground flex items-center justify-between">
<div className="flex items-center">
Network Unit Display
</div>
<Select
value={networkUnitSettings}
onValueChange={changeNetworkUnit}
>
<SelectTrigger className="w-28 h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Bits">Bits</SelectItem>
<SelectItem value="Bytes">Bytes</SelectItem>
</SelectContent>
</Select>
</div>
)}
</CardContent>
</Card>
{/* ProxMenux Optimizations */} {/* ProxMenux Optimizations */}
<Card> <Card>
<CardHeader> <CardHeader>
@@ -862,7 +1031,9 @@ export function Settings() {
<Wrench className="h-5 w-5 text-orange-500" /> <Wrench className="h-5 w-5 text-orange-500" />
<CardTitle>ProxMenux Optimizations</CardTitle> <CardTitle>ProxMenux Optimizations</CardTitle>
</div> </div>
<CardDescription>System optimizations and utilities installed via ProxMenux</CardDescription> <CardDescription>
System optimizations and utilities installed via ProxMenux
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{loadingTools ? ( {loadingTools ? (
@@ -872,14 +1043,22 @@ export function Settings() {
) : proxmenuxTools.length === 0 ? ( ) : proxmenuxTools.length === 0 ? (
<div className="text-center py-8"> <div className="text-center py-8">
<Package className="h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50" /> <Package className="h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50" />
<p className="text-muted-foreground">No ProxMenux optimizations installed yet</p> <p className="text-muted-foreground">
<p className="text-sm text-muted-foreground mt-1">Run ProxMenux to configure system optimizations</p> No ProxMenux optimizations installed yet
</p>
<p className="text-sm text-muted-foreground mt-1">
Run ProxMenux to configure system optimizations
</p>
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between mb-4 pb-2 border-b border-border"> <div className="flex items-center justify-between mb-4 pb-2 border-b border-border">
<span className="text-sm font-medium text-muted-foreground">Installed Tools</span> <span className="text-sm font-medium text-muted-foreground">
<span className="text-sm font-semibold text-orange-500">{proxmenuxTools.length} active</span> Installed Tools
</span>
<span className="text-sm font-semibold text-orange-500">
{proxmenuxTools.length} active
</span>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2"> <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{proxmenuxTools.map((tool) => ( {proxmenuxTools.map((tool) => (
@@ -901,10 +1080,10 @@ export function Settings() {
open={show2FASetup} open={show2FASetup}
onClose={() => setShow2FASetup(false)} onClose={() => setShow2FASetup(false)}
onSuccess={() => { onSuccess={() => {
setSuccess("2FA enabled successfully!") setSuccess("2FA enabled successfully!");
checkAuthStatus() checkAuthStatus();
}} }}
/> />
</div> </div>
) );
} }