//! Application State //! //! Holds shared state for the application. use axum::extract::FromRef; use axum_extra::extract::cookie::Key; #[cfg(feature = "auth-jwt")] use infra::auth::jwt::{JwtConfig, JwtValidator}; #[cfg(feature = "auth-oidc")] use infra::auth::oidc::OidcService; use std::collections::VecDeque; #[cfg(feature = "local-files")] use std::collections::HashMap; use std::sync::{Arc, Mutex}; use tokio::sync::broadcast; use crate::config::Config; use crate::events::EventBus; use crate::log_layer::LogLine; use domain::{ActivityLogRepository, ChannelService, IAppSettingsRepository, ILibraryRepository, LibrarySyncAdapter, ProviderConfigRepository, ScheduleEngineService, UserService}; #[cfg(feature = "local-files")] use domain::TranscodeSettingsRepository; use k_core::db::DatabasePool; #[derive(Clone)] pub struct AppState { pub user_service: Arc, pub channel_service: Arc, pub schedule_engine: Arc, pub provider_registry: Arc>>, pub provider_config_repo: Arc, pub cookie_key: Key, #[cfg(feature = "auth-oidc")] pub oidc_service: Option>, #[cfg(feature = "auth-jwt")] pub jwt_validator: Option>, pub config: Arc, pub event_tx: EventBus, /// Broadcast channel for streaming log lines to SSE clients. pub log_tx: broadcast::Sender, /// Ring buffer of recent log lines sent to new SSE clients on connect. pub log_history: Arc>>, /// Repository for persisted in-app activity events. pub activity_log_repo: Arc, /// Indexes for local-files provider instances, keyed by provider instance id. #[cfg(feature = "local-files")] pub local_index: Arc>>>, /// TranscodeManager for FFmpeg HLS transcoding (requires TRANSCODE_DIR). #[cfg(feature = "local-files")] pub transcode_manager: Arc>>>, /// Repository for transcode settings persistence. #[cfg(feature = "local-files")] pub transcode_settings_repo: Option>, /// Database pool — used by infra factory functions for hot-reload. pub db_pool: Arc, pub library_repo: Arc, pub library_sync_adapter: Arc, pub app_settings_repo: Arc, } impl AppState { #[allow(clippy::too_many_arguments)] pub async fn new( user_service: UserService, channel_service: ChannelService, schedule_engine: ScheduleEngineService, provider_registry: Arc>>, provider_config_repo: Arc, config: Config, event_tx: EventBus, log_tx: broadcast::Sender, log_history: Arc>>, activity_log_repo: Arc, db_pool: Arc, library_repo: Arc, library_sync_adapter: Arc, app_settings_repo: Arc, #[cfg(feature = "local-files")] transcode_settings_repo: Option>, ) -> anyhow::Result { let cookie_key = Key::derive_from(config.cookie_secret.as_bytes()); #[cfg(feature = "auth-oidc")] let oidc_service = if let (Some(issuer), Some(id), secret, Some(redirect), resource_id) = ( &config.oidc_issuer, &config.oidc_client_id, &config.oidc_client_secret, &config.oidc_redirect_url, &config.oidc_resource_id, ) { tracing::info!("Initializing OIDC service with issuer: {}", issuer); let issuer_url = domain::IssuerUrl::new(issuer) .map_err(|e| anyhow::anyhow!("Invalid OIDC issuer URL: {}", e))?; let client_id = domain::ClientId::new(id) .map_err(|e| anyhow::anyhow!("Invalid OIDC client ID: {}", e))?; let client_secret = secret.as_ref().map(|s| domain::ClientSecret::new(s)); let redirect_url = domain::RedirectUrl::new(redirect) .map_err(|e| anyhow::anyhow!("Invalid OIDC redirect URL: {}", e))?; let resource = resource_id .as_ref() .map(|r| domain::ResourceId::new(r)) .transpose() .map_err(|e| anyhow::anyhow!("Invalid OIDC resource ID: {}", e))?; Some(Arc::new( OidcService::new(issuer_url, client_id, client_secret, redirect_url, resource) .await?, )) } else { None }; #[cfg(feature = "auth-jwt")] let jwt_validator = { let secret = match &config.jwt_secret { Some(s) if !s.is_empty() => s.clone(), _ => { if config.is_production { anyhow::bail!("JWT_SECRET is required in production"); } tracing::warn!( "⚠️ JWT_SECRET not set — using insecure development secret. DO NOT USE IN PRODUCTION!" ); "k-template-dev-secret-not-for-production-use-only".to_string() } }; tracing::info!("Initializing JWT validator"); let jwt_config = JwtConfig::new( secret, config.jwt_issuer.clone(), config.jwt_audience.clone(), Some(config.jwt_expiry_hours), Some(config.jwt_refresh_expiry_days), config.is_production, )?; Some(Arc::new(JwtValidator::new(jwt_config))) }; Ok(Self { user_service: Arc::new(user_service), channel_service: Arc::new(channel_service), schedule_engine: Arc::new(schedule_engine), provider_registry, provider_config_repo, cookie_key, #[cfg(feature = "auth-oidc")] oidc_service, #[cfg(feature = "auth-jwt")] jwt_validator, config: Arc::new(config), event_tx, log_tx, log_history, activity_log_repo, #[cfg(feature = "local-files")] local_index: Arc::new(tokio::sync::RwLock::new(HashMap::new())), #[cfg(feature = "local-files")] transcode_manager: Arc::new(tokio::sync::RwLock::new(None)), #[cfg(feature = "local-files")] transcode_settings_repo, db_pool, library_repo, library_sync_adapter, app_settings_repo, }) } } impl FromRef for Arc { fn from_ref(input: &AppState) -> Self { input.user_service.clone() } } impl FromRef for Arc { fn from_ref(input: &AppState) -> Self { input.config.clone() } } impl FromRef for Key { fn from_ref(input: &AppState) -> Self { input.cookie_key.clone() } }