diff --git a/.gitignore b/.gitignore
index 82687ac..d4a2f72 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,9 @@
db.sqlite
data/settings.db
+# ssh keys (sensitive)
+data/ssh-keys/
+
# next.js
/.next/
/out/
diff --git a/src/app/_components/HelpModal.tsx b/src/app/_components/HelpModal.tsx
index 1cad0ac..2a90531 100644
--- a/src/app/_components/HelpModal.tsx
+++ b/src/app/_components/HelpModal.tsx
@@ -55,8 +55,15 @@ export function HelpModal({ isOpen, onClose, initialSection = 'server-settings'
diff --git a/src/app/_components/PublicKeyModal.tsx b/src/app/_components/PublicKeyModal.tsx
new file mode 100644
index 0000000..ac6d461
--- /dev/null
+++ b/src/app/_components/PublicKeyModal.tsx
@@ -0,0 +1,147 @@
+'use client';
+
+import { useState } from 'react';
+import { X, Copy, Check, Server, Globe } from 'lucide-react';
+import { Button } from './ui/button';
+
+interface PublicKeyModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ publicKey: string;
+ serverName: string;
+ serverIp: string;
+}
+
+export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverIp }: PublicKeyModalProps) {
+ const [copied, setCopied] = useState(false);
+
+ if (!isOpen) return null;
+
+ const handleCopy = async () => {
+ try {
+ // Try modern clipboard API first
+ if (navigator.clipboard && window.isSecureContext) {
+ await navigator.clipboard.writeText(publicKey);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } else {
+ // Fallback for older browsers or non-HTTPS
+ const textArea = document.createElement('textarea');
+ textArea.value = publicKey;
+ textArea.style.position = 'fixed';
+ textArea.style.left = '-999999px';
+ textArea.style.top = '-999999px';
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+
+ try {
+ document.execCommand('copy');
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (fallbackError) {
+ console.error('Fallback copy failed:', fallbackError);
+ // If all else fails, show the key in an alert
+ alert('Please manually copy this key:\n\n' + publicKey);
+ }
+
+ document.body.removeChild(textArea);
+ }
+ } catch (error) {
+ console.error('Failed to copy to clipboard:', error);
+ // Fallback: show the key in an alert
+ alert('Please manually copy this key:\n\n' + publicKey);
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+
+
SSH Public Key
+
Add this key to your server's authorized_keys
+
+
+
+
+
+ {/* Content */}
+
+ {/* Server Info */}
+
+
+
+ {serverName}
+
+
+
+ {serverIp}
+
+
+
+ {/* Instructions */}
+
+
Instructions:
+
+ - Copy the public key below
+ - SSH into your server:
ssh root@{serverIp}
+ - Add the key to authorized_keys:
echo "<paste-key>" >> ~/.ssh/authorized_keys
+ - Set proper permissions:
chmod 600 ~/.ssh/authorized_keys
+
+
+
+ {/* Public Key */}
+
+
+
+
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/_components/ServerForm.tsx b/src/app/_components/ServerForm.tsx
index 06111d1..ae9b6e2 100644
--- a/src/app/_components/ServerForm.tsx
+++ b/src/app/_components/ServerForm.tsx
@@ -4,6 +4,8 @@ import { useState, useEffect } from 'react';
import type { CreateServerData } from '../../types/server';
import { Button } from './ui/button';
import { SSHKeyInput } from './SSHKeyInput';
+import { PublicKeyModal } from './PublicKeyModal';
+import { Key } from 'lucide-react';
interface ServerFormProps {
onSubmit: (data: CreateServerData) => void;
@@ -30,6 +32,11 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
const [errors, setErrors] = useState
>>({});
const [sshKeyError, setSshKeyError] = useState('');
const [colorCodingEnabled, setColorCodingEnabled] = useState(false);
+ const [isGeneratingKey, setIsGeneratingKey] = useState(false);
+ const [showPublicKeyModal, setShowPublicKeyModal] = useState(false);
+ const [generatedPublicKey, setGeneratedPublicKey] = useState('');
+ const [, setIsGeneratedKey] = useState(false);
+ const [, setGeneratedServerId] = useState(null);
useEffect(() => {
const loadColorCodingSetting = async () => {
@@ -75,25 +82,18 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
// Validate authentication based on auth_type
const authType = formData.auth_type ?? 'password';
- if (authType === 'password' || authType === 'both') {
+ if (authType === 'password') {
if (!formData.password?.trim()) {
newErrors.password = 'Password is required for password authentication';
}
}
- if (authType === 'key' || authType === 'both') {
+ if (authType === 'key') {
if (!formData.ssh_key?.trim()) {
newErrors.ssh_key = 'SSH key is required for key authentication';
}
}
- // Check if at least one authentication method is provided
- if (authType === 'both') {
- if (!formData.password?.trim() && !formData.ssh_key?.trim()) {
- newErrors.password = 'At least one authentication method (password or SSH key) is required';
- newErrors.ssh_key = 'At least one authentication method (password or SSH key) is required';
- }
- }
setErrors(newErrors);
return Object.keys(newErrors).length === 0 && !sshKeyError;
@@ -127,6 +127,54 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
+
+ // Reset generated key state when switching auth types
+ if (field === 'auth_type') {
+ setIsGeneratedKey(false);
+ setGeneratedPublicKey('');
+ }
+ };
+
+ const handleGenerateKeyPair = async () => {
+ setIsGeneratingKey(true);
+ try {
+ const response = await fetch('/api/servers/generate-keypair', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to generate key pair');
+ }
+
+ const data = await response.json() as { success: boolean; privateKey?: string; publicKey?: string; serverId?: number; error?: string };
+
+ if (data.success) {
+ const serverId = data.serverId ?? 0;
+ const keyPath = `data/ssh-keys/server_${serverId}_key`;
+
+ setFormData(prev => ({
+ ...prev,
+ ssh_key: data.privateKey ?? '',
+ ssh_key_path: keyPath,
+ key_generated: 1
+ }));
+ setGeneratedPublicKey(data.publicKey ?? '');
+ setGeneratedServerId(serverId);
+ setIsGeneratedKey(true);
+ setShowPublicKeyModal(true);
+ setSshKeyError('');
+ } else {
+ throw new Error(data.error ?? 'Failed to generate key pair');
+ }
+ } catch (error) {
+ console.error('Error generating key pair:', error);
+ setSshKeyError(error instanceof Error ? error.message : 'Failed to generate key pair');
+ } finally {
+ setIsGeneratingKey(false);
+ }
};
const handleSSHKeyChange = (value: string) => {
@@ -137,6 +185,7 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
};
return (
+ <>