Add SSH password reset, fix TOTP errors, and update json-import guide.
This commit is contained in:
@@ -197,7 +197,7 @@ function AppContent() {
|
|||||||
height: showProfile ? "100vh" : 0,
|
height: showProfile ? "100vh" : 0,
|
||||||
width: showProfile ? "100%" : 0,
|
width: showProfile ? "100%" : 0,
|
||||||
position: showProfile ? "static" : "absolute",
|
position: showProfile ? "static" : "absolute",
|
||||||
overflow: "hidden",
|
overflow: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UserProfile isTopbarOpen={isTopbarOpen} />
|
<UserProfile isTopbarOpen={isTopbarOpen} />
|
||||||
|
|||||||
@@ -325,257 +325,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const infoContent = `
|
window.open('https://docs.termix.site/json-import', '_blank');
|
||||||
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(`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>SSH JSON Import Guide</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
margin: 20px;
|
|
||||||
background: #1a1a1a;
|
|
||||||
color: #ffffff;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
background: #2a2a2a;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow-x: auto;
|
|
||||||
border: 1px solid #404040;
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
background: #404040;
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
|
||||||
}
|
|
||||||
h1 { color: #60a5fa; border-bottom: 2px solid #60a5fa; padding-bottom: 10px; }
|
|
||||||
h2 { color: #34d399; margin-top: 25px; }
|
|
||||||
.field-group { margin: 15px 0; }
|
|
||||||
.field-item { margin: 8px 0; }
|
|
||||||
.copy-btn {
|
|
||||||
background: #3b82f6;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.copy-btn:hover { background: #2563eb; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>SSH JSON Import Format Guide</h1>
|
|
||||||
<p>Use this guide to create JSON files for bulk importing SSH hosts. All examples are copyable.</p>
|
|
||||||
|
|
||||||
<h2>Required Fields</h2>
|
|
||||||
<div class="field-group">
|
|
||||||
<div class="field-item">
|
|
||||||
<code>ip</code> - Host IP address (string)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('ip')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>port</code> - SSH port (number, 1-65535)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('port')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>username</code> - SSH username (string)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('username')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>authType</code> - "password" or "key"
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('authType')">Copy</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Authentication Fields</h2>
|
|
||||||
<div class="field-group">
|
|
||||||
<div class="field-item">
|
|
||||||
<code>password</code> - Required if authType is "password"
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('password')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>key</code> - SSH private key content (string) if authType is "key"
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('key')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>keyPassword</code> - Optional key passphrase
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('keyPassword')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>keyType</code> - Key type (auto, ssh-rsa, ssh-ed25519, etc.)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('keyType')">Copy</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Optional Fields</h2>
|
|
||||||
<div class="field-group">
|
|
||||||
<div class="field-item">
|
|
||||||
<code>name</code> - Display name (string)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('name')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>folder</code> - Organization folder (string)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('folder')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>tags</code> - Array of tag strings
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('tags')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>pin</code> - Pin to top (boolean)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('pin')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>enableTerminal</code> - Show in Terminal tab (boolean, default: true)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('enableTerminal')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>enableTunnel</code> - Show in Tunnel tab (boolean, default: true)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('enableTunnel')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>enableFileManager</code> - Show in File Manager tab (boolean, default: true)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('enableFileManager')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>defaultPath</code> - Default directory path (string)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('defaultPath')">Copy</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Tunnel Configuration</h2>
|
|
||||||
<div class="field-group">
|
|
||||||
<div class="field-item">
|
|
||||||
<code>tunnelConnections</code> - Array of tunnel objects
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('tunnelConnections')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div style="margin-left: 20px;">
|
|
||||||
<div class="field-item">
|
|
||||||
<code>sourcePort</code> - Local port (number)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('sourcePort')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>endpointPort</code> - Remote port (number)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('endpointPort')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>endpointHost</code> - Target host name (string)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('endpointHost')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>maxRetries</code> - Retry attempts (number, default: 3)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('maxRetries')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>retryInterval</code> - Retry delay in seconds (number, default: 10)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('retryInterval')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div class="field-item">
|
|
||||||
<code>autoStart</code> - Auto-start on launch (boolean, default: false)
|
|
||||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('autoStart')">Copy</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Example JSON Structure</h2>
|
|
||||||
<pre><code>{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}</code></pre>
|
|
||||||
|
|
||||||
<h2>Important Notes</h2>
|
|
||||||
<ul>
|
|
||||||
<li>Maximum 100 hosts per import</li>
|
|
||||||
<li>File should contain a "hosts" array or be an array of host objects</li>
|
|
||||||
<li>All fields are copyable for easy reference</li>
|
|
||||||
<li>Use the Download Sample button to get a complete example file</li>
|
|
||||||
</ul>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`);
|
|
||||||
newWindow.document.close();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Format Guide
|
Format Guide
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ function getCookie(name: string) {
|
|||||||
}, "");
|
}, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface HomepageAuthProps extends React.ComponentProps<"div"> {
|
interface HomepageAuthProps extends React.ComponentProps<"div"> {
|
||||||
setLoggedIn: (loggedIn: boolean) => void;
|
setLoggedIn: (loggedIn: boolean) => void;
|
||||||
setIsAdmin: (isAdmin: boolean) => void;
|
setIsAdmin: (isAdmin: boolean) => void;
|
||||||
@@ -486,13 +484,6 @@ export function HomepageAuth({
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertTitle>Error</AlertTitle>
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -616,7 +607,7 @@ export function HomepageAuth({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{resetStep === "verify" && (
|
{resetStep === "verify" && (
|
||||||
<>
|
<>o
|
||||||
<div className="text-center text-muted-foreground mb-4">
|
<div className="text-center text-muted-foreground mb-4">
|
||||||
<p>Enter the 6-digit code from the docker container logs for
|
<p>Enter the 6-digit code from the docker container logs for
|
||||||
user: <strong>{localUsername}</strong></p>
|
user: <strong>{localUsername}</strong></p>
|
||||||
|
|||||||
@@ -1,8 +1,112 @@
|
|||||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card.tsx";
|
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card.tsx";
|
||||||
import {Key} from "lucide-react";
|
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<string | null>(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 = (
|
||||||
|
<svg className="animate-spin mr-2 h-4 w-4 text-white 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>
|
||||||
|
);
|
||||||
|
|
||||||
export function PasswordReset() {
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -15,9 +119,143 @@ export function PasswordReset() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm text-muted-foreground">
|
<>
|
||||||
Password change functionality can be implemented here
|
{resetStep === "initiate" && !resetSuccess && (
|
||||||
</p>
|
<>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="w-full h-11 text-base font-semibold"
|
||||||
|
disabled={resetLoading || !userInfo.username.trim()}
|
||||||
|
onClick={handleInitiatePasswordReset}
|
||||||
|
>
|
||||||
|
{resetLoading ? Spinner : "Send Reset Code"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{resetStep === "verify" && (
|
||||||
|
<>
|
||||||
|
<div className="text-center text-muted-foreground mb-4">
|
||||||
|
<p>Enter the 6-digit code from the docker container logs for
|
||||||
|
user: <strong>{userInfo.username}</strong></p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="reset-code">Reset Code</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="000000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="w-full h-11 text-base font-semibold"
|
||||||
|
disabled={resetLoading || resetCode.length !== 6}
|
||||||
|
onClick={handleVerifyResetCode}
|
||||||
|
>
|
||||||
|
{resetLoading ? Spinner : "Verify Code"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full h-11 text-base font-semibold"
|
||||||
|
disabled={resetLoading}
|
||||||
|
onClick={() => {
|
||||||
|
setResetStep("initiate");
|
||||||
|
setResetCode("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{resetSuccess && (
|
||||||
|
<>
|
||||||
|
<Alert className="">
|
||||||
|
<AlertTitle>Success!</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Your password has been successfully reset! You can now log in
|
||||||
|
with your new password.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{resetStep === "newPassword" && !resetSuccess && (
|
||||||
|
<>
|
||||||
|
<div className="text-center text-muted-foreground mb-4">
|
||||||
|
<p>Enter your new password for
|
||||||
|
user: <strong>{userInfo.username}</strong></p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="new-password">New Password</Label>
|
||||||
|
<Input
|
||||||
|
id="new-password"
|
||||||
|
type="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">Confirm Password</Label>
|
||||||
|
<Input
|
||||||
|
id="confirm-password"
|
||||||
|
type="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 : "Reset Password"}
|
||||||
|
</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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive" className="mt-4">
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ export function TOTPSetup({ isEnabled: initialEnabled, onStatusChange }: TOTPSet
|
|||||||
className="font-mono text-sm"
|
className="font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="default"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => copyToClipboard(secret, "Secret key")}
|
onClick={() => copyToClipboard(secret, "Secret key")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -78,9 +78,10 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container max-w-4xl mx-auto p-6" style={{
|
<div className="container max-w-4xl mx-auto p-6 overflow-y-auto" style={{
|
||||||
marginTop: isTopbarOpen ? '60px' : '0',
|
marginTop: isTopbarOpen ? '60px' : '0',
|
||||||
transition: 'margin-top 0.3s ease'
|
transition: 'margin-top 0.3s ease',
|
||||||
|
maxHeight: 'calc(100vh - 60px)'
|
||||||
}}>
|
}}>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-3xl font-bold">User Profile</h1>
|
<h1 className="text-3xl font-bold">User Profile</h1>
|
||||||
@@ -154,9 +155,9 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{!userInfo.is_oidc && (
|
{!userInfo.is_oidc && (
|
||||||
<PasswordReset>
|
<PasswordReset
|
||||||
|
userInfo={userInfo}
|
||||||
</PasswordReset>
|
/>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
Reference in New Issue
Block a user