feat: Add SSH key authentication and custom port support (#97)

* feat: Add SSH key authentication and custom port support

- Add SSH key authentication support with three modes: password, key, or both
- Add custom SSH port support (defaults to 22)
- Create SSHKeyInput component with file upload and paste modes
- Update database schema with auth_type, ssh_key, ssh_key_passphrase, and ssh_port columns
- Update TypeScript interfaces to support new authentication fields
- Update SSH services to handle key authentication and custom ports
- Update ServerForm with authentication type selection and SSH port field
- Update API routes with validation for new fields
- Add proper cleanup for temporary SSH key files
- Support for encrypted SSH keys with passphrase protection
- Maintain backward compatibility with existing password-only servers

* fix: Resolve TypeScript build errors and improve type safety

- Replace || operators with ?? (nullish coalescing) for better type safety
- Add proper null checks for password fields in SSH services
- Fix JSDoc type annotations for better TypeScript inference
- Update error object types to use Record<keyof CreateServerData, string>
- Ensure all SSH authentication methods handle optional fields correctly
This commit is contained in:
Michel Roegl-Brunner
2025-10-10 11:54:15 +02:00
committed by GitHub
parent e8be9e7214
commit ff1ab35b46
9 changed files with 984 additions and 141 deletions

View File

@@ -52,16 +52,55 @@ export async function PUT(
}
const body = await request.json();
const { name, ip, user, password }: CreateServerData = body;
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port }: 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 SSH port
if (ssh_port !== undefined && (ssh_port < 1 || ssh_port > 65535)) {
return NextResponse.json(
{ error: 'SSH port must be between 1 and 65535' },
{ status: 400 }
);
}
// Validate authentication based on auth_type
const authType = auth_type ?? 'password';
if (authType === 'password' || authType === 'both') {
if (!password?.trim()) {
return NextResponse.json(
{ error: 'Password is required for password authentication' },
{ status: 400 }
);
}
}
if (authType === 'key' || authType === 'both') {
if (!ssh_key?.trim()) {
return NextResponse.json(
{ error: 'SSH key is required for key authentication' },
{ status: 400 }
);
}
}
// Check if at least one authentication method is provided
if (authType === 'both') {
if (!password?.trim() && !ssh_key?.trim()) {
return NextResponse.json(
{ error: 'At least one authentication method (password or SSH key) is required' },
{ status: 400 }
);
}
}
const db = getDatabase();
// Check if server exists
@@ -73,7 +112,16 @@ export async function PUT(
);
}
const result = db.updateServer(id, { name, ip, user, password });
const result = db.updateServer(id, {
name,
ip,
user,
password,
auth_type: authType,
ssh_key,
ssh_key_passphrase,
ssh_port: ssh_port ?? 22
});
return NextResponse.json(
{

View File

@@ -20,18 +20,66 @@ 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, auth_type, ssh_key, ssh_key_passphrase, ssh_port }: 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 SSH port
if (ssh_port !== undefined && (ssh_port < 1 || ssh_port > 65535)) {
return NextResponse.json(
{ error: 'SSH port must be between 1 and 65535' },
{ status: 400 }
);
}
// Validate authentication based on auth_type
const authType = auth_type ?? 'password';
if (authType === 'password' || authType === 'both') {
if (!password?.trim()) {
return NextResponse.json(
{ error: 'Password is required for password authentication' },
{ status: 400 }
);
}
}
if (authType === 'key' || authType === 'both') {
if (!ssh_key?.trim()) {
return NextResponse.json(
{ error: 'SSH key is required for key authentication' },
{ status: 400 }
);
}
}
// Check if at least one authentication method is provided
if (authType === 'both') {
if (!password?.trim() && !ssh_key?.trim()) {
return NextResponse.json(
{ error: 'At least one authentication method (password or SSH key) is required' },
{ status: 400 }
);
}
}
const db = getDatabase();
const result = db.createServer({ name, ip, user, password });
const result = db.createServer({
name,
ip,
user,
password,
auth_type: authType,
ssh_key,
ssh_key_passphrase,
ssh_port: ssh_port ?? 22
});
return NextResponse.json(
{