Feature gating refactor: modular compilation with minimal feature set

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-22 19:45:18 -03:00
parent 3db87c029d
commit 66abce913f
23 changed files with 273 additions and 102 deletions

View file

@ -10,7 +10,7 @@ features = ["database", "i18n"]
[features]
# ===== SINGLE DEFAULT FEATURE SET =====
default = ["chat", "drive", "tasks", "automation", "cache"]
default = ["chat", "drive", "tasks", "automation", "cache", "directory"]
# ===== COMMUNICATION APPS =====
chat = []
@ -30,14 +30,17 @@ tasks = ["dep:cron", "automation"]
project=["quick-xml"]
goals = []
workspace = []
productivity = ["calendar", "tasks", "project", "goals", "workspace", "cache"]
workspaces = ["workspace"]
tickets = []
billing = []
productivity = ["calendar", "tasks", "project", "goals", "workspaces", "cache"]
# ===== DOCUMENT APPS =====
paper = ["docs", "dep:pdf-extract"]
docs = ["docx-rs", "ooxmlsdk"]
sheet = ["calamine", "spreadsheet-ods"]
slides = ["ooxmlsdk"]
drive = ["dep:aws-config", "dep:aws-sdk-s3", "dep:pdf-extract"]
drive = ["dep:aws-config", "dep:aws-sdk-s3", "dep:aws-smithy-async", "dep:pdf-extract"]
documents = ["paper", "docs", "sheet", "slides", "drive"]
# ===== MEDIA APPS =====
@ -127,7 +130,7 @@ num-format = "0.4"
once_cell = "1.18.0"
rand = "0.9.2"
regex = "1.11"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "multipart", "stream"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "multipart", "stream", "json"] }
serde = { version = "1.0", default-features = false, features = ["derive", "std"] }
serde_json = "1.0"
toml = "0.8"
@ -176,8 +179,9 @@ calamine = { version = "0.26", optional = true }
spreadsheet-ods = { version = "1.0", optional = true }
# File Storage & Drive (drive feature)
aws-config = { version = "1.8.8", default-features = false, optional = true }
aws-sdk-s3 = { version = "1.109.0", default-features = false, optional = true }
aws-config = { version = "1.8.8", default-features = false, features = ["behavior-version-latest", "rt-tokio", "rustls"], optional = true }
aws-sdk-s3 = { version = "1.109.0", default-features = false, features = ["rt-tokio", "rustls"], optional = true }
aws-smithy-async = { version = "1.2", features = ["rt-tokio"], optional = true }
pdf-extract = { version = "0.10.0", optional = true }
quick-xml = { version = "0.37", optional=true, features = ["serialize"] }
flate2 = { version = "1.0", optional = false }

View file

@ -9,7 +9,7 @@ use crate::auto_task::task_manifest::{
use crate::basic::keywords::table_definition::{
generate_create_table_sql, FieldDefinition, TableDefinition,
};
use crate::core::config::ConfigManager;
use crate::core::shared::get_content_type;
use crate::core::shared::models::UserSession;
use crate::core::shared::state::{AgentActivity, AppState};
@ -20,7 +20,7 @@ use diesel::sql_query;
use log::{error, info, trace, warn};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::mpsc;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]

View file

@ -1,4 +1,4 @@
use crate::core::config::ConfigManager;
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use chrono::{DateTime, Utc};

View file

@ -1,7 +1,7 @@
use crate::auto_task::app_generator::AppGenerator;
use crate::auto_task::intent_compiler::IntentCompiler;
use crate::basic::ScriptService;
use crate::core::config::ConfigManager;
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use chrono::{DateTime, Utc};

View file

@ -1,4 +1,4 @@
use crate::core::config::ConfigManager;
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use chrono::{DateTime, Utc};

View file

@ -1,3 +1,4 @@
#[cfg(feature = "llm")]
use crate::llm::LLMProvider;
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
@ -11,6 +12,10 @@ use std::io::Read;
use std::path::PathBuf;
use std::sync::Arc;
// When llm feature is disabled, create a dummy trait for type compatibility
#[cfg(not(feature = "llm"))]
trait LLMProvider: Send + Sync {}
pub fn create_site_keyword(state: &AppState, user: UserSession, engine: &mut Engine) {
let state_clone = state.clone();
let user_clone = user;
@ -42,9 +47,9 @@ pub fn create_site_keyword(state: &AppState, user: UserSession, engine: &mut Eng
let bot_id = user_clone.bot_id.to_string();
#[cfg(feature = "llm")]
let llm: Option<Arc<dyn LLMProvider>> = Some(state_clone.llm_provider.clone());
let llm = Some(state_clone.llm_provider.clone());
#[cfg(not(feature = "llm"))]
let llm: Option<Arc<dyn LLMProvider>> = None;
let llm: Option<()> = None;
let fut = create_site(config, s3, bucket, bot_id, llm, alias, template_dir, prompt);
let result =
@ -56,6 +61,7 @@ pub fn create_site_keyword(state: &AppState, user: UserSession, engine: &mut Eng
.expect("valid syntax registration");
}
#[cfg(feature = "llm")]
async fn create_site(
config: crate::core::config::AppConfig,
s3: Option<std::sync::Arc<aws_sdk_s3::Client>>,
@ -96,6 +102,47 @@ async fn create_site(
Ok(format!("/apps/{}", alias_str))
}
#[cfg(not(feature = "llm"))]
async fn create_site(
config: crate::core::config::AppConfig,
s3: Option<std::sync::Arc<aws_sdk_s3::Client>>,
bucket: String,
bot_id: String,
_llm: Option<()>,
alias: Dynamic,
template_dir: Dynamic,
prompt: Dynamic,
) -> Result<String, Box<dyn Error + Send + Sync>> {
let alias_str = alias.to_string();
let template_dir_str = template_dir.to_string();
let prompt_str = prompt.to_string();
info!(
"CREATE SITE: {} from template {}",
alias_str, template_dir_str
);
let base_path = PathBuf::from(&config.site_path);
let template_path = base_path.join(&template_dir_str);
let combined_content = load_templates(&template_path)?;
let generated_html = generate_html_from_prompt(_llm, &combined_content, &prompt_str).await?;
let drive_path = format!("apps/{}", alias_str);
store_to_drive(s3.as_ref(), &bucket, &bot_id, &drive_path, &generated_html).await?;
let serve_path = base_path.join(&alias_str);
sync_to_serve_path(&serve_path, &generated_html, &template_path)?;
info!(
"CREATE SITE: {} completed, available at /apps/{}",
alias_str, alias_str
);
Ok(format!("/apps/{}", alias_str))
}
fn load_templates(template_path: &std::path::Path) -> Result<String, Box<dyn Error + Send + Sync>> {
let mut combined_content = String::new();
@ -129,6 +176,7 @@ fn load_templates(template_path: &std::path::Path) -> Result<String, Box<dyn Err
Ok(combined_content)
}
#[cfg(feature = "llm")]
async fn generate_html_from_prompt(
llm: Option<Arc<dyn LLMProvider>>,
templates: &str,
@ -196,6 +244,16 @@ OUTPUT: Complete index.html file only, no explanations."#,
Ok(html)
}
#[cfg(not(feature = "llm"))]
async fn generate_html_from_prompt(
_llm: Option<()>,
_templates: &str,
prompt: &str,
) -> Result<String, Box<dyn Error + Send + Sync>> {
debug!("LLM feature not enabled, using placeholder HTML");
Ok(generate_placeholder_html(prompt))
}
fn extract_html_from_response(response: &str) -> String {
let trimmed = response.trim();

View file

@ -27,6 +27,7 @@ pub mod hear_talk;
pub mod http_operations;
pub mod human_approval;
pub mod last;
#[cfg(feature = "llm")]
pub mod llm_keyword;
#[cfg(feature = "llm")]
pub mod llm_macros;

View file

@ -45,6 +45,7 @@ use self::keywords::use_tool::use_tool_keyword;
use self::keywords::use_website::{clear_websites_keyword, use_website_keyword};
use self::keywords::web_data::register_web_data_keywords;
use self::keywords::webhook::webhook_keyword;
#[cfg(feature = "llm")]
use self::keywords::llm_keyword::llm_keyword;
use self::keywords::on::on_keyword;
use self::keywords::print::print_keyword;
@ -132,6 +133,7 @@ impl ScriptService {
first_keyword(&mut engine);
last_keyword(&mut engine);
format_keyword(&mut engine);
#[cfg(feature = "llm")]
llm_keyword(state.clone(), user.clone(), &mut engine);
get_keyword(state.clone(), user.clone(), &mut engine);
set_keyword(&state, user.clone(), &mut engine);

View file

@ -1847,26 +1847,17 @@ VAULT_CACHE_TTL=300
std::env::set_var("SSL_CERT_FILE", ca_cert_path);
}
let timeout_config = aws_config::timeout::TimeoutConfig::builder()
.connect_timeout(std::time::Duration::from_secs(5))
.read_timeout(std::time::Duration::from_secs(30))
.operation_timeout(std::time::Duration::from_secs(30))
.operation_attempt_timeout(std::time::Duration::from_secs(15))
.build();
let retry_config = aws_config::retry::RetryConfig::standard()
.with_max_attempts(2);
let base_config = aws_config::defaults(BehaviorVersion::latest())
// Provide TokioSleep for retry/timeout configs
let base_config = aws_config::from_env()
.endpoint_url(endpoint)
.region("auto")
.region(aws_config::Region::new("auto"))
.credentials_provider(aws_sdk_s3::config::Credentials::new(
access_key, secret_key, None, None, "static",
))
.timeout_config(timeout_config)
.retry_config(retry_config)
.sleep_impl(std::sync::Arc::new(aws_smithy_async::rt::sleep::TokioSleep::new()))
.load()
.await;
let s3_config = aws_sdk_s3::config::Builder::from(&base_config)
.force_path_style(true)
.build();

View file

@ -3,7 +3,9 @@ use crate::core::config::ConfigManager;
#[cfg(feature = "drive")]
use crate::drive::drive_monitor::DriveMonitor;
#[cfg(feature = "llm")]
use crate::llm::llm_models;
#[cfg(feature = "llm")]
use crate::llm::OpenAIClient;
#[cfg(feature = "nvidia")]
use crate::nvidia::get_system_metrics;
@ -70,6 +72,7 @@ impl BotOrchestrator {
Ok(())
}
#[cfg(feature = "llm")]
pub async fn stream_response(
&self,
message: UserMessage,
@ -305,6 +308,33 @@ impl BotOrchestrator {
Ok(())
}
#[cfg(not(feature = "llm"))]
pub async fn stream_response(
&self,
message: UserMessage,
response_tx: mpsc::Sender<BotResponse>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
warn!("LLM feature not enabled, cannot stream response");
let error_response = BotResponse {
bot_id: message.bot_id,
user_id: message.user_id,
session_id: message.session_id,
channel: message.channel,
content: "LLM feature is not enabled in this build".to_string(),
message_type: MessageType::BOT_RESPONSE,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
context_name: None,
context_length: 0,
context_max_length: 0,
};
response_tx.send(error_response).await?;
Ok(())
}
pub async fn get_user_sessions(
&self,
user_id: Uuid,

View file

@ -179,6 +179,7 @@ impl UserProvisioningService {
Ok(())
}
#[cfg(feature = "mail")]
fn setup_email_account(&self, account: &UserAccount) -> Result<()> {
use crate::shared::models::schema::user_email_accounts;
use diesel::prelude::*;
@ -207,6 +208,12 @@ impl UserProvisioningService {
Ok(())
}
#[cfg(not(feature = "mail"))]
fn setup_email_account(&self, _account: &UserAccount) -> Result<()> {
log::debug!("Email feature not enabled, skipping email account setup");
Ok(())
}
fn setup_oauth_config(&self, _user_id: &str, account: &UserAccount) -> Result<()> {
use crate::shared::models::schema::bot_configuration;
use diesel::prelude::*;
@ -305,6 +312,7 @@ impl UserProvisioningService {
Ok(())
}
#[cfg(feature = "mail")]
fn remove_email_config(&self, username: &str) -> Result<()> {
use crate::shared::models::schema::user_email_accounts;
use diesel::prelude::*;
@ -320,4 +328,10 @@ impl UserProvisioningService {
Ok(())
}
#[cfg(not(feature = "mail"))]
fn remove_email_config(&self, _username: &str) -> Result<()> {
log::debug!("Email feature not enabled, skipping email config removal");
Ok(())
}
}

View file

@ -1440,6 +1440,7 @@ pub async fn create_invitation(
let invite_message = payload.message.clone();
let invite_id = new_id;
#[cfg(feature = "mail")]
tokio::spawn(async move {
if let Err(e) = send_invitation_email(&email_to, &invite_role, invite_message.as_deref(), invite_id).await {
warn!("Failed to send invitation email to {}: {}", email_to, e);
@ -1648,12 +1649,15 @@ pub async fn resend_invitation(
match result {
Ok(rows) if rows > 0 => {
// Trigger email resend
let resend_id = id;
tokio::spawn(async move {
if let Err(e) = send_invitation_email_by_id(resend_id).await {
warn!("Failed to resend invitation email for {}: {}", resend_id, e);
}
});
#[cfg(feature = "mail")]
{
let resend_id = id;
tokio::spawn(async move {
if let Err(e) = send_invitation_email_by_id(resend_id).await {
warn!("Failed to resend invitation email for {}: {}", resend_id, e);
}
});
}
(StatusCode::OK, Json(serde_json::json!({
"success": true,

View file

@ -1,7 +1,3 @@
use chrono::{DateTime, Utc};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub mod core;
pub use self::core::*;
@ -10,23 +6,45 @@ pub mod rbac;
pub use self::rbac::*;
#[cfg(feature = "tasks")]
pub mod tasks;
pub mod task_models;
#[cfg(feature = "tasks")]
pub use self::tasks::*;
pub use self::task_models::*;
pub use super::schema;
// Re-export schema tables for convenience, as they were before
// Re-export core schema tables
pub use super::schema::{
basic_tools, bot_configuration, bot_memories, bots, clicks, distribution_lists,
email_auto_responders, email_drafts, email_folders, email_label_assignments, email_labels,
email_rules, email_signatures, email_templates, global_email_signatures, kb_collections,
kb_documents, message_history, organizations, rbac_group_roles, rbac_groups,
basic_tools, bot_configuration, bot_memories, bots, clicks,
message_history, organizations, rbac_group_roles, rbac_groups,
rbac_permissions, rbac_role_permissions, rbac_roles, rbac_user_groups, rbac_user_roles,
scheduled_emails, session_tool_associations, shared_mailbox_members, shared_mailboxes,
system_automations, tasks, user_email_accounts, user_kb_associations, user_login_tokens,
session_tool_associations, system_automations, user_login_tokens,
user_preferences, user_sessions, users,
};
// Re-export feature-gated schema tables
#[cfg(feature = "tasks")]
pub use super::schema::tasks;
#[cfg(feature = "mail")]
pub use super::schema::{
distribution_lists, email_auto_responders, email_drafts, email_folders,
email_label_assignments, email_labels, email_rules, email_signatures,
email_templates, global_email_signatures, scheduled_emails,
shared_mailbox_members, shared_mailboxes, user_email_accounts,
};
#[cfg(feature = "people")]
pub use super::schema::{
crm_accounts, crm_activities, crm_contacts, crm_leads, crm_notes,
crm_opportunities, crm_pipeline_stages, people, people_departments,
people_org_chart, people_person_skills, people_skills, people_team_members,
people_teams, people_time_off,
};
#[cfg(feature = "vectordb")]
pub use super::schema::{
kb_collections, kb_documents, user_kb_associations,
};
pub use botlib::message_types::MessageType;
pub use botlib::models::{ApiResponse, Attachment, BotResponse, Session, Suggestion, UserMessage};

View file

@ -251,3 +251,14 @@ diesel::table! {
is_active -> Bool,
}
}
diesel::allow_tables_to_appear_in_same_query!(
rbac_roles,
rbac_groups,
rbac_permissions,
rbac_role_permissions,
rbac_user_roles,
rbac_user_groups,
rbac_group_roles,
users,
);

View file

@ -1,88 +1,86 @@
// Core (Always available)
mod core;
pub mod core;
pub use self::core::*;
#[cfg(feature = "tasks")]
mod tasks;
#[cfg(feature = "tasks")]
pub use self::tasks::*;
pub mod tasks;
#[cfg(feature = "mail")]
mod mail;
pub mod mail;
#[cfg(feature = "mail")]
pub use self::mail::*;
#[cfg(feature = "people")]
mod people;
pub mod people;
#[cfg(feature = "people")]
pub use self::people::*;
#[cfg(feature = "tickets")]
mod tickets;
pub mod tickets;
#[cfg(feature = "tickets")]
pub use self::tickets::*;
#[cfg(feature = "billing")]
mod billing;
pub mod billing;
#[cfg(feature = "billing")]
pub use self::billing::*;
#[cfg(feature = "attendant")]
mod attendant;
pub mod attendant;
#[cfg(feature = "attendant")]
pub use self::attendant::*;
#[cfg(feature = "calendar")]
mod calendar;
pub mod calendar;
#[cfg(feature = "calendar")]
pub use self::calendar::*;
#[cfg(feature = "goals")]
mod goals;
pub mod goals;
#[cfg(feature = "goals")]
pub use self::goals::*;
#[cfg(feature = "canvas")]
mod canvas;
pub mod canvas;
#[cfg(feature = "canvas")]
pub use self::canvas::*;
#[cfg(feature = "workspaces")]
mod workspaces;
pub mod workspaces;
#[cfg(feature = "workspaces")]
pub use self::workspaces::*;
#[cfg(feature = "social")]
mod social;
pub mod social;
#[cfg(feature = "social")]
pub use self::social::*;
#[cfg(feature = "analytics")]
mod analytics;
pub mod analytics;
#[cfg(feature = "analytics")]
pub use self::analytics::*;
#[cfg(feature = "compliance")]
mod compliance;
pub mod compliance;
#[cfg(feature = "compliance")]
pub use self::compliance::*;
#[cfg(feature = "meet")]
mod meet;
pub mod meet;
#[cfg(feature = "meet")]
pub use self::meet::*;
#[cfg(feature = "research")]
mod research;
pub mod research;
#[cfg(feature = "research")]
pub use self::research::*;
#[cfg(feature = "learn")]
mod learn;
pub mod learn;
#[cfg(feature = "learn")]
pub use self::learn::*;
#[cfg(feature = "project")]
mod project;
pub mod project;
#[cfg(feature = "project")]
pub use self::project::*;

View file

@ -19,3 +19,5 @@ diesel::table! {
completed_at -> Nullable<Timestamptz>,
}
}
pub use self::tasks::*;

View file

@ -7,6 +7,7 @@ use crate::core::session::SessionManager;
use crate::core::shared::analytics::MetricsCollector;
#[cfg(feature = "project")]
use crate::project::ProjectService;
#[cfg(feature = "compliance")]
use crate::legal::LegalService;
use crate::security::auth_provider::AuthProviderRegistry;
use crate::security::jwt::JwtManager;
@ -16,7 +17,7 @@ use crate::core::shared::test_utils::create_mock_auth_service;
#[cfg(all(test, feature = "llm"))]
use crate::core::shared::test_utils::MockLLMProvider;
#[cfg(feature = "directory")]
use crate::core::directory::AuthService;
use crate::directory::AuthService;
#[cfg(feature = "llm")]
use crate::llm::LLMProvider;
use crate::shared::models::BotResponse;
@ -374,6 +375,7 @@ pub struct AppState {
pub task_manifests: Arc<std::sync::RwLock<HashMap<String, TaskManifest>>>,
#[cfg(feature = "project")]
pub project_service: Arc<RwLock<ProjectService>>,
#[cfg(feature = "compliance")]
pub legal_service: Arc<RwLock<LegalService>>,
pub jwt_manager: Option<Arc<JwtManager>>,
pub auth_provider_registry: Option<Arc<AuthProviderRegistry>>,
@ -416,6 +418,7 @@ impl Clone for AppState {
task_manifests: Arc::clone(&self.task_manifests),
#[cfg(feature = "project")]
project_service: Arc::clone(&self.project_service),
#[cfg(feature = "compliance")]
legal_service: Arc::clone(&self.legal_service),
jwt_manager: self.jwt_manager.clone(),
auth_provider_registry: self.auth_provider_registry.clone(),
@ -623,6 +626,7 @@ impl Default for AppState {
task_manifests: Arc::new(std::sync::RwLock::new(HashMap::new())),
#[cfg(feature = "project")]
project_service: Arc::new(RwLock::new(crate::project::ProjectService::new())),
#[cfg(feature = "compliance")]
legal_service: Arc::new(RwLock::new(crate::legal::LegalService::new())),
jwt_manager: None,
auth_provider_registry: None,

View file

@ -36,7 +36,7 @@ const BOOTSTRAP_SECRET_ENV: &str = "GB_BOOTSTRAP_SECRET";
pub struct LoginRequest {
pub email: String,
pub password: String,
pub remember: Option<bool>,
pub remember: Option<String>,
}
#[derive(Debug, Serialize)]
@ -111,13 +111,14 @@ pub struct BootstrapResponse {
pub fn configure() -> Router<Arc<AppState>> {
Router::new()
.route("/api/auth/login", post(login))
.route("/api/auth/logout", post(logout))
.route("/api/auth/me", get(get_current_user))
.route("/api/auth/refresh", post(refresh_token))
.route("/api/auth/2fa/verify", post(verify_2fa))
.route("/api/auth/2fa/resend", post(resend_2fa))
.route("/api/auth/bootstrap", post(bootstrap_admin))
.route("/", get(crate::directory::auth_handler))
.route("/login", post(login))
.route("/logout", post(logout))
.route("/me", get(get_current_user))
.route("/refresh", post(refresh_token))
.route("/2fa/verify", post(verify_2fa))
.route("/2fa/resend", post(resend_2fa))
.route("/bootstrap", post(bootstrap_admin))
}
pub async fn login(

View file

@ -397,7 +397,9 @@ impl DriveMonitor {
if llm_lines.is_empty() {
let _ = config_manager.sync_gbot_config(&self.bot_id, &csv_content);
} else {
#[cfg(feature = "llm")]
use crate::llm::local::ensure_llama_servers_running;
#[cfg(feature = "llm")]
use crate::llm::DynamicLLMProvider;
let mut restart_needed = false;
let mut llm_url_changed = false;
@ -437,6 +439,7 @@ impl DriveMonitor {
}
}
let _ = config_manager.sync_gbot_config(&self.bot_id, &csv_content);
#[cfg(feature = "llm")]
if restart_needed {
if let Err(e) =
ensure_llama_servers_running(Arc::clone(&self.state)).await
@ -444,6 +447,7 @@ impl DriveMonitor {
log::error!("Failed to restart LLaMA servers after llm- config change: {}", e);
}
}
#[cfg(feature = "llm")]
if llm_url_changed {
info!("check_gbot: LLM config changed, updating provider...");
let effective_url = if new_llm_url.is_empty() {

View file

@ -99,7 +99,8 @@ async fn serve_embedded_file(req: Request<Body>) -> Response<Body> {
}
pub fn embedded_ui_router() -> Router {
Router::new().fallback(get(serve_embedded_file))
use axum::routing::any;
Router::new().fallback(any(serve_embedded_file))
}
pub fn has_embedded_ui() -> bool {

View file

@ -12,10 +12,12 @@ static GLOBAL: Jemalloc = Jemalloc;
#[cfg(feature = "automation")]
pub mod auto_task;
pub mod basic;
#[cfg(feature = "billing")]
pub mod billing;
#[cfg(feature = "canvas")]
pub mod canvas;
pub mod channels;
#[cfg(feature = "people")]
pub mod contacts;
pub mod core;
#[cfg(feature = "dashboards")]
@ -27,9 +29,11 @@ pub mod multimodal;
pub mod player;
#[cfg(feature = "people")]
pub mod people;
#[cfg(feature = "billing")]
pub mod products;
pub mod search;
pub mod security;
#[cfg(feature = "tickets")]
pub mod tickets;
#[cfg(feature = "attendant")]
pub mod attendant;
@ -59,8 +63,10 @@ pub mod video;
pub mod monitoring;
#[cfg(feature = "project")]
pub mod project;
#[cfg(feature = "workspaces")]
pub mod workspaces;
pub mod botmodels;
#[cfg(feature = "compliance")]
pub mod legal;
pub mod settings;
@ -224,7 +230,7 @@ use crate::core::bot_database::BotDatabaseManager;
use crate::core::config::AppConfig;
#[cfg(feature = "directory")]
use crate::core::directory::auth_handler;
use crate::directory::auth_handler;
use package_manager::InstallMode;
use session::{create_session, get_session_history, get_sessions, start_session};
@ -437,10 +443,9 @@ async fn run_axum_server(
#[cfg(feature = "directory")]
{
api_router = api_router
.route(ApiUrls::AUTH, get(auth_handler))
.merge(crate::core::directory::api::configure_user_routes())
.merge(crate::directory::router::configure())
.merge(crate::directory::auth_routes::configure());
.nest(ApiUrls::AUTH, crate::directory::auth_routes::configure());
}
#[cfg(feature = "meet")]
@ -520,8 +525,11 @@ api_router = api_router.merge(crate::slides::configure_slides_routes());
api_router = api_router.merge(crate::dashboards::configure_dashboards_routes());
api_router = api_router.merge(crate::dashboards::ui::configure_dashboards_ui_routes());
}
api_router = api_router.merge(crate::legal::configure_legal_routes());
api_router = api_router.merge(crate::legal::ui::configure_legal_ui_routes());
#[cfg(feature = "compliance")]
{
api_router = api_router.merge(crate::legal::configure_legal_routes());
api_router = api_router.merge(crate::legal::ui::configure_legal_ui_routes());
}
#[cfg(feature = "compliance")]
{
api_router = api_router.merge(crate::compliance::configure_compliance_routes());
@ -540,8 +548,11 @@ api_router = api_router.merge(crate::slides::configure_slides_routes());
api_router = api_router.merge(crate::auto_task::configure_autotask_routes());
}
api_router = api_router.merge(crate::core::shared::admin::configure());
api_router = api_router.merge(crate::workspaces::configure_workspaces_routes());
api_router = api_router.merge(crate::workspaces::ui::configure_workspaces_ui_routes());
#[cfg(feature = "workspaces")]
{
api_router = api_router.merge(crate::workspaces::configure_workspaces_routes());
api_router = api_router.merge(crate::workspaces::ui::configure_workspaces_ui_routes());
}
#[cfg(feature = "project")]
{
api_router = api_router.merge(crate::project::configure());
@ -577,14 +588,23 @@ api_router = api_router.merge(crate::email::ui::configure_email_ui_routes());
{
api_router = api_router.merge(crate::meet::ui::configure_meet_ui_routes());
}
api_router = api_router.merge(crate::contacts::crm_ui::configure_crm_routes());
api_router = api_router.merge(crate::contacts::crm::configure_crm_api_routes());
api_router = api_router.merge(crate::billing::billing_ui::configure_billing_routes());
api_router = api_router.merge(crate::billing::api::configure_billing_api_routes());
api_router = api_router.merge(crate::products::configure_products_routes());
api_router = api_router.merge(crate::products::api::configure_products_api_routes());
api_router = api_router.merge(crate::tickets::configure_tickets_routes());
api_router = api_router.merge(crate::tickets::ui::configure_tickets_ui_routes());
#[cfg(feature = "people")]
{
api_router = api_router.merge(crate::contacts::crm_ui::configure_crm_routes());
api_router = api_router.merge(crate::contacts::crm::configure_crm_api_routes());
}
#[cfg(feature = "billing")]
{
api_router = api_router.merge(crate::billing::billing_ui::configure_billing_routes());
api_router = api_router.merge(crate::billing::api::configure_billing_api_routes());
api_router = api_router.merge(crate::products::configure_products_routes());
api_router = api_router.merge(crate::products::api::configure_products_api_routes());
}
#[cfg(feature = "tickets")]
{
api_router = api_router.merge(crate::tickets::configure_tickets_routes());
api_router = api_router.merge(crate::tickets::ui::configure_tickets_ui_routes());
}
#[cfg(feature = "people")]
{
api_router = api_router.merge(crate::people::configure_people_routes());
@ -1155,7 +1175,7 @@ use crate::core::config::ConfigManager;
info!("Loaded Zitadel config from {}: url={}", config_path, base_url);
crate::core::directory::client::ZitadelConfig {
crate::directory::ZitadelConfig {
issuer_url: base_url.to_string(),
issuer: base_url.to_string(),
client_id: client_id.to_string(),
@ -1167,7 +1187,7 @@ use crate::core::config::ConfigManager;
}
} else {
warn!("Failed to parse directory_config.json, using defaults");
crate::core::directory::client::ZitadelConfig {
crate::directory::ZitadelConfig {
issuer_url: "http://localhost:8300".to_string(),
issuer: "http://localhost:8300".to_string(),
client_id: String::new(),
@ -1180,7 +1200,7 @@ use crate::core::config::ConfigManager;
}
} else {
warn!("directory_config.json not found, using default Zitadel config");
crate::core::directory::client::ZitadelConfig {
crate::directory::ZitadelConfig {
issuer_url: "http://localhost:8300".to_string(),
issuer: "http://localhost:8300".to_string(),
client_id: String::new(),
@ -1194,7 +1214,7 @@ use crate::core::config::ConfigManager;
};
#[cfg(feature = "directory")]
let auth_service = Arc::new(tokio::sync::Mutex::new(
crate::core::directory::AuthService::new(zitadel_config.clone()).map_err(|e| std::io::Error::other(format!("Failed to create auth service: {}", e)))?,
crate::directory::AuthService::new(zitadel_config.clone()).map_err(|e| std::io::Error::other(format!("Failed to create auth service: {}", e)))?,
));
#[cfg(feature = "directory")]
@ -1205,22 +1225,22 @@ use crate::core::config::ConfigManager;
Ok(pat_token) => {
let pat_token = pat_token.trim().to_string();
info!("Using admin PAT token for bootstrap authentication");
crate::core::directory::client::ZitadelClient::with_pat_token(zitadel_config, pat_token)
crate::directory::ZitadelClient::with_pat_token(zitadel_config, pat_token)
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client with PAT: {}", e)))?
}
Err(e) => {
warn!("Failed to read admin PAT token: {}, falling back to OAuth2", e);
crate::core::directory::client::ZitadelClient::new(zitadel_config)
crate::directory::ZitadelClient::new(zitadel_config)
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client: {}", e)))?
}
}
} else {
info!("Admin PAT not found, using OAuth2 client credentials for bootstrap");
crate::core::directory::client::ZitadelClient::new(zitadel_config)
crate::directory::ZitadelClient::new(zitadel_config)
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client: {}", e)))?
};
match crate::core::directory::bootstrap::check_and_bootstrap_admin(&bootstrap_client).await {
match crate::directory::bootstrap::check_and_bootstrap_admin(&bootstrap_client).await {
Ok(Some(_)) => {
info!("Bootstrap completed - admin credentials displayed in console");
}
@ -1257,13 +1277,16 @@ use crate::core::config::ConfigManager;
.get_config(&default_bot_id, "llm-key", Some(""))
.unwrap_or_default();
let base_llm_provider =crate::llm::create_llm_provider_from_url(
#[cfg(feature = "llm")]
let base_llm_provider = crate::llm::create_llm_provider_from_url(
&llm_url,
if llm_model.is_empty() { None } else { Some(llm_model.clone()) },
);
#[cfg(feature = "llm")]
let dynamic_llm_provider = Arc::new(crate::llm::DynamicLLMProvider::new(base_llm_provider));
#[cfg(feature = "llm")]
let llm_provider: Arc<dyn crate::llm::LLMProvider> = if let Some(ref cache) = redis_client {
let embedding_url = config_manager
.get_config(
@ -1284,7 +1307,7 @@ use crate::core::config::ConfigManager;
))
as Arc<dyn crate::llm::cache::EmbeddingService>);
let cache_config =crate::llm::cache::CacheConfig {
let cache_config = crate::llm::cache::CacheConfig {
ttl: 3600,
semantic_matching: true,
similarity_threshold: 0.85,
@ -1354,6 +1377,7 @@ use crate::core::config::ConfigManager;
session_manager: session_manager.clone(),
metrics_collector,
task_scheduler,
#[cfg(feature = "llm")]
llm_provider: llm_provider.clone(),
#[cfg(feature = "directory")]
auth_service: auth_service.clone(),
@ -1371,7 +1395,8 @@ use crate::core::config::ConfigManager;
kb_manager: Some(kb_manager.clone()),
task_engine,
extensions: {
let ext =crate::core::shared::state::Extensions::new();
let ext = crate::core::shared::state::Extensions::new();
#[cfg(feature = "llm")]
ext.insert_blocking(Arc::clone(&dynamic_llm_provider));
ext
},
@ -1379,7 +1404,9 @@ use crate::core::config::ConfigManager;
task_progress_broadcast: Some(task_progress_tx),
billing_alert_broadcast: None,
task_manifests: Arc::new(std::sync::RwLock::new(HashMap::new())),
#[cfg(feature = "project")]
project_service: Arc::new(tokio::sync::RwLock::new(crate::project::ProjectService::new())),
#[cfg(feature = "compliance")]
legal_service: Arc::new(tokio::sync::RwLock::new(crate::legal::LegalService::new())),
jwt_manager: None,
auth_provider_registry: None,

View file

@ -950,6 +950,7 @@ pub fn build_default_route_permissions() -> Vec<RoutePermission> {
// Auth routes - login must be anonymous
RoutePermission::new("/api/auth", "GET", "").with_anonymous(true),
RoutePermission::new("/api/auth/login", "POST", "").with_anonymous(true),
RoutePermission::new("/api/auth/bootstrap", "POST", "").with_anonymous(true),
RoutePermission::new("/api/auth/refresh", "POST", "").with_anonymous(true),