fix(server): enforce numeric ssh_port end-to-end; harden UI input; coerce in API/DB; fix runtime handler import

This commit is contained in:
Michel Roegl-Brunner
2025-10-20 14:12:18 +02:00
parent 2b5882393c
commit 5a73a30a92
3 changed files with 36 additions and 6 deletions

View File

@@ -8,7 +8,18 @@ import stripAnsi from 'strip-ansi';
import { spawn as ptySpawn } from 'node-pty';
import { getSSHExecutionService } from './src/server/ssh-execution-service.js';
import { getDatabase } from './src/server/database-prisma.js';
import { registerGlobalErrorHandlers } from './src/server/logging/globalHandlers.js';
// Fallback minimal global error handlers for Node runtime (avoid TS import)
function registerGlobalErrorHandlers() {
if (registerGlobalErrorHandlers._registered) return;
registerGlobalErrorHandlers._registered = true;
process.on('uncaughtException', (err) => {
console.error('uncaught_exception', err);
});
process.on('unhandledRejection', (reason) => {
console.error('unhandled_rejection', reason);
});
}
registerGlobalErrorHandlers._registered = false;
const dev = process.env.NODE_ENV !== 'production';
const hostname = '0.0.0.0';

View File

@@ -122,7 +122,21 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
const handleChange = (field: keyof CreateServerData) => (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
setFormData(prev => ({ ...prev, [field]: e.target.value }));
// Special handling for numeric ssh_port: keep it strictly numeric
if (field === 'ssh_port') {
const raw = (e.target as HTMLInputElement).value ?? '';
const digitsOnly = raw.replace(/\D+/g, '');
setFormData(prev => ({
...prev,
ssh_port: digitsOnly ? parseInt(digitsOnly, 10) : undefined,
}));
if (errors.ssh_port) {
setErrors(prev => ({ ...prev, ssh_port: undefined }));
}
return;
}
setFormData(prev => ({ ...prev, [field]: (e.target as HTMLInputElement).value }));
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
@@ -246,14 +260,17 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
<input
type="number"
id="ssh_port"
inputMode="numeric"
pattern="[0-9]*"
autoComplete="off"
value={formData.ssh_port ?? 22}
onChange={handleChange('ssh_port')}
className={`w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring ${
errors.ssh_port ? 'border-destructive' : 'border-border'
}`}
placeholder="22"
min="1"
max="65535"
min={1}
max={65535}
/>
{errors.ssh_port && <p className="mt-1 text-sm text-destructive">{errors.ssh_port}</p>}
</div>

View File

@@ -20,6 +20,7 @@ class DatabaseServicePrisma {
// Server CRUD operations
async createServer(serverData: CreateServerData) {
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color, key_generated } = serverData;
const normalizedPort = ssh_port !== undefined ? parseInt(String(ssh_port), 10) : 22;
let ssh_key_path = null;
@@ -38,7 +39,7 @@ class DatabaseServicePrisma {
auth_type: auth_type ?? 'password',
ssh_key,
ssh_key_passphrase,
ssh_port: ssh_port ?? 22,
ssh_port: Number.isNaN(normalizedPort) ? 22 : normalizedPort,
ssh_key_path,
key_generated: Boolean(key_generated),
color,
@@ -60,6 +61,7 @@ class DatabaseServicePrisma {
async updateServer(id: number, serverData: CreateServerData) {
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color, key_generated } = serverData;
const normalizedPort = ssh_port !== undefined ? parseInt(String(ssh_port), 10) : undefined;
// Get existing server to check for key changes
const existingServer = await this.getServerById(id);
@@ -109,7 +111,7 @@ class DatabaseServicePrisma {
auth_type: auth_type ?? 'password',
ssh_key,
ssh_key_passphrase,
ssh_port: ssh_port ?? 22,
ssh_port: normalizedPort ?? 22,
ssh_key_path,
key_generated: key_generated !== undefined ? Boolean(key_generated) : (existingServer?.key_generated ?? false),
color,