use anyhow::Result;
use std::io::{self, Write};
use crate::introspection::{introspect_postgres, introspect_with_sudo, PostgresIntrospection};
use crate::models::{Account, DatabaseAccess, AccountRole, Permission};
use chrono::Utc;
use uuid::Uuid;
pub async fn run_setup() -> Result<()> {
println!("🚀 Welcome to pg-api setup!");
println!("This wizard will help you configure your PostgreSQL API access.\n");
let introspection = connect_to_postgres().await?;
println!("\n✅ Successfully connected to PostgreSQL {}", extract_version(&introspection.version));
println!("Found {} databases and {} users", introspection.databases.len(), introspection.users.len());
display_introspection(&introspection);
let accounts = configure_accounts(&introspection).await?;
save_configuration(accounts).await?;
let config_dir = std::env::var("CONFIG_DIR").unwrap_or_else(|_| "config".to_string());
println!("\n✅ Configuration saved to {}/accounts.json", config_dir);
println!("You can now start pg-api with: cargo run");
Ok(())
}
async fn connect_to_postgres() -> Result<PostgresIntrospection> {
println!("Attempting to connect to PostgreSQL...");
match introspect_with_sudo().await {
Ok(intro) => return Ok(intro),
Err(_) => {
println!("Could not connect using sudo. Let's try with connection details.");
}
}
loop {
let host = prompt("PostgreSQL host (default: localhost): ", "localhost");
let port = prompt("PostgreSQL port (default: 5432): ", "5432");
let username = prompt("PostgreSQL username: ", "");
let password = prompt_password("PostgreSQL password: ")?;
let database = prompt("Database to connect (default: postgres): ", "postgres");
let connection_string = format!(
"host={host} port={port} user={username} password={password} dbname={database}",
);
match introspect_postgres(&connection_string).await {
Ok(intro) => return Ok(intro),
Err(e) => {
println!("❌ Connection failed: {e}");
if !confirm("Try again?")? {
anyhow::bail!("Setup cancelled");
}
}
}
}
}
fn display_introspection(intro: &PostgresIntrospection) {
println!("\n📊 Current PostgreSQL state:");
println!("\nDatabases:");
println!("{:<20} {:<15} {:<10} {:<15}", "Name", "Owner", "Encoding", "Size");
println!("{}", "-".repeat(60));
for db in &intro.databases {
println!("{:<20} {:<15} {:<10} {:<15}",
db.name,
db.owner,
db.encoding,
db.size.as_ref().unwrap_or(&"N/A".to_string())
);
}
println!("\nUsers:");
println!("{:<20} {:<10} {:<10} {:<10}", "Username", "Superuser", "Create DB", "Create Role");
println!("{}", "-".repeat(60));
for user in &intro.users {
println!("{:<20} {:<10} {:<10} {:<10}",
user.username,
if user.is_superuser { "Yes" } else { "No" },
if user.can_create_db { "Yes" } else { "No" },
if user.can_create_role { "Yes" } else { "No" }
);
}
}
async fn configure_accounts(intro: &PostgresIntrospection) -> Result<Vec<Account>> {
let mut accounts = Vec::new();
println!("\n🔧 Account Configuration");
if confirm("Create a superuser API account?")? {
let account = create_superuser_account(intro)?;
accounts.push(account);
}
if confirm("\nWould you like to expose existing PostgreSQL users through the API?")? {
for user in &intro.users {
if user.username == "postgres" {
continue; }
println!("\nUser: {}", user.username);
if confirm(&format!("Expose user '{}' through API?", user.username))? {
let account = create_user_account(user, intro)?;
accounts.push(account);
}
}
}
Ok(accounts)
}
fn create_superuser_account(intro: &PostgresIntrospection) -> Result<Account> {
println!("\nConfiguring superuser account:");
let name = prompt("Account name (default: postgres-superuser): ", "postgres-superuser");
let api_key = generate_api_key();
println!("Generated API key: {api_key}");
println!("⚠️ Save this API key securely, it won't be shown again!");
let mut databases = Vec::new();
println!("\nWhich databases should this superuser have access to?");
for db in &intro.databases {
if confirm(&format!("Include database '{}'?", db.name))? {
let username = prompt(&format!("PostgreSQL username for database '{}': ", db.name), "postgres");
let password = prompt_password(&format!("PostgreSQL password for user '{username}': "))?;
databases.push(DatabaseAccess {
database: db.name.clone(),
username,
password,
permissions: vec![Permission::All],
});
}
}
Ok(Account {
id: format!("acc_{}", Uuid::new_v4().to_string().replace("-", "").chars().take(12).collect::<String>()),
name: name.clone(),
api_key,
instance_id: "default".to_string(),
databases,
role: AccountRole::Owner,
created_at: Utc::now(),
last_used: Utc::now(),
rate_limit: 0, max_connections: 100,
notes: Some(format!("Superuser account for {}", name)),
})
}
fn create_user_account(user: &crate::introspection::UserInfo, intro: &PostgresIntrospection) -> Result<Account> {
println!("\nConfiguring account for user '{}':", user.username);
let name = prompt(&format!("Account name (default: {}): ", user.username), &user.username);
let api_key = generate_api_key();
println!("Generated API key: {api_key}");
let mut databases = Vec::new();
println!("\nDatabases accessible by this user:");
for db_name in &user.databases {
if let Some(db) = intro.databases.iter().find(|d| &d.name == db_name) {
if confirm(&format!("Include database '{}'?", db.name))? {
let password = prompt_password(&format!("PostgreSQL password for user '{}': ", user.username))?;
databases.push(DatabaseAccess {
database: db.name.clone(),
username: user.username.clone(),
password,
permissions: if user.is_superuser {
vec![Permission::All]
} else {
vec![Permission::Select, Permission::Insert, Permission::Update, Permission::Delete]
},
});
}
}
}
let rate_limit: u32 = prompt("Rate limit per minute (default: 1000): ", "1000").parse().unwrap_or(1000);
let max_connections: u32 = prompt("Max connections (default: 10): ", "10").parse().unwrap_or(10);
Ok(Account {
id: format!("acc_{}", Uuid::new_v4().to_string().replace("-", "").chars().take(12).collect::<String>()),
name,
api_key,
instance_id: "default".to_string(),
databases,
role: if user.is_superuser { AccountRole::Owner } else { AccountRole::Developer },
created_at: Utc::now(),
last_used: Utc::now(),
rate_limit,
max_connections,
notes: Some(format!("Account for user {}", user.username)),
})
}
async fn save_configuration(accounts: Vec<Account>) -> Result<()> {
let json = serde_json::to_string_pretty(&accounts)?;
let config_dir = std::env::var("CONFIG_DIR").unwrap_or_else(|_| "config".to_string());
let config_path = std::path::PathBuf::from(&config_dir).join("accounts.json");
tokio::fs::create_dir_all(&config_dir).await?;
tokio::fs::write(config_path, json).await?;
Ok(())
}
fn generate_api_key() -> String {
format!("{}{}",
Uuid::new_v4().simple(),
Uuid::new_v4().simple()
)
}
fn prompt(message: &str, default: &str) -> String {
print!("{message}");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let trimmed = input.trim();
if trimmed.is_empty() && !default.is_empty() {
default.to_string()
} else {
trimmed.to_string()
}
}
fn prompt_password(message: &str) -> Result<String> {
print!("{message}");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}
fn confirm(message: &str) -> Result<bool> {
loop {
print!("{message} (y/n): ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
match input.trim().to_lowercase().as_str() {
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("Please enter 'y' or 'n'"),
}
}
}
fn extract_version(version: &str) -> &str {
version.split_whitespace()
.find(|s| s.starts_with("PostgreSQL"))
.and_then(|s| s.split_whitespace().nth(1))
.unwrap_or("Unknown")
}