2025-12-08 14:08:49 -03:00
|
|
|
pub mod cache;
|
2025-11-22 22:55:35 -03:00
|
|
|
pub mod component;
|
|
|
|
|
pub mod installer;
|
|
|
|
|
pub mod os;
|
|
|
|
|
pub mod setup;
|
2025-12-08 14:08:49 -03:00
|
|
|
pub use cache::{CacheResult, DownloadCache};
|
2025-11-22 22:55:35 -03:00
|
|
|
pub use installer::PackageManager;
|
|
|
|
|
pub mod cli;
|
|
|
|
|
pub mod facade;
|
2026-02-12 21:09:30 +00:00
|
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
2025-11-22 22:55:35 -03:00
|
|
|
pub enum InstallMode {
|
|
|
|
|
Local,
|
|
|
|
|
Container,
|
|
|
|
|
}
|
2025-12-26 08:59:25 -03:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
2025-11-22 22:55:35 -03:00
|
|
|
pub enum OsType {
|
|
|
|
|
Linux,
|
|
|
|
|
MacOS,
|
|
|
|
|
Windows,
|
|
|
|
|
}
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct ComponentInfo {
|
|
|
|
|
pub name: &'static str,
|
|
|
|
|
pub termination_command: &'static str,
|
|
|
|
|
}
|
|
|
|
|
pub fn get_all_components() -> Vec<ComponentInfo> {
|
|
|
|
|
vec![
|
|
|
|
|
ComponentInfo {
|
|
|
|
|
name: "tables",
|
|
|
|
|
termination_command: "postgres",
|
|
|
|
|
},
|
|
|
|
|
ComponentInfo {
|
|
|
|
|
name: "cache",
|
|
|
|
|
termination_command: "redis-server",
|
|
|
|
|
},
|
|
|
|
|
ComponentInfo {
|
|
|
|
|
name: "drive",
|
|
|
|
|
termination_command: "minio",
|
|
|
|
|
},
|
|
|
|
|
ComponentInfo {
|
|
|
|
|
name: "llm",
|
|
|
|
|
termination_command: "llama-server",
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
}
|
2026-03-01 07:31:17 -03:00
|
|
|
|
|
|
|
|
/// Parse Zitadel log file to extract initial admin credentials
|
|
|
|
|
#[cfg(feature = "directory")]
|
|
|
|
|
fn extract_initial_admin_from_log(log_path: &std::path::Path) -> Option<(String, String)> {
|
|
|
|
|
use std::fs;
|
|
|
|
|
|
|
|
|
|
let log_content = fs::read_to_string(log_path).ok()?;
|
|
|
|
|
|
|
|
|
|
// Try different log formats from Zitadel
|
|
|
|
|
// Format 1: "initial admin user created. email: admin@<domain> password: <password>"
|
|
|
|
|
for line in log_content.lines() {
|
|
|
|
|
let line_lower = line.to_lowercase();
|
|
|
|
|
if line_lower.contains("initial admin") || line_lower.contains("admin credentials") {
|
|
|
|
|
// Try to extract email and password
|
|
|
|
|
let email = if let Some(email_start) = line.find("email:") {
|
|
|
|
|
let rest = &line[email_start + 6..];
|
|
|
|
|
rest.trim()
|
|
|
|
|
.split_whitespace()
|
|
|
|
|
.next()
|
|
|
|
|
.map(|s| s.trim_end_matches(',').to_string())
|
|
|
|
|
} else if let Some(email_start) = line.find("Email:") {
|
|
|
|
|
let rest = &line[email_start + 6..];
|
|
|
|
|
rest.trim()
|
|
|
|
|
.split_whitespace()
|
|
|
|
|
.next()
|
|
|
|
|
.map(|s| s.trim_end_matches(',').to_string())
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let password = if let Some(pwd_start) = line.find("password:") {
|
|
|
|
|
let rest = &line[pwd_start + 9..];
|
|
|
|
|
rest.trim()
|
|
|
|
|
.split_whitespace()
|
|
|
|
|
.next()
|
|
|
|
|
.map(|s| s.trim_end_matches(',').to_string())
|
|
|
|
|
} else if let Some(pwd_start) = line.find("Password:") {
|
|
|
|
|
let rest = &line[pwd_start + 9..];
|
|
|
|
|
rest.trim()
|
|
|
|
|
.split_whitespace()
|
|
|
|
|
.next()
|
|
|
|
|
.map(|s| s.trim_end_matches(',').to_string())
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let (Some(email), Some(password)) = (email, password) {
|
|
|
|
|
if !email.is_empty() && !password.is_empty() {
|
|
|
|
|
log::info!("Extracted initial admin credentials from log: {}", email);
|
|
|
|
|
return Some((email, password));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try multiline format
|
|
|
|
|
// Admin credentials:
|
|
|
|
|
// Email: admin@localhost
|
|
|
|
|
// Password: xxxxx
|
|
|
|
|
let lines: Vec<&str> = log_content.lines().collect();
|
|
|
|
|
for i in 0..lines.len().saturating_sub(2) {
|
|
|
|
|
if lines[i].to_lowercase().contains("admin credentials") {
|
|
|
|
|
let mut email = None;
|
|
|
|
|
let mut password = None;
|
|
|
|
|
|
|
|
|
|
for j in (i + 1)..std::cmp::min(i + 5, lines.len()) {
|
|
|
|
|
let line = lines[j];
|
|
|
|
|
if line.contains("Email:") {
|
|
|
|
|
email = line.split("Email:")
|
|
|
|
|
.nth(1)
|
|
|
|
|
.map(|s| s.trim().to_string());
|
|
|
|
|
}
|
|
|
|
|
if line.contains("Password:") {
|
|
|
|
|
password = line.split("Password:")
|
|
|
|
|
.nth(1)
|
|
|
|
|
.map(|s| s.trim().to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let (Some(e), Some(p)) = (email, password) {
|
|
|
|
|
if !e.is_empty() && !p.is_empty() {
|
|
|
|
|
log::info!("Extracted initial admin credentials from multiline log: {}", e);
|
|
|
|
|
return Some((e, p));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Initialize Directory (Zitadel) with default admin user and OAuth application
|
|
|
|
|
/// This should be called after Zitadel has started and is responding
|
|
|
|
|
#[cfg(feature = "directory")]
|
|
|
|
|
pub async fn setup_directory() -> anyhow::Result<crate::core::package_manager::setup::DirectoryConfig> {
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
let stack_path = std::env::var("BOTSERVER_STACK_PATH")
|
|
|
|
|
.unwrap_or_else(|_| "./botserver-stack".to_string());
|
|
|
|
|
|
|
|
|
|
let base_url = "http://localhost:8300".to_string();
|
|
|
|
|
let config_path = PathBuf::from(&stack_path).join("conf/system/directory_config.json");
|
|
|
|
|
|
|
|
|
|
// Check if config already exists
|
|
|
|
|
if config_path.exists() {
|
|
|
|
|
if let Ok(content) = std::fs::read_to_string(&config_path) {
|
|
|
|
|
if let Ok(config) = serde_json::from_str::<crate::core::package_manager::setup::DirectoryConfig>(&content) {
|
|
|
|
|
if !config.client_id.is_empty() && !config.client_secret.is_empty() {
|
|
|
|
|
log::info!("Directory already configured with OAuth client");
|
|
|
|
|
return Ok(config);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to get initial admin credentials from Zitadel log
|
|
|
|
|
let log_path = PathBuf::from(&stack_path).join("logs/zitadel.log");
|
|
|
|
|
let admin_credentials = extract_initial_admin_from_log(&log_path);
|
|
|
|
|
|
|
|
|
|
let mut directory_setup = if let Some((email, password)) = admin_credentials {
|
|
|
|
|
log::info!("Using initial admin credentials from log for OAuth client creation");
|
|
|
|
|
crate::core::package_manager::setup::DirectorySetup::with_admin_credentials(
|
|
|
|
|
base_url,
|
|
|
|
|
config_path.clone(),
|
|
|
|
|
email,
|
|
|
|
|
password,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
log::warn!(
|
|
|
|
|
"Could not extract initial admin credentials from Zitadel log at {}. \
|
|
|
|
|
OAuth client creation may fail. Check if Zitadel has started properly.",
|
|
|
|
|
log_path.display()
|
|
|
|
|
);
|
|
|
|
|
crate::core::package_manager::setup::DirectorySetup::new(
|
|
|
|
|
base_url,
|
|
|
|
|
config_path.clone()
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
directory_setup.initialize().await
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("Failed to initialize directory: {}", e))
|
|
|
|
|
}
|