"use client"; import { useMemo, useState } from "react"; import { AlertTriangle, CheckCircle2, ChevronDown, ChevronUp, Eye, EyeOff, KeyRound, Loader2, RotateCcw, ServerCog, ShieldCheck, } from "lucide-react"; import type { Server as ServerType, ServerConfigSnapshot, ServerSshAccessUpdateBody, } from "@/lib/api"; import { UserBadge } from "@/app/servers/components/server-config/ServerConfigPrimitives"; interface ServerConfigSshAccessPanelProps { server: ServerType; snapshot: ServerConfigSnapshot; snapshotLoadError?: string | null; canManageSystemAccounts: boolean; actionRunning: boolean; onRequestApplyConfirm: (options: ServerSshAccessUpdateBody) => void; onRequestPasswordConfirm: (options: { username: string; action: "set"; password: string; requireChange: boolean; noticeTab?: "ssh-access"; }) => void; } function PolicySwitch({ checked, disabled = false, label, onChange, }: { checked: boolean; disabled?: boolean; label: string; onChange: (checked: boolean) => void; }) { return ( ); } function SettingRow({ title, description, children, }: { title: string; description: string; children: React.ReactNode; }) { return (
{title} {description}
{children}
); } type LoginPreset = "key-only" | "key-password" | "temporary"; export default function ServerConfigSshAccessPanel({ server, snapshot, snapshotLoadError, canManageSystemAccounts, actionRunning, onRequestApplyConfirm, onRequestPasswordConfirm, }: ServerConfigSshAccessPanelProps) { const current = snapshot.sshAccess; const initialPubkey = current.pubkeyAuthentication ?? true; const initialPassword = current.passwordAuthentication ?? server.authType === "PASSWORD"; const initialRoot = current.permitRootLogin === "prohibit-password" ? "prohibit-password" : server.username === "root" && server.authType === "SSH_KEY" ? "prohibit-password" : "no"; const [publicKeyAuthentication, setPublicKeyAuthentication] = useState(initialPubkey); const [passwordAuthentication, setPasswordAuthentication] = useState(initialPassword); const [permitRootLogin, setPermitRootLogin] = useState< "no" | "prohibit-password" >(initialRoot); const [selectedPreset, setSelectedPreset] = useState(() => current.temporaryRollbackScheduled ? "temporary" : initialPassword ? "key-password" : "key-only", ); const [temporaryDuration, setTemporaryDuration] = useState< 15 | 30 | 60 | 240 >(15); const [advancedOpen, setAdvancedOpen] = useState(false); const [passwordUsername, setPasswordUsername] = useState( () => snapshot.nonRootUsers.find((user) => !user.isSshUser)?.username ?? snapshot.nonRootUsers[0]?.username ?? "", ); const [temporaryPassword, setTemporaryPassword] = useState(""); const [confirmTemporaryPassword, setConfirmTemporaryPassword] = useState(""); const [showTemporaryPassword, setShowTemporaryPassword] = useState(false); const [forcePasswordChange, setForcePasswordChange] = useState(true); const [passwordError, setPasswordError] = useState(null); const activeMethodLocked = (server.authType === "PASSWORD" && !passwordAuthentication) || (server.authType === "SSH_KEY" && !publicKeyAuthentication); const rootConnectionBlocked = server.username === "root" && (permitRootLogin === "no" || server.authType === "PASSWORD"); const noAuthenticationMethod = !publicKeyAuthentication && !passwordAuthentication; const temporaryUnsupported = server.authType !== "SSH_KEY"; const temporaryPresetBlocked = temporaryUnsupported || (current.passwordAuthentication === true && !current.temporaryRollbackScheduled); const temporaryAlreadyEnabled = selectedPreset === "temporary" && current.passwordAuthentication === true; const temporaryMinutes = selectedPreset === "temporary" && !temporaryAlreadyEnabled ? temporaryDuration : null; const hasChanges = publicKeyAuthentication !== current.pubkeyAuthentication || passwordAuthentication !== current.passwordAuthentication || permitRootLogin !== current.permitRootLogin || temporaryMinutes != null || (current.temporaryRollbackScheduled && selectedPreset !== "temporary"); const formBlocked = !current.available || !current.revision || activeMethodLocked || rootConnectionBlocked || noAuthenticationMethod || (selectedPreset === "temporary" && temporaryPresetBlocked); const configPreview = useMemo( () => [ `PubkeyAuthentication ${publicKeyAuthentication ? "yes" : "no"}`, `PasswordAuthentication ${passwordAuthentication ? "yes" : "no"}`, "KbdInteractiveAuthentication no", `PermitRootLogin ${permitRootLogin}`, "PermitEmptyPasswords no", ].join("\n"), [passwordAuthentication, permitRootLogin, publicKeyAuthentication], ); const selectPreset = (preset: LoginPreset) => { if (preset === "temporary" && temporaryPresetBlocked) return; setSelectedPreset(preset); setPublicKeyAuthentication(true); setPasswordAuthentication(preset !== "key-only"); }; const resetForm = () => { setPublicKeyAuthentication(initialPubkey); setPasswordAuthentication(initialPassword); setPermitRootLogin(initialRoot); setSelectedPreset( current.temporaryRollbackScheduled ? "temporary" : initialPassword ? "key-password" : "key-only", ); setTemporaryDuration(15); }; const reviewTemporaryPassword = () => { if (!passwordUsername) { setPasswordError("Select a non-root user first."); return; } if (temporaryPassword.length < 12 || temporaryPassword.length > 128) { setPasswordError("Use a password between 12 and 128 characters."); return; } if ( temporaryPassword.toLowerCase().includes(passwordUsername.toLowerCase()) ) { setPasswordError("The password must not contain the username."); return; } if (temporaryPassword !== confirmTemporaryPassword) { setPasswordError("Password confirmation does not match."); return; } onRequestPasswordConfirm({ username: passwordUsername, action: "set", password: temporaryPassword, requireChange: forcePasswordChange, noticeTab: "ssh-access", }); setTemporaryPassword(""); setConfirmTemporaryPassword(""); setShowTemporaryPassword(false); setPasswordError(null); }; return (
SSH Access

Manage host-wide SSH authentication without changing the SSH port. Every apply is validated, reloaded, reconnected, and audited.

{snapshotLoadError || !current.available ? (
{current.error || snapshotLoadError || "The effective SSH policy is unavailable. No change can be applied."}
) : null}
{[ { label: "Connected user", value: snapshot.currentUser ?? server.username, detail: server.authType === "SSH_KEY" ? "Doktainer uses an SSH key" : "Doktainer uses a password", }, { label: "Connection port", value: String(server.sshPort), detail: "Port changes are excluded", }, { label: "Effective login", value: current.passwordAuthentication ? "Key + password" : "Key only", detail: "Read with sshd -T", }, { label: "Root login", value: current.permitRootLogin === "no" ? "Blocked" : current.permitRootLogin === "prohibit-password" ? "Keys only" : "Allowed", detail: "Empty passwords are always blocked", }, ].map((item) => (
{item.label} {item.value} {item.detail}
))}
{current.temporaryRollbackScheduled ? (
A host-side timer is scheduled to restore the previous SSH policy. Applying a permanent policy will cancel that timer.
) : null}
How should users sign in?

Choose a preset first. Advanced settings remain available below.

{[ { value: "key-only" as const, title: "Key only", badge: "Recommended", description: "Production policy using authorized public keys.", disabled: server.authType === "PASSWORD", }, { value: "key-password" as const, title: "Key and password", badge: "Easy setup", description: "Allow either a public key or account password.", disabled: false, }, { value: "temporary" as const, title: "Temporary password", badge: `${temporaryDuration} min`, description: "Enable password login with host-side rollback.", disabled: temporaryPresetBlocked, }, ].map((preset) => { const selected = selectedPreset === preset.value; return ( ); })}
{selectedPreset === "temporary" && !temporaryAlreadyEnabled ? ( ) : null}
{selectedPreset === "temporary" ? (
Optional account password

Enabling password authentication does not create an account password. Set one here only when the selected user does not already have it.

{snapshot.nonRootUsers.length > 0 ? ( <>
{ setTemporaryPassword(event.target.value); setPasswordError(null); }} placeholder="Password (minimum 12 characters)" style={{ width: "100%", paddingRight: 40 }} />
{ setConfirmTemporaryPassword(event.target.value); setPasswordError(null); }} placeholder="Confirm password" /> {passwordError ? (

{passwordError}

) : null} ) : (

Create a non-root user from the Users tab first.

)}
) : null} {advancedOpen ? (
undefined} />
Managed drop-in {configPreview}
) : null} {(activeMethodLocked || rootConnectionBlocked || noAuthenticationMethod) ? (
{rootConnectionBlocked ? "The saved Doktainer connection still uses root. Move it to a non-root account before disabling root login." : activeMethodLocked ? "The authentication method used by Doktainer must remain enabled." : "At least one authentication method must remain enabled."}
) : null}
{[ "Write /etc/ssh/sshd_config.d/00-doktainer.conf atomically", "Validate combined configuration with sshd -t and sshd -T", "Reload SSH without restarting the daemon", "Verify a fresh connection and rollback on failure", ].map((step) => ( {step} ))}
Public keys remain managed per account from Users → Edit → Login Access. This tab controls the host-wide authentication policy only.
); }