feat: Added none password option and fixed some navbar issues (still present)

This commit is contained in:
LukeGus
2025-10-22 00:16:45 -05:00
parent 40232af503
commit 471e2ff3fa
7 changed files with 268 additions and 91 deletions

View File

@@ -678,7 +678,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.authType,
decrypted.password || null,
decrypted.key || null,
decrypted.keyPassword || decrypted.key_password || null,
decrypted.key_password || null,
decrypted.keyType || null,
decrypted.autostartPassword || null,
decrypted.autostartKey || null,
@@ -721,9 +721,9 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.username,
decrypted.password || null,
decrypted.key || null,
decrypted.privateKey || decrypted.private_key || null,
decrypted.publicKey || decrypted.public_key || null,
decrypted.keyPassword || decrypted.key_password || null,
decrypted.private_key || null,
decrypted.public_key || null,
decrypted.key_password || null,
decrypted.keyType || null,
decrypted.detectedKeyType || null,
decrypted.usageCount || 0,
@@ -1006,7 +1006,6 @@ app.post(
};
try {
try {
const importedHosts = importDb
.prepare("SELECT * FROM ssh_data")

View File

@@ -338,6 +338,38 @@ wss.on("connection", async (ws: WebSocket, req) => {
break;
}
case "password_response": {
const passwordData = data as TOTPResponseData; // Same structure
if (keyboardInteractiveFinish && passwordData?.code) {
const password = passwordData.code;
sshLogger.info("Password received from user", {
operation: "password_response",
userId,
passwordLength: password.length,
});
keyboardInteractiveFinish([password]);
keyboardInteractiveFinish = null;
} else {
sshLogger.warn(
"Password response received but no callback available",
{
operation: "password_response_error",
userId,
hasCallback: !!keyboardInteractiveFinish,
hasCode: !!passwordData?.code,
},
);
ws.send(
JSON.stringify({
type: "error",
message: "Password authentication state lost. Please reconnect.",
}),
);
}
break;
}
default:
sshLogger.warn("Unknown message type received", {
operation: "websocket_message_unknown_type",
@@ -779,7 +811,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
}),
);
} else {
// Non-TOTP prompts (password, etc.) - respond automatically
// Non-TOTP prompts (password, etc.)
if (keyboardInteractiveResponded) {
sshLogger.warn(
"Already responded to keyboard-interactive, ignoring subsequent prompt",
@@ -793,6 +825,54 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
keyboardInteractiveResponded = true;
// Check if we have stored credentials for auto-response
const hasStoredPassword =
resolvedCredentials.password &&
resolvedCredentials.authType !== "none";
if (!hasStoredPassword && resolvedCredentials.authType === "none") {
// For "none" auth type, prompt user for all keyboard-interactive inputs
const passwordPromptIndex = prompts.findIndex((p) =>
/password/i.test(p.prompt),
);
if (passwordPromptIndex !== -1) {
// Ask user for password
keyboardInteractiveFinish = (userResponses: string[]) => {
const userInput = (userResponses[0] || "").trim();
// Build responses for all prompts
const responses = prompts.map((p, index) => {
if (index === passwordPromptIndex) {
return userInput;
}
return "";
});
sshLogger.info(
"User-provided password being sent to SSH server",
{
operation: "interactive_password_verification",
hostId: id,
passwordLength: userInput.length,
totalPrompts: prompts.length,
},
);
finish(responses);
};
ws.send(
JSON.stringify({
type: "password_required",
prompt: prompts[passwordPromptIndex].prompt,
}),
);
return;
}
}
// Auto-respond with stored credentials
const responses = prompts.map((p) => {
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
return resolvedCredentials.password;
@@ -882,7 +962,23 @@ wss.on("connection", async (ws: WebSocket, req) => {
},
};
if (resolvedCredentials.authType === "key" && resolvedCredentials.key) {
if (resolvedCredentials.authType === "none") {
// No credentials provided - rely entirely on keyboard-interactive authentication
// This mimics the behavior of the ssh command-line client where it prompts for password/TOTP
sshLogger.info(
"Using interactive authentication (no stored credentials)",
{
operation: "ssh_auth_none",
hostId: id,
ip,
username,
},
);
// Don't set password or privateKey - let keyboard-interactive handle everything
} else if (
resolvedCredentials.authType === "key" &&
resolvedCredentials.key
) {
try {
if (
!resolvedCredentials.key.includes("-----BEGIN") ||

View File

@@ -18,7 +18,7 @@ export interface SSHHost {
folder: string;
tags: string[];
pin: boolean;
authType: "password" | "key" | "credential";
authType: "password" | "key" | "credential" | "none";
password?: string;
key?: string;
keyPassword?: string;
@@ -48,7 +48,7 @@ export interface SSHHostData {
folder?: string;
tags?: string[];
pin?: boolean;
authType: "password" | "key" | "credential";
authType: "password" | "key" | "credential" | "none";
password?: string;
key?: File | null;
keyPassword?: string;
@@ -298,7 +298,7 @@ export type ErrorType =
// AUTHENTICATION TYPES
// ============================================================================
export type AuthType = "password" | "key" | "credential";
export type AuthType = "password" | "key" | "credential" | "none";
export type KeyType = "rsa" | "ecdsa" | "ed25519";

View File

@@ -90,9 +90,9 @@ export function HostManagerEditor({
Array<{ id: number; username: string; authType: string }>
>([]);
const [authTab, setAuthTab] = useState<"password" | "key" | "credential">(
"password",
);
const [authTab, setAuthTab] = useState<
"password" | "key" | "credential" | "none"
>("password");
const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">(
"upload",
);
@@ -179,7 +179,7 @@ export function HostManagerEditor({
folder: z.string().optional(),
tags: z.array(z.string().min(1)).default([]),
pin: z.boolean().default(false),
authType: z.enum(["password", "key", "credential"]),
authType: z.enum(["password", "key", "credential", "none"]),
credentialId: z.number().optional().nullable(),
password: z.string().optional(),
key: z.any().optional().nullable(),
@@ -241,6 +241,11 @@ export function HostManagerEditor({
}),
})
.superRefine((data, ctx) => {
if (data.authType === "none") {
// No credentials required for "none" auth type - will use keyboard-interactive
return;
}
if (data.authType === "password") {
if (
!data.password ||
@@ -356,7 +361,9 @@ export function HostManagerEditor({
? "credential"
: cleanedHost.key
? "key"
: "password";
: cleanedHost.password
? "password"
: "none";
setAuthTab(defaultAuthType);
const formData = {
@@ -367,7 +374,7 @@ export function HostManagerEditor({
folder: cleanedHost.folder || "",
tags: cleanedHost.tags || [],
pin: Boolean(cleanedHost.pin),
authType: defaultAuthType as "password" | "key" | "credential",
authType: defaultAuthType as "password" | "key" | "credential" | "none",
credentialId: null,
password: "",
key: null,
@@ -922,7 +929,8 @@ export function HostManagerEditor({
const newAuthType = value as
| "password"
| "key"
| "credential";
| "credential"
| "none";
setAuthTab(newAuthType);
form.setValue("authType", newAuthType);
}}
@@ -936,6 +944,7 @@ export function HostManagerEditor({
<TabsTrigger value="credential">
{t("hosts.credential")}
</TabsTrigger>
<TabsTrigger value="none">{t("hosts.none")}</TabsTrigger>
</TabsList>
<TabsContent value="password">
<FormField
@@ -1154,6 +1163,19 @@ export function HostManagerEditor({
)}
/>
</TabsContent>
<TabsContent value="none">
<Alert className="mt-2">
<AlertDescription>
<strong>{t("hosts.noneAuthTitle")}</strong>
<div className="mt-2">
{t("hosts.noneAuthDescription")}
</div>
<div className="mt-2 text-sm">
{t("hosts.noneAuthDetails")}
</div>
</AlertDescription>
</Alert>
</TabsContent>
</Tabs>
</TabsContent>
<TabsContent value="terminal">

View File

@@ -613,7 +613,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
fontSize: 14,
fontFamily:
'"Caskaydia Cove Nerd Font Mono", "SF Mono", Consolas, "Liberation Mono", monospace',
theme: { background: "#18181b", foreground: "#f7f7f7" },
allowTransparency: true,
convertEol: true,
windowsMode: false,
@@ -626,6 +625,8 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
minimumContrastRatio: 1,
letterSpacing: 0,
lineHeight: 1.2,
theme: { background: "#18181b", foreground: "#f7f7f7" },
};
const fitAddon = new FitAddon();

View File

@@ -104,6 +104,7 @@ export function Host({ host }: HostProps): React.ReactElement {
className="min-w-[160px]"
>
<DropdownMenuItem
className="font-semibold"
onClick={() =>
addTab({ type: "server", title, hostConfig: host })
}
@@ -111,13 +112,17 @@ export function Host({ host }: HostProps): React.ReactElement {
Open Server Details
</DropdownMenuItem>
<DropdownMenuItem
className="font-semibold"
onClick={() =>
addTab({ type: "file_manager", title, hostConfig: host })
}
>
Open File Manager
</DropdownMenuItem>
<DropdownMenuItem onClick={() => alert("Settings clicked")}>
<DropdownMenuItem
className="font-semibold"
onClick={() => alert("Settings clicked")}
>
Edit
</DropdownMenuItem>
</DropdownMenuContent>

View File

@@ -58,12 +58,15 @@ export function TopNavbar({
const [isRecording, setIsRecording] = useState(false);
const [selectedTabIds, setSelectedTabIds] = useState<number[]>([]);
const [snippetsSidebarOpen, setSnippetsSidebarOpen] = useState(false);
const [justDroppedTabId, setJustDroppedTabId] = useState<number | null>(null); // New state variable
const [dragState, setDragState] = useState<{
draggedId: number | null;
draggedIndex: number | null;
currentX: number;
startX: number;
targetIndex: number | null;
}>({
draggedId: null,
draggedIndex: null,
currentX: 0,
startX: 0,
@@ -72,6 +75,8 @@ export function TopNavbar({
const containerRef = React.useRef<HTMLDivElement | null>(null);
const tabRefs = React.useRef<Map<number, HTMLDivElement>>(new Map());
const prevTabsRef = React.useRef<TabData[]>([]);
const handleTabActivate = (tabId: number) => {
setCurrentTab(tabId);
};
@@ -249,6 +254,37 @@ export function TopNavbar({
}
};
React.useEffect(() => {
if (prevTabsRef.current.length > 0 && tabs !== prevTabsRef.current) {
// Check if tabs actually changed
console.log("Tabs AFTER reorder (IDs and references):");
tabs.forEach((newTab, newIdx) => {
const oldTab = prevTabsRef.current.find((t) => t.id === newTab.id);
console.log(
` [${newIdx}] ID: ${newTab.id}, Ref:`,
newTab,
`(Old Ref:`,
oldTab,
`)`,
);
if (oldTab && oldTab !== newTab) {
console.warn(` Tab ID ${newTab.id} object reference CHANGED!`);
} else if (oldTab && oldTab === newTab) {
console.info(` Tab ID ${newTab.id} object reference PRESERVED.`);
}
});
// Clear prevTabsRef.current only after the comparison is done
prevTabsRef.current = [];
}
}, [tabs]); // Depend only on tabs
React.useEffect(() => {
if (justDroppedTabId !== null) {
const timer = setTimeout(() => setJustDroppedTabId(null), 50); // Clear after a short delay
return () => clearTimeout(timer);
}
}, [justDroppedTabId]);
const handleDragStart = (e: React.DragEvent, index: number) => {
console.log("Drag start:", index, e.clientX);
@@ -259,6 +295,7 @@ export function TopNavbar({
e.dataTransfer.setDragImage(img, 0, 0);
setDragState({
draggedId: tabs[index].id,
draggedIndex: index,
startX: e.clientX,
currentX: e.clientX,
@@ -333,11 +370,25 @@ export function TopNavbar({
// Moving right - find the rightmost tab whose midpoint we've passed
for (let i = draggedIndex + 1; i < tabBoundaries.length; i++) {
if (draggedCenter > tabBoundaries[i].mid) {
newTargetIndex = i;
newTargetIndex = i; // Reverted from i + 1 to i
} else {
break;
}
}
// Edge case: if dragged past the last tab, target should be at the very end
const lastTabIndex = tabBoundaries.length - 1;
if (lastTabIndex >= 0) {
// Ensure there's at least one tab
const lastTabEl = tabRefs.current.get(lastTabIndex);
if (lastTabEl) {
const lastTabRect = lastTabEl.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
const lastTabEndInContainer = lastTabRect.right - containerRect.left;
if (currentX > lastTabEndInContainer) {
newTargetIndex = tabBoundaries.length; // Insert at the very end
}
}
}
}
return newTargetIndex;
@@ -378,57 +429,40 @@ export function TopNavbar({
dragState.targetIndex !== null &&
dragState.draggedIndex !== dragState.targetIndex
) {
console.log("Tabs before reorder (IDs and references):");
tabs.forEach((tab, idx) =>
console.log(` [${idx}] ID: ${tab.id}, Ref:`, tab),
);
prevTabsRef.current = tabs; // Store current tabs before reorder
reorderTabs(dragState.draggedIndex, dragState.targetIndex);
// Delay clearing drag state to prevent visual jitter
// This allows the reorder to complete and re-render before removing transforms
setTimeout(() => {
setDragState({
draggedIndex: null,
startX: 0,
currentX: 0,
targetIndex: null,
});
}, 0);
} else {
// No reorder needed, clear immediately
setDragState({
draggedIndex: null,
startX: 0,
currentX: 0,
targetIndex: null,
});
if (dragState.draggedId !== null) {
setJustDroppedTabId(dragState.draggedId);
}
}
// Immediately reset drag state after drop to ensure a single re-render
// with updated tabs and cleared drag state.
setDragState({
draggedId: null,
draggedIndex: null,
startX: 0,
currentX: 0,
targetIndex: null,
});
};
const handleDragEnd = () => {
console.log("Drag end:", dragState);
if (
dragState.draggedIndex !== null &&
dragState.targetIndex !== null &&
dragState.draggedIndex !== dragState.targetIndex
) {
reorderTabs(dragState.draggedIndex, dragState.targetIndex);
// Delay clearing drag state to prevent visual jitter
setTimeout(() => {
setDragState({
draggedIndex: null,
startX: 0,
currentX: 0,
targetIndex: null,
});
}, 0);
} else {
// No reorder needed, clear immediately
setDragState({
draggedIndex: null,
startX: 0,
currentX: 0,
targetIndex: null,
});
}
// Immediately reset drag state. If a drop occurred, handleDrop has already
// initiated the state clear. If the drag was cancelled (e.g., dropped
// outside a valid target), this clears the drag state.
setDragState({
draggedId: null,
draggedIndex: null,
startX: 0,
currentX: 0,
targetIndex: null,
});
};
const isSplitScreenActive =
@@ -492,47 +526,64 @@ export function TopNavbar({
isSplitScreenActive);
const disableClose = (isSplitScreenActive && isActive) || isSplit;
const isDragging = dragState.draggedIndex === index;
const dragOffset = isDragging
const isDraggingThisTab = dragState.draggedIndex === index;
const isTheDraggedTab = tab.id === dragState.draggedId;
const isDroppedAndSnapping = tab.id === justDroppedTabId; // New condition
const dragOffset = isDraggingThisTab
? dragState.currentX - dragState.startX
: 0;
// Diagnostic logs
if (dragState.draggedIndex !== null) {
console.log(
`Tab ID: ${tab.id}, Index: ${index}, isDraggingThisTab: ${isDraggingThisTab}, draggedOriginalIndex: ${dragState.draggedIndex}, currentTargetIndex: ${dragState.targetIndex}, isDroppedAndSnapping: ${isDroppedAndSnapping}`,
);
}
// Calculate transform
let transform = "";
if (isDragging) {
// Dragged tab follows cursor
if (isDraggingThisTab) {
transform = `translateX(${dragOffset}px)`;
} else if (
dragState.draggedIndex !== null &&
dragState.targetIndex !== null
) {
// Other tabs shift to make room
const draggedIndex = dragState.draggedIndex;
const targetIndex = dragState.targetIndex;
const draggedOriginalIndex = dragState.draggedIndex;
const currentTargetIndex = dragState.targetIndex;
// Determine if this tab should shift left or right
if (
draggedIndex < targetIndex &&
index > draggedIndex &&
index <= targetIndex
draggedOriginalIndex < currentTargetIndex && // Dragging rightwards
index > draggedOriginalIndex && // This tab is to the right of the original position
index <= currentTargetIndex // This tab is at or before the target position
) {
// Shifting left
const draggedTabEl = tabRefs.current.get(draggedIndex);
const draggedWidth =
draggedTabEl?.getBoundingClientRect().width || 0;
transform = `translateX(-${draggedWidth + 4}px)`;
// Shift left to make space
const draggedTabWidth =
tabRefs.current
.get(draggedOriginalIndex)
?.getBoundingClientRect().width || 0;
const gap = 4;
transform = `translateX(-${draggedTabWidth + gap}px)`;
} else if (
draggedIndex > targetIndex &&
index >= targetIndex &&
index < draggedIndex
draggedOriginalIndex > currentTargetIndex && // Dragging leftwards
index >= currentTargetIndex && // This tab is at or after the target position
index < draggedOriginalIndex // This tab is to the left of the original position
) {
// Shifting right
const draggedTabEl = tabRefs.current.get(draggedIndex);
const draggedWidth =
draggedTabEl?.getBoundingClientRect().width || 0;
transform = `translateX(${draggedWidth + 4}px)`;
// Shift right to make space
const draggedTabWidth =
tabRefs.current
.get(draggedOriginalIndex)
?.getBoundingClientRect().width || 0;
const gap = 4;
transform = `translateX(${draggedTabWidth + gap}px)`;
}
}
// Diagnostic log for transform
if (dragState.draggedIndex !== null) {
console.log(` Tab ID: ${tab.id}, Transform: ${transform}`);
}
return (
<div
key={tab.id}
@@ -554,10 +605,13 @@ export function TopNavbar({
onDragEnd={handleDragEnd}
style={{
transform,
transition: isDragging ? "none" : "transform 200ms ease-out",
zIndex: isDragging ? 1000 : 1,
transition:
isDraggingThisTab || isDroppedAndSnapping
? "none"
: "transform 200ms ease-out",
zIndex: isDraggingThisTab ? 1000 : 1,
position: "relative",
cursor: isDragging ? "grabbing" : "grab",
cursor: isDraggingThisTab ? "grabbing" : "grab",
userSelect: "none",
WebkitUserSelect: "none",
}}
@@ -592,7 +646,7 @@ export function TopNavbar({
disableActivate={disableActivate}
disableSplit={disableSplit}
disableClose={disableClose}
isDragging={isDragging}
isDragging={isDraggingThisTab}
isDragOver={false}
/>
</div>