Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/_components/InstalledScriptsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1376,7 +1376,7 @@ export function InstalledScriptsTab() {
type="text"
value={editFormData.web_ui_ip}
onChange={(e) => handleInputChange('web_ui_ip', e.target.value)}
className="w-32 px-3 py-2 text-sm font-mono border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
className="w-40 px-3 py-2 text-sm font-mono border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
placeholder="IP"
/>
<span className="text-muted-foreground">:</span>
Expand Down
72 changes: 71 additions & 1 deletion src/app/_components/PublicKeyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface PublicKeyModalProps {

export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverIp }: PublicKeyModalProps) {
const [copied, setCopied] = useState(false);
const [commandCopied, setCommandCopied] = useState(false);

if (!isOpen) return null;

Expand Down Expand Up @@ -54,6 +55,42 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
}
};

const handleCopyCommand = async () => {
const command = `echo "${publicKey}" >> ~/.ssh/authorized_keys`;
try {
// Try modern clipboard API first
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(command);
setCommandCopied(true);
setTimeout(() => setCommandCopied(false), 2000);
} else {
// Fallback for older browsers or non-HTTPS
const textArea = document.createElement('textarea');
textArea.value = command;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();

try {
document.execCommand('copy');
setCommandCopied(true);
setTimeout(() => setCommandCopied(false), 2000);
} catch (fallbackError) {
console.error('Fallback copy failed:', fallbackError);
alert('Please manually copy this command:\n\n' + command);
}

document.body.removeChild(textArea);
}
} catch (error) {
console.error('Failed to copy command to clipboard:', error);
alert('Please manually copy this command:\n\n' + command);
}
};

return (
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-card rounded-lg shadow-xl max-w-2xl w-full border border-border">
Expand Down Expand Up @@ -129,11 +166,44 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
<textarea
value={publicKey}
readOnly
className="w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground font-mono text-xs min-h-[120px] resize-none border-border focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
className="w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground font-mono text-xs min-h-[60px] resize-none border-border focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
placeholder="Public key will appear here..."
/>
</div>

{/* Quick Command */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Quick Add Command:</label>
<Button
variant="outline"
size="sm"
onClick={handleCopyCommand}
className="gap-2"
>
{commandCopied ? (
<>
<Check className="h-4 w-4" />
Copied!
</>
) : (
<>
<Copy className="h-4 w-4" />
Copy Command
</>
)}
</Button>
</div>
<div className="p-3 bg-muted/50 rounded-md border border-border">
<code className="text-sm font-mono text-foreground break-all">
echo &quot;{publicKey}&quot; &gt;&gt; ~/.ssh/authorized_keys
</code>
</div>
<p className="text-xs text-muted-foreground">
Copy and paste this command directly into your server terminal to add the key to authorized_keys
</p>
</div>

{/* Footer */}
<div className="flex justify-end gap-3 pt-4 border-t border-border">
<Button variant="outline" onClick={onClose}>
Expand Down
13 changes: 11 additions & 2 deletions src/app/_components/SSHKeyInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK

const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result as string;
let content = e.target?.result as string;

// Auto-trim trailing whitespace and empty lines to fix import issues
content = content.replace(/\n\s*$/, '').trimEnd();

if (validateSSHKey(content)) {
onChange(content);
onError?.('');
Expand Down Expand Up @@ -72,7 +76,12 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
};

const handlePasteChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const content = event.target.value;
let content = event.target.value;

// Auto-trim trailing whitespace and empty lines to fix import issues
// This addresses the common problem where pasted SSH keys have extra whitespace
content = content.replace(/\n\s*$/, '').trimEnd();

onChange(content);

if (content.trim() && !validateSSHKey(content)) {
Expand Down
26 changes: 19 additions & 7 deletions src/app/_components/ServerForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -351,13 +351,25 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
{/* Show generated key status */}
{formData.key_generated && (
<div className="p-3 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-md">
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm font-medium text-green-800 dark:text-green-200">
SSH key pair generated successfully
</span>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm font-medium text-green-800 dark:text-green-200">
SSH key pair generated successfully
</span>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setShowPublicKeyModal(true)}
className="gap-2 border-blue-500/20 text-blue-400 bg-blue-500/10 hover:bg-blue-500/20"
>
<Key className="h-4 w-4" />
View Public Key
</Button>
</div>
<p className="text-xs text-green-700 dark:text-green-300 mt-1">
The private key has been generated and will be saved with the server.
Expand Down