188 lines
7.2 KiB
Rust
188 lines
7.2 KiB
Rust
//! 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<UserService>,
|
|
pub channel_service: Arc<ChannelService>,
|
|
pub schedule_engine: Arc<ScheduleEngineService>,
|
|
pub provider_registry: Arc<tokio::sync::RwLock<Arc<infra::ProviderRegistry>>>,
|
|
pub provider_config_repo: Arc<dyn ProviderConfigRepository>,
|
|
pub cookie_key: Key,
|
|
#[cfg(feature = "auth-oidc")]
|
|
pub oidc_service: Option<Arc<OidcService>>,
|
|
#[cfg(feature = "auth-jwt")]
|
|
pub jwt_validator: Option<Arc<JwtValidator>>,
|
|
pub config: Arc<Config>,
|
|
pub event_tx: EventBus,
|
|
/// Broadcast channel for streaming log lines to SSE clients.
|
|
pub log_tx: broadcast::Sender<LogLine>,
|
|
/// Ring buffer of recent log lines sent to new SSE clients on connect.
|
|
pub log_history: Arc<Mutex<VecDeque<LogLine>>>,
|
|
/// Repository for persisted in-app activity events.
|
|
pub activity_log_repo: Arc<dyn ActivityLogRepository>,
|
|
/// Indexes for local-files provider instances, keyed by provider instance id.
|
|
#[cfg(feature = "local-files")]
|
|
pub local_index: Arc<tokio::sync::RwLock<HashMap<String, Arc<infra::LocalIndex>>>>,
|
|
/// TranscodeManager for FFmpeg HLS transcoding (requires TRANSCODE_DIR).
|
|
#[cfg(feature = "local-files")]
|
|
pub transcode_manager: Arc<tokio::sync::RwLock<Option<Arc<infra::TranscodeManager>>>>,
|
|
/// Repository for transcode settings persistence.
|
|
#[cfg(feature = "local-files")]
|
|
pub transcode_settings_repo: Option<Arc<dyn TranscodeSettingsRepository>>,
|
|
/// Database pool — used by infra factory functions for hot-reload.
|
|
pub db_pool: Arc<DatabasePool>,
|
|
pub library_repo: Arc<dyn ILibraryRepository>,
|
|
pub library_sync_adapter: Arc<dyn LibrarySyncAdapter>,
|
|
pub app_settings_repo: Arc<dyn IAppSettingsRepository>,
|
|
}
|
|
|
|
impl AppState {
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn new(
|
|
user_service: UserService,
|
|
channel_service: ChannelService,
|
|
schedule_engine: ScheduleEngineService,
|
|
provider_registry: Arc<tokio::sync::RwLock<Arc<infra::ProviderRegistry>>>,
|
|
provider_config_repo: Arc<dyn ProviderConfigRepository>,
|
|
config: Config,
|
|
event_tx: EventBus,
|
|
log_tx: broadcast::Sender<LogLine>,
|
|
log_history: Arc<Mutex<VecDeque<LogLine>>>,
|
|
activity_log_repo: Arc<dyn ActivityLogRepository>,
|
|
db_pool: Arc<DatabasePool>,
|
|
library_repo: Arc<dyn ILibraryRepository>,
|
|
library_sync_adapter: Arc<dyn LibrarySyncAdapter>,
|
|
app_settings_repo: Arc<dyn IAppSettingsRepository>,
|
|
#[cfg(feature = "local-files")]
|
|
transcode_settings_repo: Option<Arc<dyn TranscodeSettingsRepository>>,
|
|
) -> anyhow::Result<Self> {
|
|
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<AppState> for Arc<UserService> {
|
|
fn from_ref(input: &AppState) -> Self {
|
|
input.user_service.clone()
|
|
}
|
|
}
|
|
|
|
impl FromRef<AppState> for Arc<Config> {
|
|
fn from_ref(input: &AppState) -> Self {
|
|
input.config.clone()
|
|
}
|
|
}
|
|
|
|
impl FromRef<AppState> for Key {
|
|
fn from_ref(input: &AppState) -> Self {
|
|
input.cookie_key.clone()
|
|
}
|
|
}
|