From 2f68dc018e0baddff496b1c95249f913ceaa94b1 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sun, 31 Aug 2025 20:18:08 -0500 Subject: [PATCH] Add SSH password reset, fix TOTP errors, and update json-import guide. --- src/App.tsx | 2 +- .../Host Manager/HostManagerHostViewer.tsx | 252 +----------------- src/ui/Homepage/HomepageAuth.tsx | 11 +- src/ui/User/PasswordReset.tsx | 248 ++++++++++++++++- src/ui/User/TOTPSetup.tsx | 2 +- src/ui/User/UserProfile.tsx | 11 +- 6 files changed, 253 insertions(+), 273 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 3e65f461..2894ede1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -197,7 +197,7 @@ function AppContent() { height: showProfile ? "100vh" : 0, width: showProfile ? "100%" : 0, position: showProfile ? "static" : "absolute", - overflow: "hidden", + overflow: "auto", }} > diff --git a/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx b/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx index aa4e65ca..476eb895 100644 --- a/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx +++ b/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx @@ -325,257 +325,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) { variant="outline" size="sm" onClick={() => { - const infoContent = ` -JSON Import Format Guide - -REQUIRED FIELDS: -• ip: Host IP address (string) -• port: SSH port (number, 1-65535) -• username: SSH username (string) -• authType: "password" or "key" - -AUTHENTICATION FIELDS: -• password: Required if authType is "password" -• key: SSH private key content (string) if authType is "key" -• keyPassword: Optional key passphrase -• keyType: Key type (auto, ssh-rsa, ssh-ed25519, etc.) - -OPTIONAL FIELDS: -• name: Display name (string) -• folder: Organization folder (string) -• tags: Array of tag strings -• pin: Pin to top (boolean) -• enableTerminal: Show in Terminal tab (boolean, default: true) -• enableTunnel: Show in Tunnel tab (boolean, default: true) -• enableFileManager: Show in File Manager tab (boolean, default: true) -• defaultPath: Default directory path (string) - -TUNNEL CONFIGURATION: -• tunnelConnections: Array of tunnel objects - - sourcePort: Local port (number) - - endpointPort: Remote port (number) - - endpointHost: Target host name (string) - - maxRetries: Retry attempts (number, default: 3) - - retryInterval: Retry delay in seconds (number, default: 10) - - autoStart: Auto-start on launch (boolean, default: false) - -EXAMPLE STRUCTURE: -{ - "hosts": [ - { - "name": "Web Server", - "ip": "192.168.1.100", - "port": 22, - "username": "admin", - "authType": "password", - "password": "your_password", - "folder": "Production", - "tags": ["web", "production"], - "pin": true, - "enableTerminal": true, - "enableTunnel": false, - "enableFileManager": true, - "defaultPath": "/var/www" - } - ] -} - -• Maximum 100 hosts per import -• File should contain a "hosts" array or be an array of host objects -• All fields are copyable for easy reference - `; - - const newWindow = window.open('', '_blank', 'width=600,height=800,scrollbars=yes,resizable=yes'); - if (newWindow) { - newWindow.document.write(` - - - - SSH JSON Import Guide - - - -

SSH JSON Import Format Guide

-

Use this guide to create JSON files for bulk importing SSH hosts. All examples are copyable.

- -

Required Fields

-
-
- ip - Host IP address (string) - -
-
- port - SSH port (number, 1-65535) - -
-
- username - SSH username (string) - -
-
- authType - "password" or "key" - -
-
- -

Authentication Fields

-
-
- password - Required if authType is "password" - -
-
- key - SSH private key content (string) if authType is "key" - -
-
- keyPassword - Optional key passphrase - -
-
- keyType - Key type (auto, ssh-rsa, ssh-ed25519, etc.) - -
-
- -

Optional Fields

-
-
- name - Display name (string) - -
-
- folder - Organization folder (string) - -
-
- tags - Array of tag strings - -
-
- pin - Pin to top (boolean) - -
-
- enableTerminal - Show in Terminal tab (boolean, default: true) - -
-
- enableTunnel - Show in Tunnel tab (boolean, default: true) - -
-
- enableFileManager - Show in File Manager tab (boolean, default: true) - -
-
- defaultPath - Default directory path (string) - -
-
- -

Tunnel Configuration

-
-
- tunnelConnections - Array of tunnel objects - -
-
-
- sourcePort - Local port (number) - -
-
- endpointPort - Remote port (number) - -
-
- endpointHost - Target host name (string) - -
-
- maxRetries - Retry attempts (number, default: 3) - -
-
- retryInterval - Retry delay in seconds (number, default: 10) - -
-
- autoStart - Auto-start on launch (boolean, default: false) - -
-
-
- -

Example JSON Structure

-
{
-  "hosts": [
-    {
-      "name": "Web Server",
-      "ip": "192.168.1.100",
-      "port": 22,
-      "username": "admin",
-      "authType": "password",
-      "password": "your_password",
-      "folder": "Production",
-      "tags": ["web", "production"],
-      "pin": true,
-      "enableTerminal": true,
-      "enableTunnel": false,
-      "enableFileManager": true,
-      "defaultPath": "/var/www"
-    }
-  ]
-}
- -

Important Notes

- - - - `); - newWindow.document.close(); - } + window.open('https://docs.termix.site/json-import', '_blank'); }} > Format Guide diff --git a/src/ui/Homepage/HomepageAuth.tsx b/src/ui/Homepage/HomepageAuth.tsx index d47d194e..51212b33 100644 --- a/src/ui/Homepage/HomepageAuth.tsx +++ b/src/ui/Homepage/HomepageAuth.tsx @@ -30,8 +30,6 @@ function getCookie(name: string) { }, ""); } - - interface HomepageAuthProps extends React.ComponentProps<"div"> { setLoggedIn: (loggedIn: boolean) => void; setIsAdmin: (isAdmin: boolean) => void; @@ -486,13 +484,6 @@ export function HomepageAuth({ > Cancel - - {error && ( - - Error - {error} - - )} )} @@ -616,7 +607,7 @@ export function HomepageAuth({ )} {resetStep === "verify" && ( - <> + <>o

Enter the 6-digit code from the docker container logs for user: {localUsername}

diff --git a/src/ui/User/PasswordReset.tsx b/src/ui/User/PasswordReset.tsx index 88acec53..19fb4724 100644 --- a/src/ui/User/PasswordReset.tsx +++ b/src/ui/User/PasswordReset.tsx @@ -1,8 +1,112 @@ import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card.tsx"; import {Key} from "lucide-react"; -import React from "react"; +import React, {useState} from "react"; +import {completePasswordReset, initiatePasswordReset, verifyPasswordResetCode} from "@/ui/main-axios.ts"; +import {Label} from "@/components/ui/label.tsx"; +import {Input} from "@/components/ui/input.tsx"; +import {Button} from "@/components/ui/button.tsx"; +import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx"; + +interface PasswordResetProps { + userInfo: { + username: string; + is_admin: boolean; + is_oidc: boolean; + totp_enabled: boolean; + } +} + +export function PasswordReset({userInfo}: PasswordResetProps) { + const [error, setError] = useState(null); + + const [resetStep, setResetStep] = useState<"initiate" | "verify" | "newPassword">("initiate"); + const [resetCode, setResetCode] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [tempToken, setTempToken] = useState(""); + const [resetLoading, setResetLoading] = useState(false); + const [resetSuccess, setResetSuccess] = useState(false); + + async function handleInitiatePasswordReset() { + setError(null); + setResetLoading(true); + try { + const result = await initiatePasswordReset(userInfo.username); + setResetStep("verify"); + setError(null); + } catch (err: any) { + setError(err?.response?.data?.error || err?.message || "Failed to initiate password reset"); + } finally { + setResetLoading(false); + } + } + + function resetPasswordState() { + setResetStep("initiate"); + setResetCode(""); + setNewPassword(""); + setConfirmPassword(""); + setTempToken(""); + setError(null); + setResetSuccess(false); + } + + async function handleVerifyResetCode() { + setError(null); + setResetLoading(true); + try { + const response = await verifyPasswordResetCode(userInfo.username, resetCode); + setTempToken(response.tempToken); + setResetStep("newPassword"); + setError(null); + } catch (err: any) { + setError(err?.response?.data?.error || "Failed to verify reset code"); + } finally { + setResetLoading(false); + } + } + + async function handleCompletePasswordReset() { + setError(null); + setResetLoading(true); + + if (newPassword !== confirmPassword) { + setError("Passwords do not match"); + setResetLoading(false); + return; + } + + if (newPassword.length < 6) { + setError("Password must be at least 6 characters long"); + setResetLoading(false); + return; + } + + try { + await completePasswordReset(userInfo.username, tempToken, newPassword); + + setResetStep("initiate"); + setResetCode(""); + setNewPassword(""); + setConfirmPassword(""); + setTempToken(""); + setError(null); + + setResetSuccess(true); + } catch (err: any) { + setError(err?.response?.data?.error || "Failed to complete password reset"); + } finally { + setResetLoading(false); + } + } + + const Spinner = ( + + + + + ); -export function PasswordReset() { return ( @@ -15,9 +119,143 @@ export function PasswordReset() { -

- Password change functionality can be implemented here -

+ <> + {resetStep === "initiate" && !resetSuccess && ( + <> +
+ +
+ + )} + + {resetStep === "verify" && ( + <> +
+

Enter the 6-digit code from the docker container logs for + user: {userInfo.username}

+
+
+
+ + setResetCode(e.target.value.replace(/\D/g, ''))} + disabled={resetLoading} + placeholder="000000" + /> +
+ + +
+ + )} + + {resetSuccess && ( + <> + + Success! + + Your password has been successfully reset! You can now log in + with your new password. + + + + )} + + {resetStep === "newPassword" && !resetSuccess && ( + <> +
+

Enter your new password for + user: {userInfo.username}

+
+
+
+ + setNewPassword(e.target.value)} + disabled={resetLoading} + autoComplete="new-password" + /> +
+
+ + setConfirmPassword(e.target.value)} + disabled={resetLoading} + autoComplete="new-password" + /> +
+ + +
+ + )} + {error && ( + + Error + {error} + + )} +
) diff --git a/src/ui/User/TOTPSetup.tsx b/src/ui/User/TOTPSetup.tsx index 08c2e491..4bbd00d8 100644 --- a/src/ui/User/TOTPSetup.tsx +++ b/src/ui/User/TOTPSetup.tsx @@ -280,7 +280,7 @@ export function TOTPSetup({ isEnabled: initialEnabled, onStatusChange }: TOTPSet className="font-mono text-sm" />