diff --git a/src/config.rs b/src/config.rs index 6e7cd64..155ac90 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,7 @@ mod js_runtime_config; mod scheduler_jobs_config; mod smtp_catch_all_config; mod smtp_config; +mod subscriptions_config; use crate::server::WebhookUrlType; use url::Url; @@ -10,7 +11,7 @@ use url::Url; pub use self::{ components_config::ComponentsConfig, js_runtime_config::JsRuntimeConfig, scheduler_jobs_config::SchedulerJobsConfig, smtp_catch_all_config::SmtpCatchAllConfig, - smtp_config::SmtpConfig, + smtp_config::SmtpConfig, subscriptions_config::SubscriptionsConfig, }; /// Secutils.dev user agent name used for all HTTP requests. @@ -36,6 +37,8 @@ pub struct Config { pub jobs: SchedulerJobsConfig, /// Configuration for the JS runtime. pub js_runtime: JsRuntimeConfig, + /// Configuration related to the Secutils.dev subscriptions. + pub subscriptions: SubscriptionsConfig, } impl AsRef for Config { diff --git a/src/config/subscriptions_config.rs b/src/config/subscriptions_config.rs new file mode 100644 index 0000000..2aca601 --- /dev/null +++ b/src/config/subscriptions_config.rs @@ -0,0 +1,10 @@ +use url::Url; + +/// Configuration related to the Secutils.dev subscriptions. +#[derive(Clone, Debug)] +pub struct SubscriptionsConfig { + /// The URL to access the subscription management page. + pub manage_url: Option, + /// The URL to access the feature overview page. + pub feature_overview_url: Option, +} diff --git a/src/main.rs b/src/main.rs index eda8b3c..4ed2386 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,7 @@ mod utils; use crate::{ config::{ ComponentsConfig, Config, JsRuntimeConfig, SchedulerJobsConfig, SmtpCatchAllConfig, - SmtpConfig, + SmtpConfig, SubscriptionsConfig, }, server::WebhookUrlType, }; @@ -140,6 +140,18 @@ fn process_command(version: &str, matches: ArgMatches) -> Result<(), anyhow::Err anyhow!(" argument is not provided.") })?, }, + subscriptions: SubscriptionsConfig { + manage_url: matches + .get_one::("SUBSCRIPTIONS_MANAGE_URL") + .map(|value| Url::parse(value.as_str())) + .transpose() + .with_context(|| "Cannot parse subscription management URL.")?, + feature_overview_url: matches + .get_one::("SUBSCRIPTIONS_FEATURE_OVERVIEW_URL") + .map(|value| Url::parse(value.as_str())) + .transpose() + .with_context(|| "Cannot parse subscription feature overview URL.")?, + }, }; let session_key = matches @@ -311,6 +323,20 @@ fn main() -> Result<(), anyhow::Error> { .default_value("30") .help("Defines the maximum duration for a single JS script execution in seconds."), ) + .arg( + Arg::new("SUBSCRIPTIONS_MANAGE_URL") + .long("subscriptions-manage-url") + .global(true) + .env("SECUTILS_SUBSCRIPTIONS_MANAGE_URL") + .help("Defines the URL to access the subscription management page."), + ) + .arg( + Arg::new("SUBSCRIPTIONS_FEATURE_OVERVIEW_URL") + .long("subscriptions-feature-overview-url") + .global(true) + .env("SECUTILS_SUBSCRIPTIONS_FEATURE_OVERVIEW_URL") + .help("Defines the URL to access the feature overview page."), + ) .get_matches(); process_command(version, matches) @@ -320,7 +346,7 @@ fn main() -> Result<(), anyhow::Error> { mod tests { use crate::{ api::Api, - config::{ComponentsConfig, Config, SchedulerJobsConfig, SmtpConfig}, + config::{ComponentsConfig, Config, SchedulerJobsConfig, SmtpConfig, SubscriptionsConfig}, database::Database, network::{DnsResolver, Network}, search::SearchItem, @@ -536,6 +562,10 @@ mod tests { max_heap_size_bytes: 10485760, max_user_script_execution_time: Duration::from_secs(30), }, + subscriptions: SubscriptionsConfig { + manage_url: Some(Url::parse("http://localhost:1234/subscription")?), + feature_overview_url: Some(Url::parse("http://localhost:1234/features")?), + }, }) } diff --git a/src/server.rs b/src/server.rs index a3bb97e..abdcf5e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -29,7 +29,7 @@ use std::sync::Arc; pub use self::app_state::tests; pub use app_state::AppState; -pub use ui_state::{Status, StatusLevel, UiState, WebhookUrlType}; +pub use ui_state::{Status, StatusLevel, SubscriptionState, UiState, WebhookUrlType}; #[tokio::main] pub async fn run( diff --git a/src/server/handlers/ui_state_get.rs b/src/server/handlers/ui_state_get.rs index 9dd5d0f..33d8eb1 100644 --- a/src/server/handlers/ui_state_get.rs +++ b/src/server/handlers/ui_state_get.rs @@ -1,6 +1,6 @@ use crate::{ error::Error as SecutilsError, - server::{AppState, UiState}, + server::{AppState, SubscriptionState, UiState}, users::{ClientUserShare, PublicUserDataNamespace, User, UserShare}, }; use actix_web::{web, HttpResponse}; @@ -42,7 +42,11 @@ pub async fn ui_state_get( Ok(HttpResponse::Ok().json(UiState { status: status.deref(), user, - features, + subscription: SubscriptionState { + features, + manage_url: state.config.subscriptions.manage_url.as_ref(), + feature_overview_url: state.config.subscriptions.feature_overview_url.as_ref(), + }, user_share: user_share.map(ClientUserShare::from), settings, utils, diff --git a/src/server/ui_state.rs b/src/server/ui_state.rs index 9ca82e0..830fe59 100644 --- a/src/server/ui_state.rs +++ b/src/server/ui_state.rs @@ -2,9 +2,14 @@ mod status; mod status_level; mod webhook_url_type; -pub use self::{status::Status, status_level::StatusLevel, webhook_url_type::WebhookUrlType}; +mod subscription_state; + +pub use self::{ + status::Status, status_level::StatusLevel, subscription_state::SubscriptionState, + webhook_url_type::WebhookUrlType, +}; use crate::{ - users::{ClientUserShare, SubscriptionFeatures, User, UserSettings}, + users::{ClientUserShare, User, UserSettings}, utils::Util, }; use serde::Serialize; @@ -15,8 +20,8 @@ pub struct UiState<'a> { pub status: &'a Status, #[serde(skip_serializing_if = "Option::is_none")] pub user: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub features: Option, + #[serde(skip_serializing_if = "default")] + pub subscription: SubscriptionState<'a>, #[serde(skip_serializing_if = "Option::is_none")] pub user_share: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -25,31 +30,47 @@ pub struct UiState<'a> { pub webhook_url_type: WebhookUrlType, } +fn default(t: &T) -> bool { + *t == Default::default() +} + #[cfg(test)] mod tests { + use std::collections::BTreeMap; + + use insta::assert_json_snapshot; + use serde_json::json; + use time::OffsetDateTime; + use url::Url; + use uuid::uuid; + use crate::{ - server::{Status, StatusLevel, UiState, WebhookUrlType}, + server::{ + ui_state::subscription_state::SubscriptionState, Status, StatusLevel, UiState, + WebhookUrlType, + }, tests::{mock_config, mock_user}, users::{ClientUserShare, SharedResource, UserId, UserShare, UserShareId}, utils::Util, }; - use insta::assert_json_snapshot; - use serde_json::json; - use std::collections::BTreeMap; - use time::OffsetDateTime; - use uuid::uuid; #[test] fn serialization() -> anyhow::Result<()> { let user = mock_user()?; let features = user.subscription.get_features(&mock_config()?); + let manage_url = Url::parse("http://localhost:1234/subscription")?; + let feature_overview_url = Url::parse("http://localhost:1234/features")?; let ui_state = UiState { status: &Status { version: "1.0.0-alpha.4".to_string(), level: StatusLevel::Available, }, user: Some(user), - features: Some(features), + subscription: SubscriptionState { + features: Some(features), + manage_url: Some(&manage_url), + feature_overview_url: Some(&feature_overview_url), + }, user_share: Some(ClientUserShare::from(UserShare { id: UserShareId::from(uuid!("00000000-0000-0000-0000-000000000001")), user_id: UserId::default(), @@ -92,8 +113,12 @@ mod tests { "startedAt": 1262340001 } }, - "features": { - "admin": true + "subscription": { + "features": { + "admin": true + }, + "manageUrl": "http://localhost:1234/subscription", + "featureOverviewUrl": "http://localhost:1234/features" }, "userShare": { "id": "00000000-0000-0000-0000-000000000001", @@ -127,7 +152,7 @@ mod tests { level: StatusLevel::Available, }, user: None, - features: None, + subscription: Default::default(), user_share: None, settings: None, utils: vec![], diff --git a/src/server/ui_state/subscription_state.rs b/src/server/ui_state/subscription_state.rs new file mode 100644 index 0000000..14f7b0f --- /dev/null +++ b/src/server/ui_state/subscription_state.rs @@ -0,0 +1,54 @@ +use crate::users::SubscriptionFeatures; +use serde_derive::Serialize; +use url::Url; + +/// Defines subscription related properties returned as a part of the UI state. +#[derive(Clone, Serialize, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SubscriptionState<'u> { + /// The subscription-dependent features available to the user. + #[serde(skip_serializing_if = "Option::is_none")] + pub features: Option, + /// The URL to the subscription management page. + #[serde(skip_serializing_if = "Option::is_none")] + pub manage_url: Option<&'u Url>, + /// The URL to the subscription overview page. + #[serde(skip_serializing_if = "Option::is_none")] + pub feature_overview_url: Option<&'u Url>, +} + +#[cfg(test)] +mod tests { + use crate::{ + server::SubscriptionState, + tests::{mock_config, mock_user}, + }; + use insta::assert_json_snapshot; + use url::Url; + + #[test] + fn serialization() -> anyhow::Result<()> { + assert_json_snapshot!(SubscriptionState::default(), @"{}"); + + let user = mock_user()?; + let features = user.subscription.get_features(&mock_config()?); + let manage_url = Url::parse("http://localhost:1234/subscription")?; + let feature_overview_url = Url::parse("http://localhost:1234/features")?; + + assert_json_snapshot!(SubscriptionState { + features: Some(features), + manage_url: Some(&manage_url), + feature_overview_url: Some(&feature_overview_url), + }, @r###" + { + "features": { + "admin": true + }, + "manageUrl": "http://localhost:1234/subscription", + "featureOverviewUrl": "http://localhost:1234/features" + } + "###); + + Ok(()) + } +}