Move setup wizard credentials to database storage

Changes setup wizard to save channel secrets (Telegram bot token, HTTP
webhook secret) to PostgreSQL via SecretsStore instead of files.

This enables the WASM channel credential injector to find and inject
the secrets properly, since it reads from the database.

Requires:
- DATABASE_URL to be set
- SECRETS_MASTER_KEY (will generate and display if not set)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-02-04 09:49:11 -08:00
co-authored by Claude Opus 4.5
parent c7f0e8014d
commit 4ab20ff939
3 changed files with 180 additions and 119 deletions
+105 -6
View File
@@ -7,9 +7,14 @@
use std::sync::Arc;
use deadpool_postgres::{Config as PoolConfig, Runtime};
use secrecy::SecretString;
use tokio_postgres::NoTls;
use crate::llm::{SessionConfig, SessionManager};
use crate::secrets::SecretsCrypto;
use crate::settings::Settings;
use crate::setup::channels::{setup_http, setup_telegram};
use crate::setup::channels::{SecretsContext, setup_http, setup_telegram};
use crate::setup::prompts::{
input, print_header, print_info, print_step, print_success, select_many, select_one,
};
@@ -23,6 +28,15 @@ pub enum SetupError {
#[error("Authentication error: {0}")]
Auth(String),
#[error("Database error: {0}")]
Database(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Channel setup error: {0}")]
Channel(String),
#[error("User cancelled")]
Cancelled,
}
@@ -242,6 +256,64 @@ impl SetupWizard {
}
}
/// Initialize secrets context for channel setup.
async fn init_secrets_context(&self) -> Result<SecretsContext, SetupError> {
// Get DATABASE_URL
let database_url = std::env::var("DATABASE_URL").map_err(|_| {
SetupError::Config(
"DATABASE_URL not set. Please set it in .env or environment.".to_string(),
)
})?;
// Get or generate SECRETS_MASTER_KEY
let master_key = match std::env::var("SECRETS_MASTER_KEY") {
Ok(key) => {
if key.len() < 32 {
return Err(SetupError::Config(
"SECRETS_MASTER_KEY must be at least 32 characters".to_string(),
));
}
key
}
Err(_) => {
// Generate a new master key
print_info("SECRETS_MASTER_KEY not set. Generating a new one...");
let key = generate_master_key();
print_info(&format!(
"Generated master key. Add to your .env file:\nSECRETS_MASTER_KEY={}",
key
));
key
}
};
// Create database pool
let mut cfg = PoolConfig::new();
cfg.url = Some(database_url);
cfg.pool = Some(deadpool_postgres::PoolConfig {
max_size: 5,
..Default::default()
});
let pool = cfg
.create_pool(Some(Runtime::Tokio1), NoTls)
.map_err(|e| SetupError::Database(format!("Failed to create pool: {}", e)))?;
// Test connection
let _ = pool
.get()
.await
.map_err(|e| SetupError::Database(format!("Failed to connect to database: {}", e)))?;
print_success("Connected to database");
// Create crypto
let crypto = SecretsCrypto::new(SecretString::from(master_key))
.map_err(|e| SetupError::Config(format!("Invalid master key: {}", e)))?;
Ok(SecretsContext::new(pool, Arc::new(crypto), "default"))
}
/// Step 3: Channel configuration.
async fn step_channels(&mut self) -> Result<(), SetupError> {
let options = [
@@ -252,12 +324,22 @@ impl SetupWizard {
let selected = select_many("Which channels do you want to enable?", &options)?;
// Only initialize secrets context if we need it (HTTP or Telegram selected)
let needs_secrets = selected.contains(&1) || selected.contains(&2);
let secrets = if needs_secrets {
Some(self.init_secrets_context().await?)
} else {
None
};
// HTTP is index 1
if selected.contains(&1) {
println!();
let result = setup_http()?;
self.settings.channels.http_enabled = result.enabled;
self.settings.channels.http_port = Some(result.port);
if let Some(ref ctx) = secrets {
let result = setup_http(ctx).await.map_err(SetupError::Channel)?;
self.settings.channels.http_enabled = result.enabled;
self.settings.channels.http_port = Some(result.port);
}
} else {
self.settings.channels.http_enabled = false;
}
@@ -265,8 +347,10 @@ impl SetupWizard {
// Telegram is index 2
if selected.contains(&2) {
println!();
let result = setup_telegram().await?;
self.settings.channels.telegram_enabled = result.enabled;
if let Some(ref ctx) = secrets {
let result = setup_telegram(ctx).await.map_err(SetupError::Channel)?;
self.settings.channels.telegram_enabled = result.enabled;
}
} else {
self.settings.channels.telegram_enabled = false;
}
@@ -318,6 +402,15 @@ impl SetupWizard {
}
}
/// Generate a random 32-byte master key as hex string.
fn generate_master_key() -> String {
use rand::RngCore;
let mut rng = rand::thread_rng();
let mut bytes = [0u8; 32];
rng.fill_bytes(&mut bytes);
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
impl Default for SetupWizard {
fn default() -> Self {
Self::new()
@@ -344,4 +437,10 @@ mod tests {
let wizard = SetupWizard::with_config(config);
assert!(wizard.config.skip_auth);
}
#[test]
fn test_generate_master_key() {
let key = generate_master_key();
assert_eq!(key.len(), 64); // 32 bytes = 64 hex chars
}
}