Skip to content
Open
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
84 changes: 80 additions & 4 deletions src/app/_components/ServerForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
ip: '',
user: '',
password: '',
ssh_key: '',
auth_method: 'password',
}
);

Expand All @@ -43,8 +45,21 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
newErrors.user = 'Username is required';
}

if (!formData.password.trim()) {
newErrors.password = 'Password is required';
// Validate authentication method
if (formData.auth_method === 'password') {
if (!formData.password?.trim()) {
newErrors.password = 'Password is required for password authentication';
}
} else if (formData.auth_method === 'ssh_key') {
if (!formData.ssh_key?.trim()) {
newErrors.ssh_key = 'SSH private key is required for key authentication';
} else {
// Basic SSH key validation
const sshKeyPattern = /^-----BEGIN (RSA|OPENSSH|DSA|EC|ED25519) PRIVATE KEY-----/;
if (!sshKeyPattern.test(formData.ssh_key.trim())) {
newErrors.ssh_key = 'Invalid SSH private key format';
}
}
}

setErrors(newErrors);
Expand All @@ -56,7 +71,7 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
if (validateForm()) {
onSubmit(formData);
if (!isEditing) {
setFormData({ name: '', ip: '', user: '', password: '' });
setFormData({ name: '', ip: '', user: '', password: '', ssh_key: '', auth_method: 'password' });
}
}
};
Expand Down Expand Up @@ -125,14 +140,45 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
{errors.user && <p className="mt-1 text-sm text-red-600">{errors.user}</p>}
</div>

<div>
<label htmlFor="auth_method" className="block text-sm font-medium text-gray-700 mb-1">
Authentication Method *
</label>
<select
id="auth_method"
value={formData.auth_method}
onChange={(e) => {
const newAuthMethod = e.target.value as 'password' | 'ssh_key';
setFormData(prev => ({
...prev,
auth_method: newAuthMethod,
// Clear the other auth field when switching methods
...(newAuthMethod === 'password' ? { ssh_key: '' } : { password: '' })
}));
// Clear related errors
if (newAuthMethod === 'password') {
setErrors(prev => ({ ...prev, ssh_key: undefined }));
} else {
setErrors(prev => ({ ...prev, password: undefined }));
}
}}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="password">Password</option>
<option value="ssh_key">SSH Key</option>
</select>
</div>
</div>

{formData.auth_method === 'password' && (
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Password *
</label>
<input
type="password"
id="password"
value={formData.password}
value={formData.password ?? ''}
onChange={handleChange('password')}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
errors.password ? 'border-red-300' : 'border-gray-300'
Expand All @@ -141,6 +187,36 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
/>
{errors.password && <p className="mt-1 text-sm text-red-600">{errors.password}</p>}
</div>
)}

{formData.auth_method === 'ssh_key' && (
<div>
<label htmlFor="ssh_key" className="block text-sm font-medium text-gray-700 mb-1">
SSH Private Key *
</label>
<textarea
id="ssh_key"
value={formData.ssh_key ?? ''}
onChange={(e) => {
setFormData(prev => ({ ...prev, ssh_key: e.target.value }));
if (errors.ssh_key) {
setErrors(prev => ({ ...prev, ssh_key: undefined }));
}
}}
rows={8}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm ${
errors.ssh_key ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----&#10;...&#10;-----END OPENSSH PRIVATE KEY-----"
/>
{errors.ssh_key && <p className="mt-1 text-sm text-red-600">{errors.ssh_key}</p>}
<p className="mt-1 text-xs text-gray-500">
Paste your SSH private key here. Make sure it&apos;s in OpenSSH format and matches a public key installed on the target server.
</p>
</div>
)}

<div className="grid grid-cols-1">{/* This div ensures proper layout continuation */}
</div>

<div className="flex justify-end space-x-3 pt-4">
Expand Down
23 changes: 19 additions & 4 deletions src/app/api/servers/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,27 @@ export async function PUT(
}

const body = await request.json();
const { name, ip, user, password }: CreateServerData = body;
const { name, ip, user, password, ssh_key, auth_method }: CreateServerData = body;

// Validate required fields
if (!name || !ip || !user || !password) {
if (!name || !ip || !user) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ error: 'Missing required fields: name, ip, and user are required' },
{ status: 400 }
);
}

// Validate authentication method and credentials
const authMethodValue = auth_method ?? 'password';
if (authMethodValue === 'password' && !password) {
return NextResponse.json(
{ error: 'Password is required for password authentication' },
{ status: 400 }
);
}
if (authMethodValue === 'ssh_key' && !ssh_key) {
return NextResponse.json(
{ error: 'SSH key is required for key authentication' },
{ status: 400 }
);
}
Expand All @@ -73,7 +88,7 @@ export async function PUT(
);
}

const result = db.updateServer(id, { name, ip, user, password });
const result = db.updateServer(id, { name, ip, user, password, ssh_key, auth_method: authMethodValue });

return NextResponse.json(
{
Expand Down
23 changes: 19 additions & 4 deletions src/app/api/servers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,33 @@ export async function GET() {
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { name, ip, user, password }: CreateServerData = body;
const { name, ip, user, password, ssh_key, auth_method }: CreateServerData = body;

// Validate required fields
if (!name || !ip || !user || !password) {
if (!name || !ip || !user) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ error: 'Missing required fields: name, ip, and user are required' },
{ status: 400 }
);
}

// Validate authentication method and credentials
const authMethodValue = auth_method ?? 'password';
if (authMethodValue === 'password' && !password) {
return NextResponse.json(
{ error: 'Password is required for password authentication' },
{ status: 400 }
);
}
if (authMethodValue === 'ssh_key' && !ssh_key) {
return NextResponse.json(
{ error: 'SSH key is required for key authentication' },
{ status: 400 }
);
}

const db = getDatabase();
const result = db.createServer({ name, ip, user, password });
const result = db.createServer({ name, ip, user, password, ssh_key, auth_method: authMethodValue });

return NextResponse.json(
{
Expand Down
20 changes: 11 additions & 9 deletions src/server/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ class DatabaseService {
name TEXT NOT NULL UNIQUE,
ip TEXT NOT NULL,
user TEXT NOT NULL,
password TEXT NOT NULL,
password TEXT,
ssh_key TEXT,
auth_method TEXT NOT NULL DEFAULT 'password' CHECK(auth_method IN ('password', 'ssh_key')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
Expand Down Expand Up @@ -53,12 +55,12 @@ class DatabaseService {
* @param {import('../types/server').CreateServerData} serverData
*/
createServer(serverData) {
const { name, ip, user, password } = serverData;
const { name, ip, user, password, ssh_key, auth_method } = serverData;
const stmt = this.db.prepare(`
INSERT INTO servers (name, ip, user, password)
VALUES (?, ?, ?, ?)
INSERT INTO servers (name, ip, user, password, ssh_key, auth_method)
VALUES (?, ?, ?, ?, ?, ?)
`);
return stmt.run(name, ip, user, password);
return stmt.run(name, ip, user, password || null, ssh_key || null, auth_method || 'password');
}

getAllServers() {
Expand All @@ -79,13 +81,13 @@ class DatabaseService {
* @param {import('../types/server').CreateServerData} serverData
*/
updateServer(id, serverData) {
const { name, ip, user, password } = serverData;
const { name, ip, user, password, ssh_key, auth_method } = serverData;
const stmt = this.db.prepare(`
UPDATE servers
SET name = ?, ip = ?, user = ?, password = ?
UPDATE servers
SET name = ?, ip = ?, user = ?, password = ?, ssh_key = ?, auth_method = ?
WHERE id = ?
`);
return stmt.run(name, ip, user, password, id);
return stmt.run(name, ip, user, password || null, ssh_key || null, auth_method || 'password', id);
}

/**
Expand Down
Loading