Skip to content

Commit be65cee

Browse files
rootclaude
andcommitted
feat: Add SSH key authentication support
- Add SSH key authentication as alternative to password auth - Update database schema to support auth_method and ssh_key fields - Implement secure SSH key handling with temporary files and cleanup - Add dual authentication UI with method selector in server form - Update all SSH services to support both password and key auth - Add comprehensive validation for SSH key format and auth methods - Maintain backward compatibility with existing password auth - Add proper TypeScript types and error handling Resolves issues with servers that have password SSH authentication disabled. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 5582d28 commit be65cee

File tree

7 files changed

+617
-149
lines changed

7 files changed

+617
-149
lines changed

src/app/_components/ServerForm.tsx

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
1717
ip: '',
1818
user: '',
1919
password: '',
20+
ssh_key: '',
21+
auth_method: 'password',
2022
}
2123
);
2224

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

46-
if (!formData.password.trim()) {
47-
newErrors.password = 'Password is required';
48+
// Validate authentication method
49+
if (formData.auth_method === 'password') {
50+
if (!formData.password?.trim()) {
51+
newErrors.password = 'Password is required for password authentication';
52+
}
53+
} else if (formData.auth_method === 'ssh_key') {
54+
if (!formData.ssh_key?.trim()) {
55+
newErrors.ssh_key = 'SSH private key is required for key authentication';
56+
} else {
57+
// Basic SSH key validation
58+
const sshKeyPattern = /^-----BEGIN (RSA|OPENSSH|DSA|EC|ED25519) PRIVATE KEY-----/;
59+
if (!sshKeyPattern.test(formData.ssh_key.trim())) {
60+
newErrors.ssh_key = 'Invalid SSH private key format';
61+
}
62+
}
4863
}
4964

5065
setErrors(newErrors);
@@ -56,7 +71,7 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
5671
if (validateForm()) {
5772
onSubmit(formData);
5873
if (!isEditing) {
59-
setFormData({ name: '', ip: '', user: '', password: '' });
74+
setFormData({ name: '', ip: '', user: '', password: '', ssh_key: '', auth_method: 'password' });
6075
}
6176
}
6277
};
@@ -125,14 +140,45 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
125140
{errors.user && <p className="mt-1 text-sm text-red-600">{errors.user}</p>}
126141
</div>
127142

143+
<div>
144+
<label htmlFor="auth_method" className="block text-sm font-medium text-gray-700 mb-1">
145+
Authentication Method *
146+
</label>
147+
<select
148+
id="auth_method"
149+
value={formData.auth_method}
150+
onChange={(e) => {
151+
const newAuthMethod = e.target.value as 'password' | 'ssh_key';
152+
setFormData(prev => ({
153+
...prev,
154+
auth_method: newAuthMethod,
155+
// Clear the other auth field when switching methods
156+
...(newAuthMethod === 'password' ? { ssh_key: '' } : { password: '' })
157+
}));
158+
// Clear related errors
159+
if (newAuthMethod === 'password') {
160+
setErrors(prev => ({ ...prev, ssh_key: undefined }));
161+
} else {
162+
setErrors(prev => ({ ...prev, password: undefined }));
163+
}
164+
}}
165+
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"
166+
>
167+
<option value="password">Password</option>
168+
<option value="ssh_key">SSH Key</option>
169+
</select>
170+
</div>
171+
</div>
172+
173+
{formData.auth_method === 'password' && (
128174
<div>
129175
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
130176
Password *
131177
</label>
132178
<input
133179
type="password"
134180
id="password"
135-
value={formData.password}
181+
value={formData.password ?? ''}
136182
onChange={handleChange('password')}
137183
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 ${
138184
errors.password ? 'border-red-300' : 'border-gray-300'
@@ -141,6 +187,36 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
141187
/>
142188
{errors.password && <p className="mt-1 text-sm text-red-600">{errors.password}</p>}
143189
</div>
190+
)}
191+
192+
{formData.auth_method === 'ssh_key' && (
193+
<div>
194+
<label htmlFor="ssh_key" className="block text-sm font-medium text-gray-700 mb-1">
195+
SSH Private Key *
196+
</label>
197+
<textarea
198+
id="ssh_key"
199+
value={formData.ssh_key ?? ''}
200+
onChange={(e) => {
201+
setFormData(prev => ({ ...prev, ssh_key: e.target.value }));
202+
if (errors.ssh_key) {
203+
setErrors(prev => ({ ...prev, ssh_key: undefined }));
204+
}
205+
}}
206+
rows={8}
207+
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 ${
208+
errors.ssh_key ? 'border-red-300' : 'border-gray-300'
209+
}`}
210+
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----&#10;...&#10;-----END OPENSSH PRIVATE KEY-----"
211+
/>
212+
{errors.ssh_key && <p className="mt-1 text-sm text-red-600">{errors.ssh_key}</p>}
213+
<p className="mt-1 text-xs text-gray-500">
214+
Paste your SSH private key here. Make sure it&apos;s in OpenSSH format and matches a public key installed on the target server.
215+
</p>
216+
</div>
217+
)}
218+
219+
<div className="grid grid-cols-1">{/* This div ensures proper layout continuation */}
144220
</div>
145221

146222
<div className="flex justify-end space-x-3 pt-4">

src/app/api/servers/[id]/route.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,27 @@ export async function PUT(
5252
}
5353

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

5757
// Validate required fields
58-
if (!name || !ip || !user || !password) {
58+
if (!name || !ip || !user) {
5959
return NextResponse.json(
60-
{ error: 'Missing required fields' },
60+
{ error: 'Missing required fields: name, ip, and user are required' },
61+
{ status: 400 }
62+
);
63+
}
64+
65+
// Validate authentication method and credentials
66+
const authMethodValue = auth_method ?? 'password';
67+
if (authMethodValue === 'password' && !password) {
68+
return NextResponse.json(
69+
{ error: 'Password is required for password authentication' },
70+
{ status: 400 }
71+
);
72+
}
73+
if (authMethodValue === 'ssh_key' && !ssh_key) {
74+
return NextResponse.json(
75+
{ error: 'SSH key is required for key authentication' },
6176
{ status: 400 }
6277
);
6378
}
@@ -73,7 +88,7 @@ export async function PUT(
7388
);
7489
}
7590

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

7893
return NextResponse.json(
7994
{

src/app/api/servers/route.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,33 @@ export async function GET() {
2020
export async function POST(request: NextRequest) {
2121
try {
2222
const body = await request.json();
23-
const { name, ip, user, password }: CreateServerData = body;
23+
const { name, ip, user, password, ssh_key, auth_method }: CreateServerData = body;
2424

2525
// Validate required fields
26-
if (!name || !ip || !user || !password) {
26+
if (!name || !ip || !user) {
2727
return NextResponse.json(
28-
{ error: 'Missing required fields' },
28+
{ error: 'Missing required fields: name, ip, and user are required' },
29+
{ status: 400 }
30+
);
31+
}
32+
33+
// Validate authentication method and credentials
34+
const authMethodValue = auth_method ?? 'password';
35+
if (authMethodValue === 'password' && !password) {
36+
return NextResponse.json(
37+
{ error: 'Password is required for password authentication' },
38+
{ status: 400 }
39+
);
40+
}
41+
if (authMethodValue === 'ssh_key' && !ssh_key) {
42+
return NextResponse.json(
43+
{ error: 'SSH key is required for key authentication' },
2944
{ status: 400 }
3045
);
3146
}
3247

3348
const db = getDatabase();
34-
const result = db.createServer({ name, ip, user, password });
49+
const result = db.createServer({ name, ip, user, password, ssh_key, auth_method: authMethodValue });
3550

3651
return NextResponse.json(
3752
{

src/server/database.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ class DatabaseService {
1616
name TEXT NOT NULL UNIQUE,
1717
ip TEXT NOT NULL,
1818
user TEXT NOT NULL,
19-
password TEXT NOT NULL,
19+
password TEXT,
20+
ssh_key TEXT,
21+
auth_method TEXT NOT NULL DEFAULT 'password' CHECK(auth_method IN ('password', 'ssh_key')),
2022
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
2123
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
2224
)
@@ -53,12 +55,12 @@ class DatabaseService {
5355
* @param {import('../types/server').CreateServerData} serverData
5456
*/
5557
createServer(serverData) {
56-
const { name, ip, user, password } = serverData;
58+
const { name, ip, user, password, ssh_key, auth_method } = serverData;
5759
const stmt = this.db.prepare(`
58-
INSERT INTO servers (name, ip, user, password)
59-
VALUES (?, ?, ?, ?)
60+
INSERT INTO servers (name, ip, user, password, ssh_key, auth_method)
61+
VALUES (?, ?, ?, ?, ?, ?)
6062
`);
61-
return stmt.run(name, ip, user, password);
63+
return stmt.run(name, ip, user, password || null, ssh_key || null, auth_method || 'password');
6264
}
6365

6466
getAllServers() {
@@ -79,13 +81,13 @@ class DatabaseService {
7981
* @param {import('../types/server').CreateServerData} serverData
8082
*/
8183
updateServer(id, serverData) {
82-
const { name, ip, user, password } = serverData;
84+
const { name, ip, user, password, ssh_key, auth_method } = serverData;
8385
const stmt = this.db.prepare(`
84-
UPDATE servers
85-
SET name = ?, ip = ?, user = ?, password = ?
86+
UPDATE servers
87+
SET name = ?, ip = ?, user = ?, password = ?, ssh_key = ?, auth_method = ?
8688
WHERE id = ?
8789
`);
88-
return stmt.run(name, ip, user, password, id);
90+
return stmt.run(name, ip, user, password || null, ssh_key || null, auth_method || 'password', id);
8991
}
9092

9193
/**

0 commit comments

Comments
 (0)