From a0fa7bdfbfe0c7d2156ded140d3331bd2d787840 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 06:15:57 +0000 Subject: [PATCH] feat: implement database user creation with password support - Add password field to PostgreSQLInstanceConfig.spec.database - Implement automatic database and user creation during instance setup - Generate secure random passwords using crypto.randomBytes - Create database and user with proper privileges after initdb - Update CLI to display user credentials after instance creation - Enhance connection-string command to include passwords in URIs - Configure default settings for remote connections (bind 0.0.0.0, allow all hosts) - Add connection info display in start command Resolves remote connection issue by ensuring database users exist with passwords Co-authored-by: Samuel --- index.ts | 19 ++++++++- src/config/manager.ts | 4 +- src/config/types.ts | 1 + src/instance/manager.ts | 92 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 4 deletions(-) diff --git a/index.ts b/index.ts index e6dfc80..64a2abc 100644 --- a/index.ts +++ b/index.ts @@ -68,6 +68,8 @@ program console.log(` Version: ${config.spec.version}`); console.log(` Port: ${config.spec.network.port}`); console.log(` Database: ${config.spec.database.name}`); + console.log(` User: ${config.spec.database.owner}`); + console.log(` Password: ${chalk.yellow(config.spec.database.password || 'N/A')}`); console.log(` Data Directory: ${config.spec.storage.dataDirectory}`); console.log(); @@ -133,6 +135,9 @@ program console.log(); console.log(chalk.gray('Connection information:')); console.log(chalk.gray(` psql -h ${config.spec.network.bindAddress} -p ${config.spec.network.port} -U ${config.spec.database.owner} -d ${config.spec.database.name}`)); + if (config.spec.database.password) { + console.log(chalk.gray(` Password: ${config.spec.database.password}`)); + } } } catch (error) { @@ -274,7 +279,10 @@ program if (options.format === 'uri') { const host = config.spec.network.bindAddress === '0.0.0.0' ? 'localhost' : config.spec.network.bindAddress; - const uri = `postgresql://${config.spec.database.owner}@${host}:${config.spec.network.port}/${config.spec.database.name}`; + const userPassword = config.spec.database.password ? + `${config.spec.database.owner}:${config.spec.database.password}` : + config.spec.database.owner; + const uri = `postgresql://${userPassword}@${host}:${config.spec.network.port}/${config.spec.database.name}`; console.log(uri); } else if (options.format === 'env') { const host = config.spec.network.bindAddress === '0.0.0.0' ? 'localhost' : config.spec.network.bindAddress; @@ -282,14 +290,21 @@ program console.log(`PGPORT=${config.spec.network.port}`); console.log(`PGDATABASE=${config.spec.database.name}`); console.log(`PGUSER=${config.spec.database.owner}`); + if (config.spec.database.password) { + console.log(`PGPASSWORD=${config.spec.database.password}`); + } } else if (options.format === 'json') { const host = config.spec.network.bindAddress === '0.0.0.0' ? 'localhost' : config.spec.network.bindAddress; + const userPassword = config.spec.database.password ? + `${config.spec.database.owner}:${config.spec.database.password}` : + config.spec.database.owner; const connectionInfo = { host, port: config.spec.network.port, database: config.spec.database.name, user: config.spec.database.owner, - uri: `postgresql://${config.spec.database.owner}@${host}:${config.spec.network.port}/${config.spec.database.name}` + password: config.spec.database.password, + uri: `postgresql://${userPassword}@${host}:${config.spec.network.port}/${config.spec.database.name}` }; console.log(formatAsJson(connectionInfo)); } else { diff --git a/src/config/manager.ts b/src/config/manager.ts index 249d5b2..eafd240 100644 --- a/src/config/manager.ts +++ b/src/config/manager.ts @@ -190,7 +190,7 @@ export class ConfigManager { version, network: { port: defaultPort, - bindAddress: '127.0.0.1', + bindAddress: '0.0.0.0', maxConnections: 100, }, storage: { @@ -210,7 +210,7 @@ export class ConfigManager { }, authentication: { method: 'md5', - allowedHosts: ['127.0.0.1/32', '::1/128'], + allowedHosts: ['127.0.0.1/32', '::1/128', '0.0.0.0/0'], }, audit: { enabled: false, diff --git a/src/config/types.ts b/src/config/types.ts index ce8cb5a..6657495 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -22,6 +22,7 @@ export interface PostgreSQLInstanceConfig { database: { name: string; owner: string; + password?: string; encoding: string; locale: string; timezone: string; diff --git a/src/instance/manager.ts b/src/instance/manager.ts index abb17a9..82e4bd4 100644 --- a/src/instance/manager.ts +++ b/src/instance/manager.ts @@ -1,5 +1,6 @@ import { spawn, exec } from 'child_process'; import { promisify } from 'util'; +import { randomBytes } from 'crypto'; import { access, mkdir, writeFile, readFile, readdir, rmdir, unlink, stat } from 'fs/promises'; import { join, dirname } from 'path'; import { ConfigManager } from '../config/manager.js'; @@ -57,6 +58,9 @@ export class InstanceManager { // Create socket directory after initdb to avoid conflicts await this.createSocketDirectory(config); + // Create database and user with password + await this.createDatabaseAndUser(config); + // Generate configuration files await this.generateConfigFiles(config); @@ -196,6 +200,94 @@ export class InstanceManager { console.log(` Logs: ${config.spec.storage.logDirectory}`); } + private async createDatabaseAndUser(config: PostgreSQLInstanceConfig): Promise { + // Generate a secure password for the database user + const password = this.generateSecurePassword(); + config.spec.database.password = password; + + // Start PostgreSQL temporarily to create database and user + const postgresPath = await this.findPostgreSQLBinary('postgres', config.spec.version); + const psqlPath = await this.findPostgreSQLBinary('psql', config.spec.version); + + // Start PostgreSQL in background + const tempProcess = spawn(postgresPath, [ + '-D', config.spec.storage.dataDirectory, + '-p', config.spec.network.port.toString(), + '-c', 'listen_addresses=127.0.0.1', + ], { + detached: false, + stdio: 'ignore', + }); + + try { + // Wait for PostgreSQL to start + await this.waitForPostgreSQLReady(config.spec.network.port, config.spec.version); + + // Create the database + await execAsync(`${psqlPath} -h 127.0.0.1 -p ${config.spec.network.port} -U postgres -d postgres -c "CREATE DATABASE \\"${config.spec.database.name}\\""`); + + // Create the user with password + await execAsync(`${psqlPath} -h 127.0.0.1 -p ${config.spec.network.port} -U postgres -d postgres -c "CREATE USER \\"${config.spec.database.owner}\\" WITH PASSWORD '${password}'"`); + + // Grant privileges to the user on the database + await execAsync(`${psqlPath} -h 127.0.0.1 -p ${config.spec.network.port} -U postgres -d postgres -c "GRANT ALL PRIVILEGES ON DATABASE \\"${config.spec.database.name}\\" TO \\"${config.spec.database.owner}\\""`); + + // Grant the user permission to create schemas in the database + await execAsync(`${psqlPath} -h 127.0.0.1 -p ${config.spec.network.port} -U postgres -d "${config.spec.database.name}" -c "GRANT CREATE ON SCHEMA public TO \\"${config.spec.database.owner}\\""`); + + } catch (error) { + throw new Error(`Failed to create database and user: ${error}`); + } finally { + // Stop the temporary PostgreSQL process + if (tempProcess && tempProcess.pid) { + try { + process.kill(tempProcess.pid, 'SIGTERM'); + // Wait for process to exit + await new Promise(resolve => setTimeout(resolve, 2000)); + } catch { + // If SIGTERM doesn't work, try SIGKILL + try { + process.kill(tempProcess.pid, 'SIGKILL'); + } catch { + // Process already dead, ignore + } + } + } + } + } + + private generateSecurePassword(length: number = 16): string { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let password = ''; + + // Use crypto.randomBytes for cryptographically secure random generation + const bytes = randomBytes(length); + + for (let i = 0; i < length; i++) { + password += characters[bytes[i] % characters.length]; + } + + return password; + } + + private async waitForPostgreSQLReady(port: number, version: string, maxAttempts: number = 30): Promise { + const psqlPath = await this.findPostgreSQLBinary('psql', version); + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + await execAsync(`${psqlPath} -h 127.0.0.1 -p ${port} -U postgres -d postgres -c "SELECT 1"`, { + timeout: 2000 + }); + return; // Connection successful + } catch { + // Wait before retrying + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + + throw new Error(`PostgreSQL did not become ready after ${maxAttempts} attempts`); + } + private async createInstanceDirectories(config: PostgreSQLInstanceConfig): Promise { const directories = [ config.spec.storage.dataDirectory,