@@ -4,6 +4,8 @@ import { useState, useEffect } from 'react';
44import type { CreateServerData } from '../../types/server' ;
55import { Button } from './ui/button' ;
66import { SSHKeyInput } from './SSHKeyInput' ;
7+ import { PublicKeyModal } from './PublicKeyModal' ;
8+ import { Key } from 'lucide-react' ;
79
810interface ServerFormProps {
911 onSubmit : ( data : CreateServerData ) => void ;
@@ -30,6 +32,11 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
3032 const [ errors , setErrors ] = useState < Partial < Record < keyof CreateServerData , string > > > ( { } ) ;
3133 const [ sshKeyError , setSshKeyError ] = useState < string > ( '' ) ;
3234 const [ colorCodingEnabled , setColorCodingEnabled ] = useState ( false ) ;
35+ const [ isGeneratingKey , setIsGeneratingKey ] = useState ( false ) ;
36+ const [ showPublicKeyModal , setShowPublicKeyModal ] = useState ( false ) ;
37+ const [ generatedPublicKey , setGeneratedPublicKey ] = useState ( '' ) ;
38+ const [ , setIsGeneratedKey ] = useState ( false ) ;
39+ const [ , setGeneratedServerId ] = useState < number | null > ( null ) ;
3340
3441 useEffect ( ( ) => {
3542 const loadColorCodingSetting = async ( ) => {
@@ -75,25 +82,18 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
7582 // Validate authentication based on auth_type
7683 const authType = formData . auth_type ?? 'password' ;
7784
78- if ( authType === 'password' || authType === 'both' ) {
85+ if ( authType === 'password' ) {
7986 if ( ! formData . password ?. trim ( ) ) {
8087 newErrors . password = 'Password is required for password authentication' ;
8188 }
8289 }
8390
84- if ( authType === 'key' || authType === 'both' ) {
91+ if ( authType === 'key' ) {
8592 if ( ! formData . ssh_key ?. trim ( ) ) {
8693 newErrors . ssh_key = 'SSH key is required for key authentication' ;
8794 }
8895 }
8996
90- // Check if at least one authentication method is provided
91- if ( authType === 'both' ) {
92- if ( ! formData . password ?. trim ( ) && ! formData . ssh_key ?. trim ( ) ) {
93- newErrors . password = 'At least one authentication method (password or SSH key) is required' ;
94- newErrors . ssh_key = 'At least one authentication method (password or SSH key) is required' ;
95- }
96- }
9797
9898 setErrors ( newErrors ) ;
9999 return Object . keys ( newErrors ) . length === 0 && ! sshKeyError ;
@@ -127,6 +127,54 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
127127 if ( errors [ field ] ) {
128128 setErrors ( prev => ( { ...prev , [ field ] : undefined } ) ) ;
129129 }
130+
131+ // Reset generated key state when switching auth types
132+ if ( field === 'auth_type' ) {
133+ setIsGeneratedKey ( false ) ;
134+ setGeneratedPublicKey ( '' ) ;
135+ }
136+ } ;
137+
138+ const handleGenerateKeyPair = async ( ) => {
139+ setIsGeneratingKey ( true ) ;
140+ try {
141+ const response = await fetch ( '/api/servers/generate-keypair' , {
142+ method : 'POST' ,
143+ headers : {
144+ 'Content-Type' : 'application/json' ,
145+ } ,
146+ } ) ;
147+
148+ if ( ! response . ok ) {
149+ throw new Error ( 'Failed to generate key pair' ) ;
150+ }
151+
152+ const data = await response . json ( ) as { success : boolean ; privateKey ?: string ; publicKey ?: string ; serverId ?: number ; error ?: string } ;
153+
154+ if ( data . success ) {
155+ const serverId = data . serverId ?? 0 ;
156+ const keyPath = `data/ssh-keys/server_${ serverId } _key` ;
157+
158+ setFormData ( prev => ( {
159+ ...prev ,
160+ ssh_key : data . privateKey ?? '' ,
161+ ssh_key_path : keyPath ,
162+ key_generated : 1
163+ } ) ) ;
164+ setGeneratedPublicKey ( data . publicKey ?? '' ) ;
165+ setGeneratedServerId ( serverId ) ;
166+ setIsGeneratedKey ( true ) ;
167+ setShowPublicKeyModal ( true ) ;
168+ setSshKeyError ( '' ) ;
169+ } else {
170+ throw new Error ( data . error ?? 'Failed to generate key pair' ) ;
171+ }
172+ } catch ( error ) {
173+ console . error ( 'Error generating key pair:' , error ) ;
174+ setSshKeyError ( error instanceof Error ? error . message : 'Failed to generate key pair' ) ;
175+ } finally {
176+ setIsGeneratingKey ( false ) ;
177+ }
130178 } ;
131179
132180 const handleSSHKeyChange = ( value : string ) => {
@@ -137,6 +185,7 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
137185 } ;
138186
139187 return (
188+ < >
140189 < form onSubmit = { handleSubmit } className = "space-y-6" >
141190 < div className = "grid grid-cols-1 sm:grid-cols-2 gap-4" >
142191 < div >
@@ -221,7 +270,6 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
221270 >
222271 < option value = "password" > Password Only</ option >
223272 < option value = "key" > SSH Key Only</ option >
224- < option value = "both" > Both Password & SSH Key </ option >
225273 </ select >
226274 </ div >
227275
@@ -247,10 +295,10 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
247295 </ div >
248296
249297 { /* Password Authentication */ }
250- { ( formData . auth_type === 'password' || formData . auth_type === 'both' ) && (
298+ { formData . auth_type === 'password' && (
251299 < div >
252300 < label htmlFor = "password" className = "block text-sm font-medium text-muted-foreground mb-1" >
253- Password { formData . auth_type === 'both' ? '(Optional)' : '*' }
301+ Password *
254302 </ label >
255303 < input
256304 type = "password"
@@ -267,19 +315,55 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
267315 ) }
268316
269317 { /* SSH Key Authentication */ }
270- { ( formData . auth_type === 'key' || formData . auth_type === 'both' ) && (
318+ { formData . auth_type === 'key' && (
271319 < div className = "space-y-4" >
272320 < div >
273- < label className = "block text-sm font-medium text-muted-foreground mb-1" >
274- SSH Private Key { formData . auth_type === 'both' ? '(Optional)' : '*' }
275- </ label >
276- < SSHKeyInput
277- value = { formData . ssh_key ?? '' }
278- onChange = { handleSSHKeyChange }
279- onError = { setSshKeyError }
280- />
281- { errors . ssh_key && < p className = "mt-1 text-sm text-destructive" > { errors . ssh_key } </ p > }
282- { sshKeyError && < p className = "mt-1 text-sm text-destructive" > { sshKeyError } </ p > }
321+ < div className = "flex items-center justify-between mb-1" >
322+ < label className = "block text-sm font-medium text-muted-foreground" >
323+ SSH Private Key *
324+ </ label >
325+ < Button
326+ type = "button"
327+ variant = "outline"
328+ size = "sm"
329+ onClick = { handleGenerateKeyPair }
330+ disabled = { isGeneratingKey }
331+ className = "gap-2"
332+ >
333+ < Key className = "h-4 w-4" />
334+ { isGeneratingKey ? 'Generating...' : 'Generate Key Pair' }
335+ </ Button >
336+ </ div >
337+
338+ { /* Show manual key input only if no key has been generated */ }
339+ { ! formData . key_generated && (
340+ < >
341+ < SSHKeyInput
342+ value = { formData . ssh_key ?? '' }
343+ onChange = { handleSSHKeyChange }
344+ onError = { setSshKeyError }
345+ />
346+ { errors . ssh_key && < p className = "mt-1 text-sm text-destructive" > { errors . ssh_key } </ p > }
347+ { sshKeyError && < p className = "mt-1 text-sm text-destructive" > { sshKeyError } </ p > }
348+ </ >
349+ ) }
350+
351+ { /* Show generated key status */ }
352+ { formData . key_generated && (
353+ < div className = "p-3 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-md" >
354+ < div className = "flex items-center gap-2" >
355+ < svg className = "w-4 h-4 text-green-600 dark:text-green-400" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
356+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M5 13l4 4L19 7" />
357+ </ svg >
358+ < span className = "text-sm font-medium text-green-800 dark:text-green-200" >
359+ SSH key pair generated successfully
360+ </ span >
361+ </ div >
362+ < p className = "text-xs text-green-700 dark:text-green-300 mt-1" >
363+ The private key has been generated and will be saved with the server.
364+ </ p >
365+ </ div >
366+ ) }
283367 </ div >
284368
285369 < div >
@@ -323,6 +407,16 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
323407 </ Button >
324408 </ div >
325409 </ form >
410+
411+ { /* Public Key Modal */ }
412+ < PublicKeyModal
413+ isOpen = { showPublicKeyModal }
414+ onClose = { ( ) => setShowPublicKeyModal ( false ) }
415+ publicKey = { generatedPublicKey }
416+ serverName = { formData . name || 'New Server' }
417+ serverIp = { formData . ip }
418+ />
419+ </ >
326420 ) ;
327421}
328422
0 commit comments