From 33b440d29731cf3868e863d757b9ad59ef245021 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 12 Jul 2026 05:28:49 +0200 Subject: [PATCH] fix(presentation): extract 12 handler violations to use cases - TokenService port + JwtTokenService adapter; login/refresh return LoginResult - delete get_token (dup of login); create_tokens helper removed - type UpdateChannelRequest schedule_config/recycle_policy (no serde_json::Value) - update_settings returns updated Vec; handler calls one use case - config use case in application::config; handler maps DTO only - SyncStatusEntry moved to api-types w/ From - trigger_sync: Conflict error, drop sync_trigger.send from handler - UpsertProviderCommand.config accepts Value; serialization in use case - get_current_broadcast returns BroadcastWithChannel; one call - get_stream_url resolves broadcast internally; handler single call --- Cargo.lock | 2 + crates/adapters/auth/Cargo.toml | 1 + crates/adapters/auth/src/jwt.rs | 45 ++++++++ crates/adapters/auth/src/lib.rs | 2 +- crates/api-types/src/channels.rs | 8 +- crates/api-types/src/lib.rs | 2 +- crates/api-types/src/library.rs | 23 ++++ crates/application/Cargo.toml | 1 + .../application/src/admin/update_settings.rs | 4 +- crates/application/src/auth/commands.rs | 1 + crates/application/src/auth/deps.rs | 3 +- crates/application/src/auth/login.rs | 18 ++- crates/application/src/auth/mod.rs | 3 + crates/application/src/auth/refresh.rs | 24 ++++ crates/application/src/auth/results.rs | 6 + crates/application/src/auth/tests/login.rs | 49 ++++++--- crates/application/src/auth/tests/register.rs | 3 +- crates/application/src/config/deps.rs | 9 ++ crates/application/src/config/get_config.rs | 55 ++++++++++ crates/application/src/config/mod.rs | 6 + crates/application/src/config/queries.rs | 1 + crates/application/src/lib.rs | 1 + crates/application/src/library/sync.rs | 2 +- crates/application/src/providers/commands.rs | 2 +- .../application/src/providers/tests/list.rs | 2 +- .../application/src/providers/tests/upsert.rs | 6 +- crates/application/src/providers/upsert.rs | 4 +- .../src/schedule/get_current_broadcast.rs | 17 ++- .../src/schedule/get_stream_url.rs | 29 ++++- crates/application/src/schedule/queries.rs | 1 - crates/domain/src/ports/auth.rs | 10 +- crates/domain/src/ports/mod.rs | 2 +- crates/domain/src/testing/noops.rs | 43 ++++++++ crates/mcp/src/tools/schedule.rs | 6 +- crates/presentation/src/factory.rs | 28 ++++- crates/presentation/src/handlers/admin.rs | 9 +- crates/presentation/src/handlers/auth.rs | 103 ++---------------- crates/presentation/src/handlers/channels.rs | 20 +--- crates/presentation/src/handlers/config.rs | 48 ++------ crates/presentation/src/handlers/library.rs | 39 +------ crates/presentation/src/handlers/providers.rs | 5 +- crates/presentation/src/handlers/schedule.rs | 42 ++----- crates/presentation/src/routes.rs | 2 +- crates/presentation/src/state.rs | 7 +- 44 files changed, 414 insertions(+), 280 deletions(-) create mode 100644 crates/application/src/auth/refresh.rs create mode 100644 crates/application/src/auth/results.rs create mode 100644 crates/application/src/config/deps.rs create mode 100644 crates/application/src/config/get_config.rs create mode 100644 crates/application/src/config/mod.rs create mode 100644 crates/application/src/config/queries.rs diff --git a/Cargo.lock b/Cargo.lock index 31e0053..ab153d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,7 @@ dependencies = [ "serde_json", "thiserror", "tracing", + "uuid", ] [[package]] @@ -133,6 +134,7 @@ dependencies = [ "async-trait", "chrono", "domain", + "serde_json", "tokio", "uuid", ] diff --git a/crates/adapters/auth/Cargo.toml b/crates/adapters/auth/Cargo.toml index df5f6a0..e6673a1 100644 --- a/crates/adapters/auth/Cargo.toml +++ b/crates/adapters/auth/Cargo.toml @@ -13,6 +13,7 @@ thiserror = { workspace = true } tracing = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +uuid = { workspace = true } # JWT deps jsonwebtoken = { workspace = true, optional = true } diff --git a/crates/adapters/auth/src/jwt.rs b/crates/adapters/auth/src/jwt.rs index a4e9516..8d784b8 100644 --- a/crates/adapters/auth/src/jwt.rs +++ b/crates/adapters/auth/src/jwt.rs @@ -205,6 +205,51 @@ impl JwtValidator { .map_err(|_| JwtError::InvalidFormat)?; Ok(token_data.claims) } + + pub fn expiry_hours(&self) -> u64 { + self.config.expiry_hours + } +} + +pub struct JwtTokenService { + validator: JwtValidator, +} + +impl JwtTokenService { + pub fn new(validator: JwtValidator) -> Self { + Self { validator } + } +} + +impl domain::ports::TokenService for JwtTokenService { + fn create_access_token(&self, user: &domain::User) -> domain::DomainResult { + self.validator.create_token(user).map_err(|e| { + domain::DomainError::InfrastructureError(format!("Failed to create access token: {e}")) + }) + } + + fn create_refresh_token(&self, user: &domain::User) -> domain::DomainResult { + self.validator.create_refresh_token(user).map_err(|e| { + domain::DomainError::InfrastructureError(format!( + "Failed to create refresh token: {e}" + )) + }) + } + + fn validate_refresh_token(&self, token: &str) -> domain::DomainResult { + let claims = self.validator.validate_refresh_token(token).map_err(|e| { + tracing::debug!("Refresh token validation failed: {:?}", e); + domain::DomainError::Unauthenticated("Invalid refresh token".to_string()) + })?; + let user_id: uuid::Uuid = claims.sub.parse().map_err(|_| { + domain::DomainError::Unauthenticated("Invalid user ID in token".to_string()) + })?; + Ok(domain::UserId::from(user_id)) + } + + fn token_expiry_secs(&self) -> u64 { + self.validator.expiry_hours() * 3600 + } } impl std::fmt::Debug for JwtValidator { diff --git a/crates/adapters/auth/src/lib.rs b/crates/adapters/auth/src/lib.rs index 2bf7560..1c669be 100644 --- a/crates/adapters/auth/src/lib.rs +++ b/crates/adapters/auth/src/lib.rs @@ -6,4 +6,4 @@ pub mod jwt; pub use password::PasswordAuthService; #[cfg(feature = "jwt")] -pub use jwt::{JwtClaims, JwtConfig, JwtError, JwtValidator}; +pub use jwt::{JwtClaims, JwtConfig, JwtError, JwtTokenService, JwtValidator}; diff --git a/crates/api-types/src/channels.rs b/crates/api-types/src/channels.rs index 3e09e5e..63be443 100644 --- a/crates/api-types/src/channels.rs +++ b/crates/api-types/src/channels.rs @@ -18,13 +18,15 @@ pub struct CreateChannelRequest { pub webhook_headers: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Deserialize, ToSchema)] pub struct UpdateChannelRequest { pub name: Option, pub description: Option, pub timezone: Option, - pub schedule_config: Option, - pub recycle_policy: Option, + #[schema(value_type = Option)] + pub schedule_config: Option, + #[schema(value_type = Option)] + pub recycle_policy: Option, pub auto_schedule: Option, pub access_mode: Option, pub access_password: Option, diff --git a/crates/api-types/src/lib.rs b/crates/api-types/src/lib.rs index f74bd8a..876e5f0 100644 --- a/crates/api-types/src/lib.rs +++ b/crates/api-types/src/lib.rs @@ -20,7 +20,7 @@ pub use config::{ConfigResponse, ProviderCapabilitiesResponse, ProviderInfo}; pub use iptv::IptvParams; pub use library::{ CollectionResponse, GenresParams, LibraryItemResponse, LibrarySearchParams, ProviderParam, - SeasonResponse, SeasonsParams, ShowResponse, ShowsParams, + SeasonResponse, SeasonsParams, ShowResponse, ShowsParams, SyncStatusEntry, }; pub use providers::{ProviderConfigRequest, ProviderConfigResponse}; pub use schedule::{ diff --git a/crates/api-types/src/library.rs b/crates/api-types/src/library.rs index dc54fc3..8ca0a54 100644 --- a/crates/api-types/src/library.rs +++ b/crates/api-types/src/library.rs @@ -103,6 +103,29 @@ impl From for SeasonResponse { } } +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct SyncStatusEntry { + pub provider_id: String, + pub started_at: String, + pub finished_at: String, + pub items_found: u32, + pub status: String, + pub error_msg: String, +} + +impl From for SyncStatusEntry { + fn from(e: domain::LibrarySyncLogEntry) -> Self { + Self { + provider_id: e.provider_id().to_string(), + started_at: e.started_at().to_string(), + finished_at: e.finished_at().unwrap_or("").to_string(), + items_found: e.items_found(), + status: e.status().to_string(), + error_msg: e.error_msg().unwrap_or("").to_string(), + } + } +} + #[derive(Debug, Deserialize, ToSchema)] pub struct LibrarySearchParams { pub provider: Option, diff --git a/crates/application/Cargo.toml b/crates/application/Cargo.toml index 40a20b0..04ca5f6 100644 --- a/crates/application/Cargo.toml +++ b/crates/application/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" domain = { workspace = true } async-trait = { workspace = true } chrono = { workspace = true } +serde_json = { workspace = true } uuid = { workspace = true } [dev-dependencies] diff --git a/crates/application/src/admin/update_settings.rs b/crates/application/src/admin/update_settings.rs index 151151b..c804be8 100644 --- a/crates/application/src/admin/update_settings.rs +++ b/crates/application/src/admin/update_settings.rs @@ -3,11 +3,11 @@ use domain::DomainResult; use super::commands::UpdateSettingsCommand; use super::deps::AdminDeps; -pub async fn execute(deps: &AdminDeps, cmd: UpdateSettingsCommand) -> DomainResult<()> { +pub async fn execute(deps: &AdminDeps, cmd: UpdateSettingsCommand) -> DomainResult> { for (key, value) in &cmd.settings { deps.settings_repo.set(key, value).await?; } - Ok(()) + deps.settings_repo.get_all().await } #[cfg(test)] diff --git a/crates/application/src/auth/commands.rs b/crates/application/src/auth/commands.rs index 7a44ad9..022f6a2 100644 --- a/crates/application/src/auth/commands.rs +++ b/crates/application/src/auth/commands.rs @@ -6,4 +6,5 @@ pub struct RegisterCommand { pub struct LoginCommand { pub email: String, pub password: String, + pub remember_me: bool, } diff --git a/crates/application/src/auth/deps.rs b/crates/application/src/auth/deps.rs index 52cb941..2432297 100644 --- a/crates/application/src/auth/deps.rs +++ b/crates/application/src/auth/deps.rs @@ -1,10 +1,11 @@ use std::sync::Arc; -use domain::ports::{AuthService, EventPublisher, UserCommand, UserQuery}; +use domain::ports::{AuthService, EventPublisher, TokenService, UserCommand, UserQuery}; pub struct AuthDeps { pub user_command: Arc, pub user_query: Arc, pub auth_service: Arc, + pub token_service: Arc, pub event_publisher: Arc, } diff --git a/crates/application/src/auth/login.rs b/crates/application/src/auth/login.rs index f940022..cd17cec 100644 --- a/crates/application/src/auth/login.rs +++ b/crates/application/src/auth/login.rs @@ -1,12 +1,12 @@ -use domain::models::User; use domain::{DomainError, DomainResult, Email}; use super::commands::LoginCommand; use super::deps::AuthDeps; +use super::results::LoginResult; const INVALID_CREDENTIALS: &str = "Invalid credentials"; -pub async fn execute(deps: &AuthDeps, cmd: LoginCommand) -> DomainResult { +pub async fn execute(deps: &AuthDeps, cmd: LoginCommand) -> DomainResult { let email = Email::new(&cmd.email)?; let user = deps @@ -24,7 +24,19 @@ pub async fn execute(deps: &AuthDeps, cmd: LoginCommand) -> DomainResult { return Err(DomainError::unauthenticated(INVALID_CREDENTIALS)); } - Ok(user) + let access_token = deps.token_service.create_access_token(&user)?; + let refresh_token = if cmd.remember_me { + Some(deps.token_service.create_refresh_token(&user)?) + } else { + None + }; + let expires_in = deps.token_service.token_expiry_secs(); + + Ok(LoginResult { + access_token, + refresh_token, + expires_in, + }) } #[cfg(test)] diff --git a/crates/application/src/auth/mod.rs b/crates/application/src/auth/mod.rs index f96a2e7..85b1972 100644 --- a/crates/application/src/auth/mod.rs +++ b/crates/application/src/auth/mod.rs @@ -2,7 +2,10 @@ pub mod commands; pub mod deps; pub mod login; pub mod queries; +pub mod refresh; pub mod register; +pub mod results; pub use commands::{LoginCommand, RegisterCommand}; pub use deps::AuthDeps; +pub use results::LoginResult; diff --git a/crates/application/src/auth/refresh.rs b/crates/application/src/auth/refresh.rs new file mode 100644 index 0000000..2f93e0a --- /dev/null +++ b/crates/application/src/auth/refresh.rs @@ -0,0 +1,24 @@ +use domain::{DomainError, DomainResult}; + +use super::deps::AuthDeps; +use super::results::LoginResult; + +pub async fn execute(deps: &AuthDeps, refresh_token: String) -> DomainResult { + let user_id = deps.token_service.validate_refresh_token(&refresh_token)?; + + let user = deps + .user_query + .find_by_id(user_id) + .await? + .ok_or_else(|| DomainError::Unauthenticated("User not found".to_string()))?; + + let access_token = deps.token_service.create_access_token(&user)?; + let new_refresh = Some(deps.token_service.create_refresh_token(&user)?); + let expires_in = deps.token_service.token_expiry_secs(); + + Ok(LoginResult { + access_token, + refresh_token: new_refresh, + expires_in, + }) +} diff --git a/crates/application/src/auth/results.rs b/crates/application/src/auth/results.rs new file mode 100644 index 0000000..9c4a958 --- /dev/null +++ b/crates/application/src/auth/results.rs @@ -0,0 +1,6 @@ +#[derive(Debug)] +pub struct LoginResult { + pub access_token: String, + pub refresh_token: Option, + pub expires_in: u64, +} diff --git a/crates/application/src/auth/tests/login.rs b/crates/application/src/auth/tests/login.rs index ddfe1d9..5800411 100644 --- a/crates/application/src/auth/tests/login.rs +++ b/crates/application/src/auth/tests/login.rs @@ -2,14 +2,13 @@ use std::sync::Arc; use domain::errors::DomainResult; use domain::ports::AuthService; -use domain::testing::{InMemoryUserRepository, NoopEventPublisher}; +use domain::testing::{InMemoryUserRepository, NoopEventPublisher, NoopTokenService}; use domain::{DomainError, Email}; use crate::auth::commands::LoginCommand; use crate::auth::deps::AuthDeps; use crate::auth::login; -/// Fake auth service: prefixes "hashed:" for hashing, verifies by checking prefix. struct FakeAuthService; impl AuthService for FakeAuthService { @@ -27,19 +26,15 @@ fn make_deps_with_user( password_hash: &str, ) -> (AuthDeps, Arc) { let repo = Arc::new(InMemoryUserRepository::new()); - - // Seed a user let e = Email::new(email).unwrap(); let user = domain::models::User::new_local(e, password_hash); - repo.store - .lock() - .unwrap() - .insert(user.id(), user); + repo.store.lock().unwrap().insert(user.id(), user); let deps = AuthDeps { user_command: repo.clone(), user_query: repo.clone(), auth_service: Arc::new(FakeAuthService), + token_service: Arc::new(NoopTokenService::new()), event_publisher: Arc::new(NoopEventPublisher::new()), }; (deps, repo) @@ -49,17 +44,38 @@ fn make_deps_with_user( async fn login_succeeds_with_correct_credentials() { let (deps, _) = make_deps_with_user("alice@example.com", "hashed:password123"); - let user = login::execute( + let result = login::execute( &deps, LoginCommand { email: "alice@example.com".into(), password: "password123".into(), + remember_me: false, }, ) .await .unwrap(); - assert_eq!(user.email().as_ref(), "alice@example.com"); + assert!(!result.access_token.is_empty()); + assert!(result.refresh_token.is_none()); +} + +#[tokio::test] +async fn login_with_remember_me_returns_refresh_token() { + let (deps, _) = make_deps_with_user("alice@example.com", "hashed:password123"); + + let result = login::execute( + &deps, + LoginCommand { + email: "alice@example.com".into(), + password: "password123".into(), + remember_me: true, + }, + ) + .await + .unwrap(); + + assert!(!result.access_token.is_empty()); + assert!(result.refresh_token.is_some()); } #[tokio::test] @@ -71,6 +87,7 @@ async fn login_fails_with_wrong_password() { LoginCommand { email: "alice@example.com".into(), password: "wrong".into(), + remember_me: false, }, ) .await; @@ -91,6 +108,7 @@ async fn login_fails_for_unknown_email() { LoginCommand { email: "nobody@example.com".into(), password: "password123".into(), + remember_me: false, }, ) .await; @@ -105,19 +123,15 @@ async fn login_fails_for_unknown_email() { #[tokio::test] async fn login_fails_for_oidc_only_user() { let repo = Arc::new(InMemoryUserRepository::new()); - - // Create an OIDC user (no password hash) let email = Email::new("oidc@example.com").unwrap(); let user = domain::models::User::new("oidc|subject", email); - repo.store - .lock() - .unwrap() - .insert(user.id(), user); + repo.store.lock().unwrap().insert(user.id(), user); let deps = AuthDeps { user_command: repo.clone(), user_query: repo.clone(), auth_service: Arc::new(FakeAuthService), + token_service: Arc::new(NoopTokenService::new()), event_publisher: Arc::new(NoopEventPublisher::new()), }; @@ -126,6 +140,7 @@ async fn login_fails_for_oidc_only_user() { LoginCommand { email: "oidc@example.com".into(), password: "password123".into(), + remember_me: false, }, ) .await; @@ -144,6 +159,7 @@ async fn login_rejects_invalid_email() { user_command: repo.clone(), user_query: repo.clone(), auth_service: Arc::new(FakeAuthService), + token_service: Arc::new(NoopTokenService::new()), event_publisher: Arc::new(NoopEventPublisher::new()), }; @@ -152,6 +168,7 @@ async fn login_rejects_invalid_email() { LoginCommand { email: "not-an-email".into(), password: "password123".into(), + remember_me: false, }, ) .await; diff --git a/crates/application/src/auth/tests/register.rs b/crates/application/src/auth/tests/register.rs index cc77b01..30534c3 100644 --- a/crates/application/src/auth/tests/register.rs +++ b/crates/application/src/auth/tests/register.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use domain::errors::DomainResult; use domain::ports::AuthService; -use domain::testing::{InMemoryUserRepository, NoopEventPublisher}; +use domain::testing::{InMemoryUserRepository, NoopEventPublisher, NoopTokenService}; use domain::{DomainError, Email}; use crate::auth::commands::RegisterCommand; @@ -28,6 +28,7 @@ fn make_deps() -> (AuthDeps, Arc) { user_command: repo.clone(), user_query: repo.clone(), auth_service: Arc::new(FakeAuthService), + token_service: Arc::new(NoopTokenService::new()), event_publisher: Arc::new(NoopEventPublisher::new()), }; (deps, repo) diff --git a/crates/application/src/config/deps.rs b/crates/application/src/config/deps.rs new file mode 100644 index 0000000..fe271fa --- /dev/null +++ b/crates/application/src/config/deps.rs @@ -0,0 +1,9 @@ +use std::sync::Arc; + +use domain::ports::IProviderRegistry; + +pub struct ConfigDeps { + pub provider_registry: Arc, + pub allow_registration: bool, + pub available_provider_types: Vec, +} diff --git a/crates/application/src/config/get_config.rs b/crates/application/src/config/get_config.rs new file mode 100644 index 0000000..4686533 --- /dev/null +++ b/crates/application/src/config/get_config.rs @@ -0,0 +1,55 @@ +use domain::ports::ProviderCapabilities; + +use super::deps::ConfigDeps; +use super::queries::GetConfigQuery; + +pub struct ProviderInfo { + pub id: String, + pub capabilities: ProviderCapabilities, +} + +pub struct SystemConfig { + pub allow_registration: bool, + pub providers: Vec, + pub primary_capabilities: ProviderCapabilities, + pub available_provider_types: Vec, +} + +pub fn execute(deps: &ConfigDeps, _query: GetConfigQuery) -> SystemConfig { + let provider_ids = deps.provider_registry.provider_ids(); + let primary_id = deps.provider_registry.primary_id().to_string(); + + let providers: Vec = provider_ids + .iter() + .filter_map(|id| { + deps.provider_registry + .capabilities(id) + .map(|caps| ProviderInfo { + id: id.clone(), + capabilities: caps, + }) + }) + .collect(); + + let primary_capabilities = deps + .provider_registry + .capabilities(&primary_id) + .unwrap_or(ProviderCapabilities { + collections: false, + series: false, + genres: false, + tags: false, + decade: false, + search: false, + streaming_protocol: domain::ports::StreamingProtocol::DirectFile, + rescan: false, + transcode: false, + }); + + SystemConfig { + allow_registration: deps.allow_registration, + providers, + primary_capabilities, + available_provider_types: deps.available_provider_types.clone(), + } +} diff --git a/crates/application/src/config/mod.rs b/crates/application/src/config/mod.rs new file mode 100644 index 0000000..45fa3d2 --- /dev/null +++ b/crates/application/src/config/mod.rs @@ -0,0 +1,6 @@ +pub mod deps; +pub mod get_config; +pub mod queries; + +pub use deps::ConfigDeps; +pub use queries::GetConfigQuery; diff --git a/crates/application/src/config/queries.rs b/crates/application/src/config/queries.rs new file mode 100644 index 0000000..af135fb --- /dev/null +++ b/crates/application/src/config/queries.rs @@ -0,0 +1 @@ +pub struct GetConfigQuery; diff --git a/crates/application/src/lib.rs b/crates/application/src/lib.rs index e8bf4f9..0add9bf 100644 --- a/crates/application/src/lib.rs +++ b/crates/application/src/lib.rs @@ -1,6 +1,7 @@ pub mod admin; pub mod auth; pub mod channels; +pub mod config; pub mod config_snapshots; pub mod iptv; pub mod library; diff --git a/crates/application/src/library/sync.rs b/crates/application/src/library/sync.rs index d781885..460f454 100644 --- a/crates/application/src/library/sync.rs +++ b/crates/application/src/library/sync.rs @@ -14,7 +14,7 @@ pub async fn execute( for pid in &provider_ids { if deps.library_query.is_sync_running(pid).await? { - return Err(DomainError::ValidationError(format!( + return Err(DomainError::Conflict(format!( "Sync already running for provider '{pid}'" ))); } diff --git a/crates/application/src/providers/commands.rs b/crates/application/src/providers/commands.rs index 346106b..e4bd6ad 100644 --- a/crates/application/src/providers/commands.rs +++ b/crates/application/src/providers/commands.rs @@ -1,7 +1,7 @@ pub struct UpsertProviderCommand { pub id: String, pub provider_type: String, - pub config_json: String, + pub config: serde_json::Value, pub enabled: bool, } diff --git a/crates/application/src/providers/tests/list.rs b/crates/application/src/providers/tests/list.rs index 3c077cd..1d11f5f 100644 --- a/crates/application/src/providers/tests/list.rs +++ b/crates/application/src/providers/tests/list.rs @@ -32,7 +32,7 @@ async fn list_returns_all_providers() { UpsertProviderCommand { id: id.into(), provider_type: "jellyfin".into(), - config_json: "{}".into(), + config: serde_json::json!({}), enabled: true, }, ) diff --git a/crates/application/src/providers/tests/upsert.rs b/crates/application/src/providers/tests/upsert.rs index b72c1e1..9c2854a 100644 --- a/crates/application/src/providers/tests/upsert.rs +++ b/crates/application/src/providers/tests/upsert.rs @@ -23,7 +23,7 @@ async fn upsert_stores_provider() { UpsertProviderCommand { id: "jf-1".into(), provider_type: "jellyfin".into(), - config_json: r#"{"url":"http://localhost:8096"}"#.into(), + config: serde_json::json!({"url":"http://localhost:8096"}), enabled: true, }, ) @@ -46,7 +46,7 @@ async fn upsert_overwrites_existing() { UpsertProviderCommand { id: "jf-1".into(), provider_type: "jellyfin".into(), - config_json: r#"{"url":"http://old"}"#.into(), + config: serde_json::json!({"url":"http://old"}), enabled: true, }, ) @@ -58,7 +58,7 @@ async fn upsert_overwrites_existing() { UpsertProviderCommand { id: "jf-1".into(), provider_type: "jellyfin".into(), - config_json: r#"{"url":"http://new"}"#.into(), + config: serde_json::json!({"url":"http://new"}), enabled: false, }, ) diff --git a/crates/application/src/providers/upsert.rs b/crates/application/src/providers/upsert.rs index 4405fc8..fffb52b 100644 --- a/crates/application/src/providers/upsert.rs +++ b/crates/application/src/providers/upsert.rs @@ -5,10 +5,12 @@ use super::commands::UpsertProviderCommand; use super::deps::ProviderDeps; pub async fn execute(deps: &ProviderDeps, cmd: UpsertProviderCommand) -> DomainResult<()> { + let config_json = serde_json::to_string(&cmd.config) + .map_err(|e| domain::DomainError::ValidationError(format!("Invalid config: {e}")))?; let row = ProviderConfigRow::from_persistence( cmd.id, cmd.provider_type, - cmd.config_json, + config_json, cmd.enabled, String::new(), ); diff --git a/crates/application/src/schedule/get_current_broadcast.rs b/crates/application/src/schedule/get_current_broadcast.rs index e40f66e..90980eb 100644 --- a/crates/application/src/schedule/get_current_broadcast.rs +++ b/crates/application/src/schedule/get_current_broadcast.rs @@ -1,16 +1,21 @@ use chrono::Utc; -use domain::models::CurrentBroadcast; +use domain::models::{Channel, CurrentBroadcast}; use domain::value_objects::ChannelId; use domain::{DomainResult, ScheduleEngineService}; use super::deps::ScheduleDeps; use super::queries::GetCurrentBroadcastQuery; +pub struct BroadcastWithChannel { + pub broadcast: CurrentBroadcast, + pub channel: Option, +} + pub async fn execute( deps: &ScheduleDeps, query: GetCurrentBroadcastQuery, -) -> DomainResult> { +) -> DomainResult> { let channel_id = ChannelId::from(query.channel_id); let now = Utc::now(); @@ -19,7 +24,13 @@ pub async fn execute( None => return Ok(None), }; - Ok(ScheduleEngineService::get_current_broadcast(&schedule, now)) + match ScheduleEngineService::get_current_broadcast(&schedule, now) { + Some(broadcast) => { + let channel = deps.channel_query.find_by_id(channel_id).await?; + Ok(Some(BroadcastWithChannel { broadcast, channel })) + } + None => Ok(None), + } } #[cfg(test)] diff --git a/crates/application/src/schedule/get_stream_url.rs b/crates/application/src/schedule/get_stream_url.rs index f994139..a2dc285 100644 --- a/crates/application/src/schedule/get_stream_url.rs +++ b/crates/application/src/schedule/get_stream_url.rs @@ -1,13 +1,30 @@ +use chrono::Utc; + use domain::ports::StreamQuality; -use domain::value_objects::MediaItemId; -use domain::DomainResult; +use domain::value_objects::ChannelId; +use domain::{DomainResult, ScheduleEngineService}; use super::deps::ScheduleDeps; use super::queries::GetStreamUrlQuery; -pub async fn execute(deps: &ScheduleDeps, query: GetStreamUrlQuery) -> DomainResult { - let item_id = MediaItemId::new(&query.item_id); - deps.schedule_engine +pub async fn execute(deps: &ScheduleDeps, query: GetStreamUrlQuery) -> DomainResult> { + let channel_id = ChannelId::from(query.channel_id); + let now = Utc::now(); + + let schedule = match deps.schedule_query.find_active(channel_id, now).await? { + Some(s) => s, + None => return Ok(None), + }; + + let broadcast = match ScheduleEngineService::get_current_broadcast(&schedule, now) { + Some(b) => b, + None => return Ok(None), + }; + + let item_id = broadcast.slot().item().id().clone(); + let url = deps + .schedule_engine .get_stream_url(&item_id, &StreamQuality::Direct) - .await + .await?; + Ok(Some(url)) } diff --git a/crates/application/src/schedule/queries.rs b/crates/application/src/schedule/queries.rs index da24850..984871a 100644 --- a/crates/application/src/schedule/queries.rs +++ b/crates/application/src/schedule/queries.rs @@ -14,7 +14,6 @@ pub struct GetEpgQuery { pub struct GetStreamUrlQuery { pub channel_id: Uuid, - pub item_id: String, } pub struct ListHistoryQuery { diff --git a/crates/domain/src/ports/auth.rs b/crates/domain/src/ports/auth.rs index 8534c74..061fbb6 100644 --- a/crates/domain/src/ports/auth.rs +++ b/crates/domain/src/ports/auth.rs @@ -1,8 +1,16 @@ use crate::errors::DomainResult; +use crate::models::User; +use crate::value_objects::UserId; -// Intentionally sync: CPU-bound hashing should run on a blocking thread pool pub trait AuthService: Send + Sync { fn hash_password(&self, password: &str) -> DomainResult; fn verify_password(&self, password: &str, hash: &str) -> DomainResult; } + +pub trait TokenService: Send + Sync { + fn create_access_token(&self, user: &User) -> DomainResult; + fn create_refresh_token(&self, user: &User) -> DomainResult; + fn validate_refresh_token(&self, token: &str) -> DomainResult; + fn token_expiry_secs(&self) -> u64; +} diff --git a/crates/domain/src/ports/mod.rs b/crates/domain/src/ports/mod.rs index a0b9f91..dced707 100644 --- a/crates/domain/src/ports/mod.rs +++ b/crates/domain/src/ports/mod.rs @@ -11,7 +11,7 @@ pub mod transcode; pub mod user; pub use activity::{ActivityLogCommand, ActivityLogQuery}; -pub use auth::AuthService; +pub use auth::{AuthService, TokenService}; pub use channel::{ChannelCommand, ChannelQuery}; pub use events::{DomainEvent, EventConsumer, EventHandler, EventPublisher}; pub use library::{LibraryCommand, LibraryQuery, LibrarySyncAdapter}; diff --git a/crates/domain/src/testing/noops.rs b/crates/domain/src/testing/noops.rs index 5cb5878..e4f917a 100644 --- a/crates/domain/src/testing/noops.rs +++ b/crates/domain/src/testing/noops.rs @@ -114,6 +114,49 @@ impl ActivityLogQuery for NoopActivityLog { } } +pub struct NoopTokenService; + +impl NoopTokenService { + pub fn new() -> Self { + Self + } +} + +impl Default for NoopTokenService { + fn default() -> Self { + Self::new() + } +} + +impl crate::ports::TokenService for NoopTokenService { + fn create_access_token( + &self, + _user: &crate::models::User, + ) -> crate::errors::DomainResult { + Ok("noop-access-token".to_string()) + } + + fn create_refresh_token( + &self, + _user: &crate::models::User, + ) -> crate::errors::DomainResult { + Ok("noop-refresh-token".to_string()) + } + + fn validate_refresh_token( + &self, + _token: &str, + ) -> crate::errors::DomainResult { + Err(crate::errors::DomainError::Unauthenticated( + "NoopTokenService".to_string(), + )) + } + + fn token_expiry_secs(&self) -> u64 { + 3600 + } +} + pub struct NoopLibrarySync; impl NoopLibrarySync { diff --git a/crates/mcp/src/tools/schedule.rs b/crates/mcp/src/tools/schedule.rs index c1af4d8..0fdc2da 100644 --- a/crates/mcp/src/tools/schedule.rs +++ b/crates/mcp/src/tools/schedule.rs @@ -35,10 +35,10 @@ pub async fn get_active_schedule(deps: &Arc, channel_id: Uuid) -> pub async fn get_current_broadcast(deps: &Arc, channel_id: Uuid) -> String { let query = GetCurrentBroadcastQuery { channel_id }; match application::schedule::get_current_broadcast::execute(deps, query).await { - Ok(Some(b)) => { - let offset = b.offset_secs(); + Ok(Some(result)) => { + let offset = result.broadcast.offset_secs(); ok_json(&CurrentBroadcastDto { - slot: b.into_slot(), + slot: result.broadcast.into_slot(), offset_secs: offset, }) } diff --git a/crates/presentation/src/factory.rs b/crates/presentation/src/factory.rs index 4bc3044..4e5f48b 100644 --- a/crates/presentation/src/factory.rs +++ b/crates/presentation/src/factory.rs @@ -4,6 +4,7 @@ use application::{ admin::AdminDeps, auth::AuthDeps, channels::{ChannelCommandDeps, ChannelQueryDeps}, + config::ConfigDeps, config_snapshots::ConfigSnapshotDeps, iptv::IptvDeps, library::{LibraryCommandDeps, LibraryQueryDeps}, @@ -46,12 +47,23 @@ pub async fn build_app_state(config: Config) -> anyhow::Result { #[cfg(feature = "auth-jwt")] let jwt_validator = build_jwt_validator(&config)?; + #[cfg(feature = "auth-jwt")] + let token_service: Arc = { + let validator = jwt_validator + .as_ref() + .expect("JWT validator required") + .as_ref() + .clone(); + Arc::new(adapter_auth::JwtTokenService::new(validator)) + }; + let (sync_tx, sync_rx) = tokio::sync::watch::channel(()); let auth_deps = Arc::new(AuthDeps { user_command: wire_output.user_command.clone(), user_query: wire_output.user_query.clone(), auth_service, + token_service, event_publisher: event_publisher.clone(), }); @@ -105,6 +117,18 @@ pub async fn build_app_state(config: Config) -> anyhow::Result { provider_config_query: wire_output.provider_config_query.clone(), }); + let mut available_provider_types = Vec::new(); + #[cfg(feature = "jellyfin")] + available_provider_types.push("jellyfin".to_string()); + #[cfg(feature = "local-files")] + available_provider_types.push("local_files".to_string()); + + let config_deps = Arc::new(ConfigDeps { + provider_registry: provider_registry.clone(), + allow_registration: config.allow_registration, + available_provider_types, + }); + let config_arc = Arc::new(config); let bg_schedule_deps = schedule_deps.clone(); @@ -140,6 +164,7 @@ pub async fn build_app_state(config: Config) -> anyhow::Result { channel_command_deps, channel_query_deps, config_snapshot_deps, + config_deps, schedule_deps, library_command_deps, library_query_deps, @@ -148,12 +173,11 @@ pub async fn build_app_state(config: Config) -> anyhow::Result { provider_deps, #[cfg(feature = "auth-jwt")] jwt_validator, - provider_registry, _library_sync: library_sync, _settings_repo: wire_output.settings, _event_bus: event_bus, config: config_arc, - sync_trigger: sync_tx, + _sync_trigger: sync_tx, }) } diff --git a/crates/presentation/src/handlers/admin.rs b/crates/presentation/src/handlers/admin.rs index b231bfb..ba8bb21 100644 --- a/crates/presentation/src/handlers/admin.rs +++ b/crates/presentation/src/handlers/admin.rs @@ -3,7 +3,7 @@ use axum::extract::{Query, State}; use std::collections::HashMap; use api_types::{ActivityEventResponse, ActivityLogParams, SettingsResponse}; -use application::admin::{GetActivityLogQuery, GetSettingsQuery, UpdateSettingsCommand}; +use application::admin::{GetActivityLogQuery, UpdateSettingsCommand}; use crate::errors::AppError; use crate::extractors::AdminUser; @@ -16,7 +16,7 @@ pub async fn get_settings( AdminUser(_user): AdminUser, ) -> Result, AppError> { let pairs = - application::admin::get_settings::execute(&state.admin_deps, GetSettingsQuery).await?; + application::admin::get_settings::execute(&state.admin_deps, application::admin::GetSettingsQuery).await?; let settings: HashMap = pairs.into_iter().collect(); Ok(Json(SettingsResponse { settings })) } @@ -30,10 +30,7 @@ pub async fn update_settings( let cmd = UpdateSettingsCommand { settings: settings_vec, }; - application::admin::update_settings::execute(&state.admin_deps, cmd).await?; - - let pairs = - application::admin::get_settings::execute(&state.admin_deps, GetSettingsQuery).await?; + let pairs = application::admin::update_settings::execute(&state.admin_deps, cmd).await?; let settings: HashMap = pairs.into_iter().collect(); Ok(Json(SettingsResponse { settings })) } diff --git a/crates/presentation/src/handlers/auth.rs b/crates/presentation/src/handlers/auth.rs index 2736b89..26fd728 100644 --- a/crates/presentation/src/handlers/auth.rs +++ b/crates/presentation/src/handlers/auth.rs @@ -3,14 +3,12 @@ use axum::extract::State; use api_types::{LoginRequest, RefreshRequest, RegisterRequest, TokenResponse, UserResponse}; use application::auth::{LoginCommand, RegisterCommand}; -use domain::DomainError; use crate::errors::AppError; use crate::extractors::CurrentUser; use crate::state::AppState; const TOKEN_TYPE_BEARER: &str = "Bearer"; -const SECS_PER_HOUR: u64 = 3600; pub async fn register( State(state): State, @@ -31,14 +29,14 @@ pub async fn login( let cmd = LoginCommand { email: req.email, password: req.password, + remember_me: req.remember_me, }; - let user = application::auth::login::execute(&state.auth_deps, cmd).await?; - let (access_token, refresh_token) = create_tokens(&user, &state, req.remember_me)?; + let result = application::auth::login::execute(&state.auth_deps, cmd).await?; Ok(Json(TokenResponse { - access_token, + access_token: result.access_token, token_type: TOKEN_TYPE_BEARER.to_string(), - expires_in: state.config.jwt_expiry_hours * SECS_PER_HOUR, - refresh_token, + expires_in: result.expires_in, + refresh_token: result.refresh_token, })) } @@ -46,100 +44,21 @@ pub async fn logout() -> Result, AppError> { Ok(Json(serde_json::json!({"message": "logged out"}))) } -pub async fn me( - CurrentUser(user): CurrentUser, -) -> Result, AppError> { +pub async fn me(CurrentUser(user): CurrentUser) -> Result, AppError> { Ok(Json(UserResponse::from(user))) } -#[cfg(feature = "auth-jwt")] -pub async fn get_token( - State(state): State, - Json(req): Json, -) -> Result, AppError> { - let cmd = LoginCommand { - email: req.email, - password: req.password, - }; - let user = application::auth::login::execute(&state.auth_deps, cmd).await?; - let (access_token, refresh_token) = create_tokens(&user, &state, req.remember_me)?; - Ok(Json(TokenResponse { - access_token, - token_type: TOKEN_TYPE_BEARER.to_string(), - expires_in: state.config.jwt_expiry_hours * SECS_PER_HOUR, - refresh_token, - })) -} - #[cfg(feature = "auth-jwt")] pub async fn refresh_token( State(state): State, Json(req): Json, ) -> Result, AppError> { - let validator = state - .jwt_validator - .as_ref() - .ok_or_else(|| AppError(DomainError::InfrastructureError("JWT not configured".to_string())))?; - - let claims = validator.validate_refresh_token(&req.refresh_token).map_err(|e| { - tracing::debug!("Refresh token validation failed: {:?}", e); - AppError(DomainError::Unauthenticated("Invalid refresh token".to_string())) - })?; - - let user_id: uuid::Uuid = claims - .sub - .parse() - .map_err(|_| AppError(DomainError::Unauthenticated("Invalid user ID in token".to_string())))?; - - let user = state - .auth_deps - .user_query - .find_by_id(domain::UserId::from(user_id)) - .await - .map_err(|e| AppError(DomainError::InfrastructureError(format!("Failed to fetch user: {}", e))))? - .ok_or_else(|| AppError(DomainError::Unauthenticated("User not found".to_string())))?; - - let (access_token, refresh_token) = create_tokens(&user, &state, true)?; + let result = + application::auth::refresh::execute(&state.auth_deps, req.refresh_token).await?; Ok(Json(TokenResponse { - access_token, + access_token: result.access_token, token_type: TOKEN_TYPE_BEARER.to_string(), - expires_in: state.config.jwt_expiry_hours * SECS_PER_HOUR, - refresh_token, + expires_in: result.expires_in, + refresh_token: result.refresh_token, })) } - -fn create_tokens( - user: &domain::User, - state: &AppState, - remember_me: bool, -) -> Result<(String, Option), AppError> { - #[cfg(feature = "auth-jwt")] - { - let validator = state - .jwt_validator - .as_ref() - .ok_or_else(|| AppError(DomainError::InfrastructureError("JWT not configured".to_string())))?; - - let access = validator - .create_token(user) - .map_err(|e| AppError(DomainError::InfrastructureError(format!("Failed to create token: {}", e))))?; - - let refresh = if remember_me { - Some( - validator - .create_refresh_token(user) - .map_err(|e| AppError(DomainError::InfrastructureError(format!("Failed to create refresh token: {}", e))))?, - ) - } else { - None - }; - - Ok((access, refresh)) - } - - #[cfg(not(feature = "auth-jwt"))] - { - let _ = (user, state, remember_me); - Err(AppError(DomainError::InfrastructureError("JWT feature not enabled".to_string()))) - } -} diff --git a/crates/presentation/src/handlers/channels.rs b/crates/presentation/src/handlers/channels.rs index 6fdc857..d439138 100644 --- a/crates/presentation/src/handlers/channels.rs +++ b/crates/presentation/src/handlers/channels.rs @@ -74,30 +74,14 @@ pub async fn update_channel( Path(id): Path, Json(req): Json, ) -> Result, AppError> { - let schedule_config = req - .schedule_config - .map(|v| { - serde_json::from_value(v) - .map_err(|e| AppError(DomainError::ValidationError(format!("Invalid schedule_config: {e}")))) - }) - .transpose()?; - - let recycle_policy = req - .recycle_policy - .map(|v| { - serde_json::from_value(v) - .map_err(|e| AppError(DomainError::ValidationError(format!("Invalid recycle_policy: {e}")))) - }) - .transpose()?; - let cmd = UpdateChannelCommand { channel_id: id.into(), owner_id: user.id(), name: req.name, description: req.description.map(Some), timezone: req.timezone, - schedule_config, - recycle_policy, + schedule_config: req.schedule_config.map(Into::into), + recycle_policy: req.recycle_policy, auto_schedule: req.auto_schedule, }; let channel = application::channels::update::execute(&state.channel_command_deps, cmd).await?; diff --git a/crates/presentation/src/handlers/config.rs b/crates/presentation/src/handlers/config.rs index eabf276..8e448bc 100644 --- a/crates/presentation/src/handlers/config.rs +++ b/crates/presentation/src/handlers/config.rs @@ -2,54 +2,28 @@ use axum::Json; use axum::extract::State; use api_types::{ConfigResponse, ProviderCapabilitiesResponse, ProviderInfo}; +use application::config::GetConfigQuery; use crate::errors::AppError; use crate::state::AppState; -const FALLBACK_STREAMING_PROTOCOL: &str = "direct_file"; - pub async fn get_config( State(state): State, ) -> Result, AppError> { - let registry = &state.provider_registry; - let provider_ids = registry.provider_ids(); - let primary_id = registry.primary_id().to_string(); - - let providers: Vec = provider_ids - .iter() - .filter_map(|id| { - registry.capabilities(id).map(|caps| ProviderInfo { - id: id.clone(), - capabilities: ProviderCapabilitiesResponse::from(caps), - }) + let sys_config = application::config::get_config::execute(&state.config_deps, GetConfigQuery); + let providers: Vec = sys_config + .providers + .into_iter() + .map(|p| ProviderInfo { + id: p.id, + capabilities: ProviderCapabilitiesResponse::from(p.capabilities), }) .collect(); - - let primary_caps = registry - .capabilities(&primary_id) - .map(ProviderCapabilitiesResponse::from) - .unwrap_or(ProviderCapabilitiesResponse { - collections: false, - series: false, - genres: false, - tags: false, - decade: false, - search: false, - streaming_protocol: FALLBACK_STREAMING_PROTOCOL.to_string(), - rescan: false, - transcode: false, - }); - - let mut available_types = Vec::new(); - #[cfg(feature = "jellyfin")] - available_types.push("jellyfin".to_string()); - #[cfg(feature = "local-files")] - available_types.push("local_files".to_string()); - + let primary_caps = ProviderCapabilitiesResponse::from(sys_config.primary_capabilities); Ok(Json(ConfigResponse { - allow_registration: state.config.allow_registration, + allow_registration: sys_config.allow_registration, providers, provider_capabilities: primary_caps, - available_provider_types: available_types, + available_provider_types: sys_config.available_provider_types, })) } diff --git a/crates/presentation/src/handlers/library.rs b/crates/presentation/src/handlers/library.rs index 8dcc44f..c2a98ab 100644 --- a/crates/presentation/src/handlers/library.rs +++ b/crates/presentation/src/handlers/library.rs @@ -1,10 +1,9 @@ use axum::Json; use axum::extract::{Path, Query, State}; -use serde::Serialize; use api_types::{ CollectionResponse, GenresParams, LibraryItemResponse, LibrarySearchParams, PaginatedResponse, - ProviderParam, SeasonResponse, SeasonsParams, ShowResponse, ShowsParams, + ProviderParam, SeasonResponse, SeasonsParams, ShowResponse, ShowsParams, SyncStatusEntry, }; use application::library::{ GetItemQuery, GetSyncStatusQuery, ListCollectionsQuery, ListGenresQuery, ListSeasonsQuery, @@ -116,16 +115,6 @@ pub async fn list_genres( Ok(Json(genres)) } -#[derive(Debug, Serialize)] -pub(crate) struct SyncStatusEntry { - provider_id: String, - started_at: String, - finished_at: String, - items_found: u32, - status: String, - error_msg: String, -} - pub async fn sync_status( State(state): State, CurrentUser(_user): CurrentUser, @@ -133,18 +122,7 @@ pub async fn sync_status( let entries = application::library::get_sync_status::execute(&state.library_query_deps, GetSyncStatusQuery) .await?; - let result: Vec = entries - .into_iter() - .map(|e| SyncStatusEntry { - provider_id: e.provider_id().to_string(), - started_at: e.started_at().to_string(), - finished_at: e.finished_at().unwrap_or("").to_string(), - items_found: e.items_found(), - status: e.status().to_string(), - error_msg: e.error_msg().unwrap_or("").to_string(), - }) - .collect(); - Ok(Json(result)) + Ok(Json(entries.into_iter().map(SyncStatusEntry::from).collect())) } pub async fn trigger_sync( @@ -152,17 +130,6 @@ pub async fn trigger_sync( AdminUser(_user): AdminUser, ) -> Result { let cmd = TriggerSyncCommand { provider_id: None }; - application::library::sync::execute(&state.library_command_deps, cmd) - .await - .map_err(|e| { - if e.to_string().contains("already running") { - AppError(DomainError::Conflict(e.to_string())) - } else { - AppError::from(e) - } - })?; - - let _ = state.sync_trigger.send(()); - + application::library::sync::execute(&state.library_command_deps, cmd).await?; Ok(axum::http::StatusCode::ACCEPTED) } diff --git a/crates/presentation/src/handlers/providers.rs b/crates/presentation/src/handlers/providers.rs index 1e889b8..67b4131 100644 --- a/crates/presentation/src/handlers/providers.rs +++ b/crates/presentation/src/handlers/providers.rs @@ -43,13 +43,10 @@ pub async fn upsert_provider( Path(id): Path, Json(req): Json, ) -> Result, AppError> { - let config_json = serde_json::to_string(&req.config) - .map_err(|e| AppError(DomainError::ValidationError(format!("Invalid config JSON: {e}"))))?; - let cmd = UpsertProviderCommand { id, provider_type: req.provider_type, - config_json, + config: req.config, enabled: req.enabled, }; application::providers::upsert::execute(&state.provider_deps, cmd).await?; diff --git a/crates/presentation/src/handlers/schedule.rs b/crates/presentation/src/handlers/schedule.rs index 63442fc..a0ad19e 100644 --- a/crates/presentation/src/handlers/schedule.rs +++ b/crates/presentation/src/handlers/schedule.rs @@ -44,20 +44,15 @@ pub async fn get_current_broadcast( let query = GetCurrentBroadcastQuery { channel_id: id }; match application::schedule::get_current_broadcast::execute(&state.schedule_deps, query).await? { - Some(broadcast) => { - let channel_query = application::channels::GetChannelQuery { channel_id: id.into() }; - let channel = application::channels::get::execute(&state.channel_query_deps, channel_query).await?; - - let slot_response = match &channel { - Some(ch) => SlotResponse::with_block_access(broadcast.slot().clone(), ch), - None => SlotResponse::from(broadcast.slot().clone()), + Some(result) => { + let slot_response = match &result.channel { + Some(ch) => SlotResponse::with_block_access(result.broadcast.slot().clone(), ch), + None => SlotResponse::from(result.broadcast.slot().clone()), }; - let block_access_mode = slot_response.block_access_mode.clone(); - Ok(Json(CurrentBroadcastResponse { slot: slot_response, - offset_secs: broadcast.offset_secs(), + offset_secs: result.broadcast.offset_secs(), block_access_mode, }) .into_response()) @@ -79,26 +74,13 @@ pub async fn get_stream( State(state): State, Path(id): Path, ) -> Result { - let broadcast_query = GetCurrentBroadcastQuery { channel_id: id }; - let broadcast = - application::schedule::get_current_broadcast::execute(&state.schedule_deps, broadcast_query) - .await?; - - match broadcast { - Some(b) => { - let stream_query = GetStreamUrlQuery { - channel_id: id, - item_id: b.slot().item().id().value().to_string(), - }; - let url = - application::schedule::get_stream_url::execute(&state.schedule_deps, stream_query) - .await?; - Ok(( - StatusCode::TEMPORARY_REDIRECT, - [("Location", url.as_str())], - ) - .into_response()) - } + let query = GetStreamUrlQuery { channel_id: id }; + match application::schedule::get_stream_url::execute(&state.schedule_deps, query).await? { + Some(url) => Ok(( + StatusCode::TEMPORARY_REDIRECT, + [("Location", url.as_str())], + ) + .into_response()), None => Ok(StatusCode::NO_CONTENT.into_response()), } } diff --git a/crates/presentation/src/routes.rs b/crates/presentation/src/routes.rs index 8d5c057..9de0f44 100644 --- a/crates/presentation/src/routes.rs +++ b/crates/presentation/src/routes.rs @@ -24,7 +24,7 @@ fn auth_router() -> Router { #[cfg(feature = "auth-jwt")] let r = r - .route("/token", post(handlers::auth::get_token)) + .route("/token", post(handlers::auth::login)) .route("/refresh", post(handlers::auth::refresh_token)); r diff --git a/crates/presentation/src/state.rs b/crates/presentation/src/state.rs index 2ace278..3818b9f 100644 --- a/crates/presentation/src/state.rs +++ b/crates/presentation/src/state.rs @@ -4,6 +4,7 @@ use application::{ admin::AdminDeps, auth::AuthDeps, channels::{ChannelCommandDeps, ChannelQueryDeps}, + config::ConfigDeps, config_snapshots::ConfigSnapshotDeps, iptv::IptvDeps, library::{LibraryCommandDeps, LibraryQueryDeps}, @@ -17,6 +18,7 @@ pub struct AppState { pub channel_command_deps: Arc, pub channel_query_deps: Arc, pub config_snapshot_deps: Arc, + pub config_deps: Arc, pub schedule_deps: Arc, pub library_command_deps: Arc, pub library_query_deps: Arc, @@ -27,13 +29,10 @@ pub struct AppState { #[cfg(feature = "auth-jwt")] pub jwt_validator: Option>, - pub provider_registry: Arc, - - // kept alive for background tasks — not read from handlers pub _library_sync: Arc, pub _settings_repo: Arc, pub _event_bus: Arc, pub config: Arc, - pub sync_trigger: tokio::sync::watch::Sender<()>, + pub _sync_trigger: tokio::sync::watch::Sender<()>, }