diff --git a/apps/labrinth/.env.docker-compose b/apps/labrinth/.env.docker-compose index 49ddff31c9..5198de5ad7 100644 --- a/apps/labrinth/.env.docker-compose +++ b/apps/labrinth/.env.docker-compose @@ -8,6 +8,8 @@ SITE_URL=http://localhost:3000 # This CDN URL matches the local storage backend set below, which uses MOCK_FILE_PATH CDN_URL=file:///tmp/modrinth LABRINTH_ADMIN_KEY=feedbeef +LABRINTH_MEDAL_KEY= +LABRINTH_EXTERNAL_NOTIFICATION_KEY=beeffeed RATE_LIMIT_IGNORE_KEY=feedbeef DATABASE_URL=postgresql://labrinth:labrinth@labrinth-postgres/labrinth @@ -152,6 +154,6 @@ ARCHON_URL=none MURALPAY_API_URL=https://api.muralpay.com MURALPAY_API_KEY=none MURALPAY_TRANSFER_API_KEY=none -MURALPAY_SOURCE_ACCOUNT_ID=none +MURALPAY_SOURCE_ACCOUNT_ID=00000000-0000-0000-0000-000000000000 DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1 diff --git a/apps/labrinth/.env.local b/apps/labrinth/.env.local index 98b92c184f..0040a36ba5 100644 --- a/apps/labrinth/.env.local +++ b/apps/labrinth/.env.local @@ -8,6 +8,7 @@ SITE_URL=http://localhost:3000 # This CDN URL matches the local storage backend set below, which uses MOCK_FILE_PATH CDN_URL=file:///tmp/modrinth LABRINTH_ADMIN_KEY=feedbeef +LABRINTH_MEDAL_KEY= LABRINTH_EXTERNAL_NOTIFICATION_KEY=beeffeed RATE_LIMIT_IGNORE_KEY=feedbeef @@ -163,6 +164,6 @@ ARCHON_URL=none MURALPAY_API_URL=https://api-staging.muralpay.com MURALPAY_API_KEY=none MURALPAY_TRANSFER_API_KEY=none -MURALPAY_SOURCE_ACCOUNT_ID=none +MURALPAY_SOURCE_ACCOUNT_ID=00000000-0000-0000-0000-000000000000 DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1 diff --git a/apps/labrinth/src/auth/validate.rs b/apps/labrinth/src/auth/validate.rs index fe3644ebd2..c695bc45dc 100644 --- a/apps/labrinth/src/auth/validate.rs +++ b/apps/labrinth/src/auth/validate.rs @@ -2,6 +2,7 @@ use super::AuthProvider; use crate::auth::AuthenticationError; use crate::database::models::{DBUser, user_item}; use crate::database::redis::RedisPool; +use crate::env::ENV; use crate::models::pats::Scopes; use crate::models::users::User; use crate::queue::session::AuthQueue; @@ -146,7 +147,7 @@ where user_item::DBUser::get_id(session.user_id, executor, redis) .await?; - let rate_limit_ignore = dotenvy::var("RATE_LIMIT_IGNORE_KEY")?; + let rate_limit_ignore = &ENV.RATE_LIMIT_IGNORE_KEY; if req .headers() .get("x-ratelimit-key") diff --git a/apps/labrinth/src/clickhouse/mod.rs b/apps/labrinth/src/clickhouse/mod.rs index 2c3fc6da7f..95d8e7b276 100644 --- a/apps/labrinth/src/clickhouse/mod.rs +++ b/apps/labrinth/src/clickhouse/mod.rs @@ -5,9 +5,10 @@ mod fetch; pub use fetch::*; +use crate::env::ENV; + pub async fn init_client() -> clickhouse::error::Result { - init_client_with_database(&dotenvy::var("CLICKHOUSE_DATABASE").unwrap()) - .await + init_client_with_database(&ENV.CLICKHOUSE_DATABASE).await } pub async fn init_client_with_database( @@ -24,9 +25,9 @@ pub async fn init_client_with_database( .build(https_connector); clickhouse::Client::with_http_client(hyper_client) - .with_url(dotenvy::var("CLICKHOUSE_URL").unwrap()) - .with_user(dotenvy::var("CLICKHOUSE_USER").unwrap()) - .with_password(dotenvy::var("CLICKHOUSE_PASSWORD").unwrap()) + .with_url(&ENV.CLICKHOUSE_URL) + .with_user(&ENV.CLICKHOUSE_USER) + .with_password(&ENV.CLICKHOUSE_PASSWORD) .with_validation(false) }; @@ -35,8 +36,7 @@ pub async fn init_client_with_database( .execute() .await?; - let clickhouse_replicated = - dotenvy::var("CLICKHOUSE_REPLICATED").unwrap() == "true"; + let clickhouse_replicated = ENV.CLICKHOUSE_REPLICATED; let cluster_line = if clickhouse_replicated { "ON cluster '{cluster}'" } else { diff --git a/apps/labrinth/src/database/postgres_database.rs b/apps/labrinth/src/database/postgres_database.rs index a0f4d45739..726621848d 100644 --- a/apps/labrinth/src/database/postgres_database.rs +++ b/apps/labrinth/src/database/postgres_database.rs @@ -13,6 +13,8 @@ pub type PgTransaction<'c> = sqlx_tracing::Transaction<'c, Postgres>; pub use sqlx_tracing::Acquire; pub use sqlx_tracing::Executor; +use crate::env::ENV; + // pub type PgPool = sqlx::PgPool; // pub type PgTransaction<'c> = sqlx::Transaction<'c, Postgres>; // pub use sqlx::Acquire; @@ -50,57 +52,27 @@ impl DerefMut for ReadOnlyPgPool { pub async fn connect_all() -> Result<(PgPool, ReadOnlyPgPool), sqlx::Error> { info!("Initializing database connection"); - let database_url = - dotenvy::var("DATABASE_URL").expect("`DATABASE_URL` not in .env"); + let database_url = &ENV.DATABASE_URL; let acquire_timeout = - dotenvy::var("DATABASE_ACQUIRE_TIMEOUT_MS") - .ok() - .map_or_else( - || Duration::from_millis(30000), - |x| { - Duration::from_millis(x.parse::().expect( - "DATABASE_ACQUIRE_TIMEOUT_MS must be a valid u64", - )) - }, - ); + Duration::from_millis(ENV.DATABASE_ACQUIRE_TIMEOUT_MS); let pool = PgPoolOptions::new() .acquire_timeout(acquire_timeout) - .min_connections( - dotenvy::var("DATABASE_MIN_CONNECTIONS") - .ok() - .and_then(|x| x.parse().ok()) - .unwrap_or(0), - ) - .max_connections( - dotenvy::var("DATABASE_MAX_CONNECTIONS") - .ok() - .and_then(|x| x.parse().ok()) - .unwrap_or(16), - ) + .min_connections(ENV.DATABASE_MIN_CONNECTIONS) + .max_connections(ENV.DATABASE_MAX_CONNECTIONS) .max_lifetime(Some(Duration::from_secs(60 * 60))) - .connect(&database_url) + .connect(database_url) .await?; let pool = PgPool::from(pool); - if let Ok(url) = dotenvy::var("READONLY_DATABASE_URL") { + if !ENV.READONLY_DATABASE_URL.is_empty() { let ro_pool = PgPoolOptions::new() .acquire_timeout(acquire_timeout) - .min_connections( - dotenvy::var("READONLY_DATABASE_MIN_CONNECTIONS") - .ok() - .and_then(|x| x.parse().ok()) - .unwrap_or(0), - ) - .max_connections( - dotenvy::var("READONLY_DATABASE_MAX_CONNECTIONS") - .ok() - .and_then(|x| x.parse().ok()) - .unwrap_or(1), - ) + .min_connections(ENV.READONLY_DATABASE_MIN_CONNECTIONS) + .max_connections(ENV.READONLY_DATABASE_MAX_CONNECTIONS) .max_lifetime(Some(Duration::from_secs(60 * 60))) - .connect(&url) + .connect(&ENV.READONLY_DATABASE_URL) .await?; let ro_pool = PgPool::from(ro_pool); @@ -112,8 +84,7 @@ pub async fn connect_all() -> Result<(PgPool, ReadOnlyPgPool), sqlx::Error> { } pub async fn check_for_migrations() -> eyre::Result<()> { - let uri = - dotenvy::var("DATABASE_URL").wrap_err("`DATABASE_URL` not in .env")?; + let uri = &ENV.DATABASE_URL; let uri = uri.as_str(); if !Postgres::database_exists(uri) .await diff --git a/apps/labrinth/src/database/redis/mod.rs b/apps/labrinth/src/database/redis/mod.rs index da2f336edb..1b8ac30c01 100644 --- a/apps/labrinth/src/database/redis/mod.rs +++ b/apps/labrinth/src/database/redis/mod.rs @@ -1,3 +1,5 @@ +use crate::env::ENV; + use super::models::DatabaseError; use ariadne::ids::base62_impl::{parse_base62, to_base62}; use chrono::{TimeZone, Utc}; @@ -42,44 +44,26 @@ impl RedisPool { // testing pool uses a hashmap to mimic redis behaviour for very small data sizes (ie: tests) // PANICS: production pool will panic if redis url is not set pub fn new(meta_namespace: impl Into>) -> Self { - let wait_timeout = - dotenvy::var("REDIS_WAIT_TIMEOUT_MS").ok().map_or_else( - || Duration::from_millis(15000), - |x| { - Duration::from_millis( - x.parse::().expect( - "REDIS_WAIT_TIMEOUT_MS must be a valid u64", - ), - ) - }, - ); - - let url = dotenvy::var("REDIS_URL").expect("Redis URL not set"); + let wait_timeout = Duration::from_millis(ENV.REDIS_WAIT_TIMEOUT_MS); + + let url = &ENV.REDIS_URL; let pool = Config::from_url(url.clone()) .builder() .expect("Error building Redis pool") - .max_size( - dotenvy::var("REDIS_MAX_CONNECTIONS") - .ok() - .and_then(|x| x.parse().ok()) - .unwrap_or(10000), - ) + .max_size(ENV.REDIS_MAX_CONNECTIONS as usize) .wait_timeout(Some(wait_timeout)) .runtime(Runtime::Tokio1) .build() .expect("Redis connection failed"); let pool = RedisPool { - url, + url: url.clone(), pool, cache_list: Arc::new(DashMap::with_capacity(2048)), meta_namespace: meta_namespace.into(), }; - let redis_min_connections = dotenvy::var("REDIS_MIN_CONNECTIONS") - .ok() - .and_then(|x| x.parse::().ok()) - .unwrap_or(0); + let redis_min_connections = ENV.REDIS_MIN_CONNECTIONS; let spawn_min_connections = (0..redis_min_connections) .map(|_| { let pool = pool.clone(); diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs new file mode 100644 index 0000000000..a78b167799 --- /dev/null +++ b/apps/labrinth/src/env.rs @@ -0,0 +1,280 @@ +use std::{any::type_name, convert::Infallible, str::FromStr, sync::LazyLock}; + +use derive_more::{Deref, DerefMut}; +use eyre::{Context, eyre}; +use rust_decimal::Decimal; +use serde::de::DeserializeOwned; + +macro_rules! vars { + ( + $( + $field:ident: $ty:ty $(= $default:expr)?; + )* + ) => { + #[derive(Debug)] + #[allow( + non_snake_case, + reason = "environment variables are UPPER_SNAKE_CASE", + )] + pub struct EnvVars { + $( + pub $field: $ty, + )* + } + + impl EnvVars { + pub fn from_env() -> eyre::Result { + let mut err = eyre!("failed to read environment variables"); + + $( + #[expect( + non_snake_case, + reason = "environment variables are UPPER_SNAKE_CASE", + )] + #[allow( + unused_assignments, + unused_mut, + reason = "`default` is not used if there is no default", + )] + let $field: Option<$ty> = { + let mut default = None::<$ty>; + $( default = Some({ $default }.into()); )? + + match parse_value::<$ty>(stringify!($field), default) { + Ok(value) => Some(value), + Err(source) => { + err = err.wrap_err(eyre!("{source:#}")); + None + } + } + }; + )* + + Ok(EnvVars { + $( + $field: match $field { + Some(value) => value, + None => return Err(err), + }, + )* + }) + } + } + }; +} + +pub static ENV: LazyLock = LazyLock::new(|| { + EnvVars::from_env().unwrap_or_else(|err| panic!("{err:?}")) +}); + +fn parse_value(key: &str, default: Option) -> eyre::Result +where + T: FromStr, + T::Err: std::error::Error + Send + Sync + 'static, +{ + match (dotenvy::var(key), default) { + (Ok(value), _) => value.parse::().wrap_err_with(|| { + eyre!("`{key}` is not a valid `{}`", type_name::()) + }), + (Err(_), Some(default)) => Ok(default), + (Err(_), None) => Err(eyre!("`{key}` missing")), + } +} + +pub fn init() -> eyre::Result<()> { + EnvVars::from_env()?; + LazyLock::force(&ENV); + Ok(()) +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Deref, DerefMut, +)] +pub struct Json(pub T); + +impl FromStr for Json { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s).map(Self) + } +} + +#[derive( + Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deref, DerefMut, +)] +pub struct StringCsv(pub Vec); + +impl FromStr for StringCsv { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + let v = s + .split(',') + .filter(|s| !s.trim().is_empty()) + .map(|s| s.to_string()) + .collect::>(); + Ok(Self(v)) + } +} + +vars! { + SENTRY_ENVIRONMENT: String; + SENTRY_TRACES_SAMPLE_RATE: f32; + SITE_URL: String; + CDN_URL: String; + LABRINTH_ADMIN_KEY: String; + LABRINTH_MEDAL_KEY: String; + LABRINTH_EXTERNAL_NOTIFICATION_KEY: String; + RATE_LIMIT_IGNORE_KEY: String; + DATABASE_URL: String; + MEILISEARCH_READ_ADDR: String; + MEILISEARCH_WRITE_ADDRS: StringCsv; + MEILISEARCH_KEY: String; + REDIS_URL: String; + BIND_ADDR: String; + SELF_ADDR: String; + + LOCAL_INDEX_INTERVAL: u64; + VERSION_INDEX_INTERVAL: u64; + + WHITELISTED_MODPACK_DOMAINS: Json>; + ALLOWED_CALLBACK_URLS: Json>; + ANALYTICS_ALLOWED_ORIGINS: Json>; + + // storage + STORAGE_BACKEND: crate::file_hosting::FileHostKind; + + // s3 + S3_PUBLIC_BUCKET_NAME: String = ""; + S3_PUBLIC_USES_PATH_STYLE_BUCKET: bool = false; + S3_PUBLIC_REGION: String = ""; + S3_PUBLIC_URL: String = ""; + S3_PUBLIC_ACCESS_TOKEN: String = ""; + S3_PUBLIC_SECRET: String = ""; + + S3_PRIVATE_BUCKET_NAME: String = ""; + S3_PRIVATE_USES_PATH_STYLE_BUCKET: bool = false; + S3_PRIVATE_REGION: String = ""; + S3_PRIVATE_URL: String = ""; + S3_PRIVATE_ACCESS_TOKEN: String = ""; + S3_PRIVATE_SECRET: String = ""; + + // local + MOCK_FILE_PATH: String = ""; + + GITHUB_CLIENT_ID: String; + GITHUB_CLIENT_SECRET: String; + GITLAB_CLIENT_ID: String; + GITLAB_CLIENT_SECRET: String; + DISCORD_CLIENT_ID: String; + DISCORD_CLIENT_SECRET: String; + MICROSOFT_CLIENT_ID: String; + MICROSOFT_CLIENT_SECRET: String; + GOOGLE_CLIENT_ID: String; + GOOGLE_CLIENT_SECRET: String; + STEAM_API_KEY: String; + + TREMENDOUS_API_URL: String; + TREMENDOUS_API_KEY: String; + TREMENDOUS_PRIVATE_KEY: String; + + PAYPAL_API_URL: String; + PAYPAL_WEBHOOK_ID: String; + PAYPAL_CLIENT_ID: String; + PAYPAL_CLIENT_SECRET: String; + PAYPAL_NVP_USERNAME: String; + PAYPAL_NVP_PASSWORD: String; + PAYPAL_NVP_SIGNATURE: String; + + PAYPAL_BALANCE_ALERT_THRESHOLD: u64 = 0u64; + BREX_BALANCE_ALERT_THRESHOLD: u64 = 0u64; + TREMENDOUS_BALANCE_ALERT_THRESHOLD: u64 = 0u64; + MURAL_BALANCE_ALERT_THRESHOLD: u64 = 0u64; + + HCAPTCHA_SECRET: String; + + SMTP_USERNAME: String; + SMTP_PASSWORD: String; + SMTP_HOST: String; + SMTP_PORT: u16; + SMTP_TLS: String; + SMTP_FROM_NAME: String; + SMTP_FROM_ADDRESS: String; + + SITE_VERIFY_EMAIL_PATH: String; + SITE_RESET_PASSWORD_PATH: String; + SITE_BILLING_PATH: String; + + SENDY_URL: String; + SENDY_LIST_ID: String; + SENDY_API_KEY: String; + + CLICKHOUSE_REPLICATED: bool; + CLICKHOUSE_URL: String; + CLICKHOUSE_USER: String; + CLICKHOUSE_PASSWORD: String; + CLICKHOUSE_DATABASE: String; + + FLAME_ANVIL_URL: String; + + GOTENBERG_URL: String; + GOTENBERG_CALLBACK_BASE: String; + GOTENBERG_TIMEOUT: u64; + + STRIPE_API_KEY: String; + STRIPE_WEBHOOK_SECRET: String; + + ADITUDE_API_KEY: String; + + PYRO_API_KEY: String; + + BREX_API_URL: String; + BREX_API_KEY: String; + + DELPHI_URL: String; + + AVALARA_1099_API_URL: String; + AVALARA_1099_API_KEY: String; + AVALARA_1099_API_TEAM_ID: String; + AVALARA_1099_COMPANY_ID: String; + + ANROK_API_URL: String; + ANROK_API_KEY: String; + + COMPLIANCE_PAYOUT_THRESHOLD: String; + + PAYOUT_ALERT_SLACK_WEBHOOK: String; + CLOUDFLARE_INTEGRATION: bool = false; + + ARCHON_URL: String; + + MURALPAY_API_URL: String; + MURALPAY_API_KEY: String; + MURALPAY_TRANSFER_API_KEY: String; + MURALPAY_SOURCE_ACCOUNT_ID: muralpay::AccountId = muralpay::AccountId(uuid::Uuid::nil()); + + DEFAULT_AFFILIATE_REVENUE_SPLIT: Decimal; + + DATABASE_ACQUIRE_TIMEOUT_MS: u64 = 30000u64; + DATABASE_MIN_CONNECTIONS: u32 = 0u32; + DATABASE_MAX_CONNECTIONS: u32 = 16u32; + READONLY_DATABASE_URL: String = ""; + READONLY_DATABASE_MIN_CONNECTIONS: u32 = 0u32; + READONLY_DATABASE_MAX_CONNECTIONS: u32 = 1u32; + + REDIS_WAIT_TIMEOUT_MS: u64 = 15000u64; + REDIS_MAX_CONNECTIONS: u32 = 10000u32; + REDIS_MIN_CONNECTIONS: usize = 0usize; + + SEARCH_OPERATION_TIMEOUT: u64 = 300000u64; + + SMTP_REPLY_TO_NAME: String = ""; + SMTP_REPLY_TO_ADDRESS: String = ""; + + PUBLIC_DISCORD_WEBHOOK: String = ""; + MODERATION_SLACK_WEBHOOK: String = ""; + DELPHI_SLACK_WEBHOOK: String = ""; + + TREMENDOUS_CAMPAIGN_ID: String = ""; +} diff --git a/apps/labrinth/src/file_hosting/mock.rs b/apps/labrinth/src/file_hosting/mock.rs index 2565bd287c..3e414bd393 100644 --- a/apps/labrinth/src/file_hosting/mock.rs +++ b/apps/labrinth/src/file_hosting/mock.rs @@ -9,6 +9,8 @@ use hex::ToHex; use sha2::Digest; use std::path::PathBuf; +use crate::env::ENV; + #[derive(Default)] pub struct MockHost(()); @@ -54,8 +56,7 @@ impl FileHost for MockHost { file_name: &str, _expiry_secs: u32, ) -> Result { - let cdn_url = dotenvy::var("CDN_URL").unwrap(); - Ok(format!("{cdn_url}/private/{file_name}")) + Ok(format!("{}/private/{file_name}", ENV.CDN_URL)) } async fn delete_file( @@ -77,7 +78,7 @@ fn get_file_path( file_name: &str, file_publicity: FileHostPublicity, ) -> PathBuf { - let mut path = PathBuf::from(dotenvy::var("MOCK_FILE_PATH").unwrap()); + let mut path = PathBuf::from(ENV.MOCK_FILE_PATH.clone()); if matches!(file_publicity, FileHostPublicity::Private) { path.push("private"); diff --git a/apps/labrinth/src/file_hosting/mod.rs b/apps/labrinth/src/file_hosting/mod.rs index 7de0ff6a9e..2a930a32a6 100644 --- a/apps/labrinth/src/file_hosting/mod.rs +++ b/apps/labrinth/src/file_hosting/mod.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use async_trait::async_trait; use thiserror::Error; @@ -63,3 +65,25 @@ pub trait FileHost { file_publicity: FileHostPublicity, ) -> Result; } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FileHostKind { + S3, + Local, +} + +#[derive(Debug, Error)] +#[error("invalid file host kind")] +pub struct InvalidFileHostKind; + +impl FromStr for FileHostKind { + type Err = InvalidFileHostKind; + + fn from_str(s: &str) -> Result { + Ok(match s { + "s3" => Self::S3, + "local" => Self::Local, + _ => return Err(InvalidFileHostKind), + }) + } +} diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs index dce5404992..4b6f484a95 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -16,11 +16,11 @@ use util::gotenberg::GotenbergClient; use crate::background_task::update_versions; use crate::database::{PgPool, ReadOnlyPgPool}; +use crate::env::ENV; use crate::queue::billing::{index_billing, index_subscriptions}; use crate::queue::moderation::AutomatedModerationQueue; use crate::util::anrok; use crate::util::archon::ArchonClient; -use crate::util::env::{parse_strings_from_var, parse_var}; use crate::util::ratelimit::{AsyncRateLimiter, GCRAParameters}; use sync::friends::handle_pubsub; @@ -28,6 +28,7 @@ pub mod auth; pub mod background_task; pub mod clickhouse; pub mod database; +pub mod env; pub mod file_hosting; pub mod models; pub mod queue; @@ -83,10 +84,7 @@ pub fn app_setup( gotenberg_client: GotenbergClient, enable_background_tasks: bool, ) -> LabrinthConfig { - info!( - "Starting labrinth on {}", - dotenvy::var("BIND_ADDR").unwrap() - ); + info!("Starting labrinth on {}", &ENV.BIND_ADDR); let automated_moderation_queue = web::Data::new(AutomatedModerationQueue::default()); @@ -112,9 +110,8 @@ pub fn app_setup( if enable_background_tasks { // The interval in seconds at which the local database is indexed // for searching. Defaults to 1 hour if unset. - let local_index_interval = Duration::from_secs( - parse_var("LOCAL_INDEX_INTERVAL").unwrap_or(3600), - ); + let local_index_interval = + Duration::from_secs(ENV.LOCAL_INDEX_INTERVAL); let pool_ref = pool.clone(); let search_config_ref = search_config.clone(); let redis_pool_ref = redis_pool.clone(); @@ -142,9 +139,8 @@ pub fn app_setup( } }); - let version_index_interval = Duration::from_secs( - parse_var("VERSION_INDEX_INTERVAL").unwrap_or(1800), - ); + let version_index_interval = + Duration::from_secs(ENV.VERSION_INDEX_INTERVAL); let pool_ref = pool.clone(); let redis_pool_ref = redis_pool.clone(); scheduler.run(version_index_interval, move || { @@ -349,188 +345,3 @@ pub fn utoipa_app_config( .configure(routes::v3::utoipa_config) .configure(routes::internal::utoipa_config); } - -// This is so that env vars not used immediately don't panic at runtime -pub fn check_env_vars() -> bool { - let mut failed = false; - - fn check_var(var: &str) -> bool { - let check = parse_var::(var).is_none(); - if check { - warn!( - "Variable `{}` missing in dotenv or not of type `{}`", - var, - std::any::type_name::() - ); - } - check - } - - failed |= check_var::("SENTRY_ENVIRONMENT"); - failed |= check_var::("SENTRY_TRACES_SAMPLE_RATE"); - failed |= check_var::("SITE_URL"); - failed |= check_var::("CDN_URL"); - failed |= check_var::("LABRINTH_ADMIN_KEY"); - failed |= check_var::("LABRINTH_EXTERNAL_NOTIFICATION_KEY"); - failed |= check_var::("RATE_LIMIT_IGNORE_KEY"); - failed |= check_var::("DATABASE_URL"); - failed |= check_var::("MEILISEARCH_READ_ADDR"); - failed |= check_var::("MEILISEARCH_WRITE_ADDRS"); - failed |= check_var::("MEILISEARCH_KEY"); - failed |= check_var::("REDIS_URL"); - failed |= check_var::("BIND_ADDR"); - failed |= check_var::("SELF_ADDR"); - - failed |= check_var::("STORAGE_BACKEND"); - - let storage_backend = dotenvy::var("STORAGE_BACKEND").ok(); - match storage_backend.as_deref() { - Some("s3") => { - let mut check_var_set = |var_prefix| { - failed |= check_var::(&format!( - "S3_{var_prefix}_BUCKET_NAME" - )); - failed |= check_var::(&format!( - "S3_{var_prefix}_USES_PATH_STYLE_BUCKET" - )); - failed |= - check_var::(&format!("S3_{var_prefix}_REGION")); - failed |= check_var::(&format!("S3_{var_prefix}_URL")); - failed |= check_var::(&format!( - "S3_{var_prefix}_ACCESS_TOKEN" - )); - failed |= - check_var::(&format!("S3_{var_prefix}_SECRET")); - }; - - check_var_set("PUBLIC"); - check_var_set("PRIVATE"); - } - Some("local") => { - failed |= check_var::("MOCK_FILE_PATH"); - } - Some(backend) => { - warn!( - "Variable `STORAGE_BACKEND` contains an invalid value: {backend}. Expected \"s3\" or \"local\"." - ); - failed |= true; - } - _ => { - warn!("Variable `STORAGE_BACKEND` is not set!"); - failed |= true; - } - } - - failed |= check_var::("LOCAL_INDEX_INTERVAL"); - failed |= check_var::("VERSION_INDEX_INTERVAL"); - - if parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS").is_none() { - warn!( - "Variable `WHITELISTED_MODPACK_DOMAINS` missing in dotenv or not a json array of strings" - ); - failed |= true; - } - - if parse_strings_from_var("ALLOWED_CALLBACK_URLS").is_none() { - warn!( - "Variable `ALLOWED_CALLBACK_URLS` missing in dotenv or not a json array of strings" - ); - failed |= true; - } - - failed |= check_var::("GITHUB_CLIENT_ID"); - failed |= check_var::("GITHUB_CLIENT_SECRET"); - failed |= check_var::("GITLAB_CLIENT_ID"); - failed |= check_var::("GITLAB_CLIENT_SECRET"); - failed |= check_var::("DISCORD_CLIENT_ID"); - failed |= check_var::("DISCORD_CLIENT_SECRET"); - failed |= check_var::("MICROSOFT_CLIENT_ID"); - failed |= check_var::("MICROSOFT_CLIENT_SECRET"); - failed |= check_var::("GOOGLE_CLIENT_ID"); - failed |= check_var::("GOOGLE_CLIENT_SECRET"); - failed |= check_var::("STEAM_API_KEY"); - - failed |= check_var::("TREMENDOUS_API_URL"); - failed |= check_var::("TREMENDOUS_API_KEY"); - failed |= check_var::("TREMENDOUS_PRIVATE_KEY"); - - failed |= check_var::("PAYPAL_API_URL"); - failed |= check_var::("PAYPAL_WEBHOOK_ID"); - failed |= check_var::("PAYPAL_CLIENT_ID"); - failed |= check_var::("PAYPAL_CLIENT_SECRET"); - failed |= check_var::("PAYPAL_NVP_USERNAME"); - failed |= check_var::("PAYPAL_NVP_PASSWORD"); - failed |= check_var::("PAYPAL_NVP_SIGNATURE"); - - failed |= check_var::("HCAPTCHA_SECRET"); - - failed |= check_var::("SMTP_USERNAME"); - failed |= check_var::("SMTP_PASSWORD"); - failed |= check_var::("SMTP_HOST"); - failed |= check_var::("SMTP_PORT"); - failed |= check_var::("SMTP_TLS"); - failed |= check_var::("SMTP_FROM_NAME"); - failed |= check_var::("SMTP_FROM_ADDRESS"); - - failed |= check_var::("SITE_VERIFY_EMAIL_PATH"); - failed |= check_var::("SITE_RESET_PASSWORD_PATH"); - failed |= check_var::("SITE_BILLING_PATH"); - - failed |= check_var::("SENDY_URL"); - failed |= check_var::("SENDY_LIST_ID"); - failed |= check_var::("SENDY_API_KEY"); - - if parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS").is_none() { - warn!( - "Variable `ANALYTICS_ALLOWED_ORIGINS` missing in dotenv or not a json array of strings" - ); - failed |= true; - } - - failed |= check_var::("CLICKHOUSE_REPLICATED"); - failed |= check_var::("CLICKHOUSE_URL"); - failed |= check_var::("CLICKHOUSE_USER"); - failed |= check_var::("CLICKHOUSE_PASSWORD"); - failed |= check_var::("CLICKHOUSE_DATABASE"); - - failed |= check_var::("FLAME_ANVIL_URL"); - - failed |= check_var::("GOTENBERG_URL"); - failed |= check_var::("GOTENBERG_CALLBACK_BASE"); - failed |= check_var::("GOTENBERG_TIMEOUT"); - - failed |= check_var::("STRIPE_API_KEY"); - failed |= check_var::("STRIPE_WEBHOOK_SECRET"); - - failed |= check_var::("ADITUDE_API_KEY"); - - failed |= check_var::("PYRO_API_KEY"); - - failed |= check_var::("BREX_API_URL"); - failed |= check_var::("BREX_API_KEY"); - - failed |= check_var::("DELPHI_URL"); - - failed |= check_var::("AVALARA_1099_API_URL"); - failed |= check_var::("AVALARA_1099_API_KEY"); - failed |= check_var::("AVALARA_1099_API_TEAM_ID"); - failed |= check_var::("AVALARA_1099_COMPANY_ID"); - - failed |= check_var::("ANROK_API_URL"); - failed |= check_var::("ANROK_API_KEY"); - - failed |= check_var::("COMPLIANCE_PAYOUT_THRESHOLD"); - - failed |= check_var::("PAYOUT_ALERT_SLACK_WEBHOOK"); - - failed |= check_var::("ARCHON_URL"); - - failed |= check_var::("MURALPAY_API_URL"); - failed |= check_var::("MURALPAY_API_KEY"); - failed |= check_var::("MURALPAY_TRANSFER_API_KEY"); - failed |= check_var::("MURALPAY_SOURCE_ACCOUNT_ID"); - - failed |= check_var::("DEFAULT_AFFILIATE_REVENUE_SPLIT"); - - failed -} diff --git a/apps/labrinth/src/main.rs b/apps/labrinth/src/main.rs index 4763a7af4b..d18984391c 100644 --- a/apps/labrinth/src/main.rs +++ b/apps/labrinth/src/main.rs @@ -4,21 +4,21 @@ use actix_web::{App, HttpServer}; use actix_web_prom::PrometheusMetricsBuilder; use clap::Parser; -use labrinth::app_config; use labrinth::background_task::BackgroundTask; use labrinth::database::redis::RedisPool; -use labrinth::file_hosting::{S3BucketConfig, S3Host}; +use labrinth::env::ENV; +use labrinth::file_hosting::{FileHostKind, S3BucketConfig, S3Host}; use labrinth::queue::email::EmailQueue; use labrinth::search; use labrinth::util::anrok; -use labrinth::util::env::parse_var; use labrinth::util::gotenberg::GotenbergClient; use labrinth::util::ratelimit::rate_limit_middleware; use labrinth::utoipa_app_config; -use labrinth::{check_env_vars, clickhouse, database, file_hosting}; +use labrinth::{app_config, env}; +use labrinth::{clickhouse, database, file_hosting}; use std::ffi::CStr; use std::sync::Arc; -use tracing::{Instrument, error, info, info_span}; +use tracing::{Instrument, info, info_span}; use tracing_actix_web::TracingLogger; use utoipa::OpenApi; use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}; @@ -58,11 +58,7 @@ fn main() -> std::io::Result<()> { color_eyre::install().expect("failed to install `color-eyre`"); dotenvy::dotenv().ok(); modrinth_util::log::init().expect("failed to initialize logging"); - - if check_env_vars() { - error!("Some environment variables are missing!"); - std::process::exit(1); - } + env::init().expect("failed to initialize environment variables"); // Sentry must be set up before the async runtime is started // @@ -70,11 +66,8 @@ fn main() -> std::io::Result<()> { // Has no effect if not set. let sentry = sentry::init(sentry::ClientOptions { release: sentry::release_name!(), - traces_sample_rate: dotenvy::var("SENTRY_TRACES_SAMPLE_RATE") - .unwrap() - .parse() - .expect("failed to parse `SENTRY_TRACES_SAMPLE_RATE` as number"), - environment: Some(dotenvy::var("SENTRY_ENVIRONMENT").unwrap().into()), + traces_sample_rate: ENV.SENTRY_TRACES_SAMPLE_RATE, + environment: Some((&ENV.SENTRY_ENVIRONMENT).into()), ..Default::default() }); if sentry.is_enabled() { @@ -99,10 +92,7 @@ async fn app() -> std::io::Result<()> { .unwrap(); if args.run_background_task.is_none() { - info!( - "Starting labrinth on {}", - dotenvy::var("BIND_ADDR").unwrap() - ); + info!("Starting labrinth on {}", &ENV.BIND_ADDR); if !args.no_migrations { database::check_for_migrations() @@ -119,40 +109,44 @@ async fn app() -> std::io::Result<()> { // Redis connector let redis_pool = RedisPool::new(""); - let storage_backend = - dotenvy::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string()); - + let storage_backend = ENV.STORAGE_BACKEND; let file_host: Arc = - match storage_backend.as_str() { - "s3" => { - let config_from_env = |bucket_type| S3BucketConfig { - name: parse_var(&format!("S3_{bucket_type}_BUCKET_NAME")) - .unwrap(), - uses_path_style: parse_var(&format!( - "S3_{bucket_type}_USES_PATH_STYLE_BUCKET" - )) - .unwrap(), - region: parse_var(&format!("S3_{bucket_type}_REGION")) - .unwrap(), - url: parse_var(&format!("S3_{bucket_type}_URL")).unwrap(), - access_token: parse_var(&format!( - "S3_{bucket_type}_ACCESS_TOKEN" - )) - .unwrap(), - secret: parse_var(&format!("S3_{bucket_type}_SECRET")) - .unwrap(), + match storage_backend { + FileHostKind::S3 => { + let not_empty = |v: &str| -> String { + assert!(!v.is_empty(), "S3 env var is empty"); + v.to_string() }; Arc::new( S3Host::new( - config_from_env("PUBLIC"), - config_from_env("PRIVATE"), + S3BucketConfig { + name: not_empty(&ENV.S3_PUBLIC_BUCKET_NAME), + uses_path_style: ENV + .S3_PUBLIC_USES_PATH_STYLE_BUCKET, + region: not_empty(&ENV.S3_PUBLIC_REGION), + url: not_empty(&ENV.S3_PUBLIC_URL), + access_token: not_empty( + &ENV.S3_PUBLIC_ACCESS_TOKEN, + ), + secret: not_empty(&ENV.S3_PUBLIC_SECRET), + }, + S3BucketConfig { + name: not_empty(&ENV.S3_PRIVATE_BUCKET_NAME), + uses_path_style: ENV + .S3_PRIVATE_USES_PATH_STYLE_BUCKET, + region: not_empty(&ENV.S3_PRIVATE_REGION), + url: not_empty(&ENV.S3_PRIVATE_URL), + access_token: not_empty( + &ENV.S3_PRIVATE_ACCESS_TOKEN, + ), + secret: not_empty(&ENV.S3_PRIVATE_SECRET), + }, ) .unwrap(), ) } - "local" => Arc::new(file_hosting::MockHost::new()), - _ => panic!("Invalid storage backend specified. Aborting startup!"), + FileHostKind::Local => Arc::new(file_hosting::MockHost::new()), }; info!("Initializing clickhouse connection"); @@ -160,8 +154,7 @@ async fn app() -> std::io::Result<()> { let search_config = search::SearchConfig::new(None); - let stripe_client = - stripe::Client::new(dotenvy::var("STRIPE_API_KEY").unwrap()); + let stripe_client = stripe::Client::new(ENV.STRIPE_API_KEY.clone()); let anrok_client = anrok::Client::from_env().unwrap(); let email_queue = @@ -272,7 +265,7 @@ async fn app() -> std::io::Result<()> { .into_app() .configure(|cfg| app_config(cfg, labrinth_config.clone())) }) - .bind(dotenvy::var("BIND_ADDR").unwrap())? + .bind(&ENV.BIND_ADDR)? .run() .await } diff --git a/apps/labrinth/src/models/v3/pack.rs b/apps/labrinth/src/models/v3/pack.rs index 1e95999c34..4993e86df7 100644 --- a/apps/labrinth/src/models/v3/pack.rs +++ b/apps/labrinth/src/models/v3/pack.rs @@ -1,6 +1,4 @@ -use crate::{ - models::v2::projects::LegacySideType, util::env::parse_strings_from_var, -}; +use crate::{env::ENV, models::v2::projects::LegacySideType}; use path_util::SafeRelativeUtf8UnixPathBuf; use serde::{Deserialize, Serialize}; use validator::Validate; @@ -44,9 +42,7 @@ fn validate_download_url( return Err(validator::ValidationError::new("invalid URL")); } - let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS") - .unwrap_or_default(); - if !domains.contains( + if !ENV.WHITELISTED_MODPACK_DOMAINS.contains( &url.domain() .ok_or_else(|| validator::ValidationError::new("invalid URL"))? .to_string(), diff --git a/apps/labrinth/src/queue/billing.rs b/apps/labrinth/src/queue/billing.rs index c529e9f57a..967dabaf76 100644 --- a/apps/labrinth/src/queue/billing.rs +++ b/apps/labrinth/src/queue/billing.rs @@ -12,6 +12,7 @@ use crate::database::models::{ }; use crate::database::redis::RedisPool; use crate::database::{PgPool, PgTransaction}; +use crate::env::ENV; use crate::models::billing::{ ChargeStatus, ChargeType, PaymentPlatform, Price, PriceDuration, ProductMetadata, SubscriptionMetadata, SubscriptionStatus, @@ -913,10 +914,10 @@ async fn unprovision_subscriptions( let res = reqwest::Client::new() .post(format!( "{}/modrinth/v0/servers/{}/suspend", - dotenvy::var("ARCHON_URL")?, + ENV.ARCHON_URL, server_id )) - .header("X-Master-Key", dotenvy::var("PYRO_API_KEY")?) + .header("X-Master-Key", &ENV.PYRO_API_KEY) .json(&serde_json::json!({ "reason": if charge.status == ChargeStatus::Cancelled || charge.status == ChargeStatus::Expiring { "cancelled" diff --git a/apps/labrinth/src/queue/email.rs b/apps/labrinth/src/queue/email.rs index 554b91ed46..bb9a205468 100644 --- a/apps/labrinth/src/queue/email.rs +++ b/apps/labrinth/src/queue/email.rs @@ -5,6 +5,7 @@ use crate::database::models::notifications_template_item::NotificationTemplate; use crate::database::models::user_item::DBUser; use crate::database::redis::RedisPool; use crate::database::{PgPool, PgTransaction}; +use crate::env::ENV; use crate::models::notifications::{NotificationBody, NotificationType}; use crate::models::v3::notifications::{ NotificationChannel, NotificationDeliveryStatus, @@ -36,16 +37,16 @@ impl Mailer { ) -> Result>, MailError> { let maybe_transport = match self { Mailer::Uninitialized => { - let username = dotenvy::var("SMTP_USERNAME")?; - let password = dotenvy::var("SMTP_PASSWORD")?; - let host = dotenvy::var("SMTP_HOST")?; - let port = - dotenvy::var("SMTP_PORT")?.parse::().unwrap_or(465); + let username = &ENV.SMTP_USERNAME; + let password = &ENV.SMTP_PASSWORD; + let host = &ENV.SMTP_HOST; + let port = ENV.SMTP_PORT; - let creds = (!username.is_empty()) - .then(|| Credentials::new(username, password)); + let creds = (!username.is_empty()).then(|| { + Credentials::new(username.clone(), password.clone()) + }); - let tls_setting = match dotenvy::var("SMTP_TLS")?.as_str() { + let tls_setting = match ENV.SMTP_TLS.as_str() { "none" => Tls::None, "opportunistic_start_tls" => Tls::Opportunistic( TlsParameters::new(host.to_string())?, @@ -65,7 +66,7 @@ impl Mailer { }; let mut mailer = - AsyncSmtpTransport::::relay(&host)? + AsyncSmtpTransport::::relay(host)? .port(port) .tls(tls_setting); diff --git a/apps/labrinth/src/queue/email/templates.rs b/apps/labrinth/src/queue/email/templates.rs index 3815db40d3..e5cecdb3cc 100644 --- a/apps/labrinth/src/queue/email/templates.rs +++ b/apps/labrinth/src/queue/email/templates.rs @@ -8,6 +8,7 @@ use crate::database::models::{ DBOrganization, DBProject, DBUser, DatabaseError, }; use crate::database::redis::RedisPool; +use crate::env::ENV; use crate::models::v3::notifications::NotificationBody; use crate::routes::ApiError; use crate::util::error::Context; @@ -96,10 +97,18 @@ pub struct MailingIdentity { impl MailingIdentity { pub fn from_env() -> dotenvy::Result { Ok(Self { - from_name: dotenvy::var("SMTP_FROM_NAME")?, - from_address: dotenvy::var("SMTP_FROM_ADDRESS")?, - reply_name: dotenvy::var("SMTP_REPLY_TO_NAME").ok(), - reply_address: dotenvy::var("SMTP_REPLY_TO_ADDRESS").ok(), + from_name: ENV.SMTP_FROM_NAME.clone(), + from_address: ENV.SMTP_FROM_ADDRESS.clone(), + reply_name: if ENV.SMTP_REPLY_TO_NAME.is_empty() { + None + } else { + Some(ENV.SMTP_REPLY_TO_NAME.clone()) + }, + reply_address: if ENV.SMTP_REPLY_TO_ADDRESS.is_empty() { + None + } else { + Some(ENV.SMTP_REPLY_TO_ADDRESS.clone()) + }, }) } } @@ -558,9 +567,7 @@ async fn collect_template_variables( NotificationBody::ResetPassword { flow } => { let url = format!( "{}/{}?flow={}", - dotenvy::var("SITE_URL")?, - dotenvy::var("SITE_RESET_PASSWORD_PATH")?, - flow + ENV.SITE_URL, ENV.SITE_RESET_PASSWORD_PATH, flow ); map.insert(RESETPASSWORD_URL, url); @@ -571,9 +578,7 @@ async fn collect_template_variables( NotificationBody::VerifyEmail { flow } => { let url = format!( "{}/{}?flow={}", - dotenvy::var("SITE_URL")?, - dotenvy::var("SITE_VERIFY_EMAIL_PATH")?, - flow + ENV.SITE_URL, ENV.SITE_VERIFY_EMAIL_PATH, flow ); map.insert(VERIFYEMAIL_URL, url); @@ -603,11 +608,7 @@ async fn collect_template_variables( } NotificationBody::PaymentFailed { amount, service } => { - let url = format!( - "{}/{}", - dotenvy::var("SITE_URL")?, - dotenvy::var("SITE_BILLING_PATH")?, - ); + let url = format!("{}/{}", ENV.SITE_URL, ENV.SITE_BILLING_PATH,); let mut map = HashMap::new(); map.insert(PAYMENTFAILED_AMOUNT, amount.clone()); @@ -748,8 +749,7 @@ async fn dynamic_email_body( key: &str, ) -> Result { get_or_set_cached_dynamic_html(redis, key, || async { - let site_url = dotenvy::var("SITE_URL") - .wrap_internal_err("SITE_URL is not set")?; + let site_url = &ENV.SITE_URL; let site_url = site_url.trim_end_matches('/'); let url = format!("{site_url}/_internal/templates/email/dynamic"); diff --git a/apps/labrinth/src/queue/moderation.rs b/apps/labrinth/src/queue/moderation.rs index 21ced3616b..7c8340d9db 100644 --- a/apps/labrinth/src/queue/moderation.rs +++ b/apps/labrinth/src/queue/moderation.rs @@ -4,6 +4,7 @@ use crate::database::PgPool; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::thread_item::ThreadMessageBuilder; use crate::database::redis::RedisPool; +use crate::env::ENV; use crate::models::ids::ProjectId; use crate::models::notifications::NotificationBody; use crate::models::pack::{PackFile, PackFileHash, PackFormat}; @@ -454,7 +455,7 @@ impl AutomatedModerationQueue { let client = reqwest::Client::new(); let res = client - .post(format!("{}/v1/fingerprints", dotenvy::var("FLAME_ANVIL_URL")?)) + .post(format!("{}/v1/fingerprints", ENV.FLAME_ANVIL_URL)) .json(&serde_json::json!({ "fingerprints": hashes.iter().filter_map(|x| x.3).collect::>() })) @@ -553,11 +554,11 @@ impl AutomatedModerationQueue { continue; } - let flame_projects = if flame_files.is_empty() { - Vec::new() - } else { - let res = client - .post(format!("{}v1/mods", dotenvy::var("FLAME_ANVIL_URL")?)) + let flame_projects = if flame_files.is_empty() { + Vec::new() + } else { + let res = client + .post(format!("{}v1/mods", ENV.FLAME_ANVIL_URL)) .json(&serde_json::json!({ "modIds": flame_files.iter().map(|x| x.1).collect::>() })) @@ -664,16 +665,16 @@ impl AutomatedModerationQueue { .insert_many(members.into_iter().map(|x| x.user_id).collect(), &mut transaction, &redis) .await?; - if let Ok(webhook_url) = dotenvy::var("MODERATION_SLACK_WEBHOOK") { + if !ENV.MODERATION_SLACK_WEBHOOK.is_empty() { crate::util::webhook::send_slack_project_webhook( project.inner.id.into(), &pool, &redis, - webhook_url, + &ENV.MODERATION_SLACK_WEBHOOK, Some( format!( "*<{}/user/AutoMod|AutoMod>* changed project status from *{}* to *Rejected*", - dotenvy::var("SITE_URL")?, + ENV.SITE_URL, &project.inner.status.as_friendly_str(), ) .to_string(), diff --git a/apps/labrinth/src/queue/payouts/affiliate.rs b/apps/labrinth/src/queue/payouts/affiliate.rs index 980b18b0fe..913e973749 100644 --- a/apps/labrinth/src/queue/payouts/affiliate.rs +++ b/apps/labrinth/src/queue/payouts/affiliate.rs @@ -1,4 +1,5 @@ use crate::database::PgPool; +use crate::env::ENV; use chrono::{Datelike, Duration, TimeZone, Utc}; use eyre::{Context, Result, eyre}; use rust_decimal::{Decimal, dec}; @@ -62,11 +63,7 @@ pub async fn process_affiliate_payouts(postgres: &PgPool) -> Result<()> { .await .wrap_err("failed to fetch charges awaiting affiliate payout")?; - let default_affiliate_revenue_split = - dotenvy::var("DEFAULT_AFFILIATE_REVENUE_SPLIT") - .wrap_err("no env var `DEFAULT_AFFILIATE_REVENUE_SPLIT`")? - .parse::() - .wrap_err("`DEFAULT_AFFILIATE_REVENUE_SPLIT` is not a decimal")?; + let default_affiliate_revenue_split = ENV.DEFAULT_AFFILIATE_REVENUE_SPLIT; let ( mut insert_usap_charges, diff --git a/apps/labrinth/src/queue/payouts/flow/tremendous.rs b/apps/labrinth/src/queue/payouts/flow/tremendous.rs index 0c292b0041..21f6b7100e 100644 --- a/apps/labrinth/src/queue/payouts/flow/tremendous.rs +++ b/apps/labrinth/src/queue/payouts/flow/tremendous.rs @@ -8,6 +8,7 @@ use serde_json::json; use crate::{ database::models::payout_item::DBPayout, + env::ENV, models::payouts::{ PayoutMethod, PayoutMethodFee, PayoutMethodType, PayoutStatus, TremendousCurrency, TremendousDetails, TremendousForexResponse, @@ -210,7 +211,7 @@ pub(super) async fn execute( "products": [ method_id, ], - "campaign_id": dotenvy::var("TREMENDOUS_CAMPAIGN_ID")?, + "campaign_id": ENV.TREMENDOUS_CAMPAIGN_ID.as_str(), }] }); diff --git a/apps/labrinth/src/queue/payouts/mod.rs b/apps/labrinth/src/queue/payouts/mod.rs index cd6c38b8a6..ffd2ee94a6 100644 --- a/apps/labrinth/src/queue/payouts/mod.rs +++ b/apps/labrinth/src/queue/payouts/mod.rs @@ -2,13 +2,13 @@ use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::payouts_values_notifications; use crate::database::redis::RedisPool; use crate::database::{PgPool, PgTransaction}; +use crate::env::ENV; use crate::models::payouts::{ PayoutDecimal, PayoutInterval, PayoutMethod, PayoutMethodType, TremendousForexResponse, }; use crate::models::projects::MonetizationStatus; use crate::routes::ApiError; -use crate::util::env::env_var; use crate::util::error::Context; use crate::util::webhook::{ PayoutSourceAlertType, send_slack_payout_source_alert_webhook, @@ -76,21 +76,18 @@ impl Default for PayoutsQueue { } pub fn create_muralpay_client() -> Result { - let api_url = env_var("MURALPAY_API_URL")?; - let api_key = env_var("MURALPAY_API_KEY")?; - let transfer_api_key = env_var("MURALPAY_TRANSFER_API_KEY")?; - Ok(muralpay::Client::new(api_url, api_key, transfer_api_key)) + Ok(muralpay::Client::new( + &ENV.MURALPAY_API_URL, + ENV.MURALPAY_API_KEY.as_str(), + ENV.MURALPAY_TRANSFER_API_KEY.as_str(), + )) } pub fn create_muralpay() -> Result { let client = create_muralpay_client()?; - let source_account_id = env_var("MURALPAY_SOURCE_ACCOUNT_ID")? - .parse::() - .wrap_err("failed to parse source account ID")?; - Ok(MuralPayConfig { client, - source_account_id, + source_account_id: ENV.MURALPAY_SOURCE_ACCOUNT_ID, }) } @@ -185,11 +182,8 @@ impl PayoutsQueue { let mut creds = self.credential.write().await; let client = reqwest::Client::new(); - let combined_key = format!( - "{}:{}", - dotenvy::var("PAYPAL_CLIENT_ID")?, - dotenvy::var("PAYPAL_CLIENT_SECRET")? - ); + let combined_key = + format!("{}:{}", ENV.PAYPAL_CLIENT_ID, ENV.PAYPAL_CLIENT_SECRET); let formatted_key = format!( "Basic {}", base64::engine::general_purpose::STANDARD.encode(combined_key) @@ -206,7 +200,7 @@ impl PayoutsQueue { } let credential: PaypalCredential = client - .post(format!("{}oauth2/token", dotenvy::var("PAYPAL_API_URL")?)) + .post(format!("{}oauth2/token", ENV.PAYPAL_API_URL)) .header("Accept", "application/json") .header("Accept-Language", "en_US") .header("Authorization", formatted_key) @@ -274,7 +268,7 @@ impl PayoutsQueue { if no_api_prefix.unwrap_or(false) { path.to_string() } else { - format!("{}{path}", dotenvy::var("PAYPAL_API_URL")?) + format!("{}{path}", ENV.PAYPAL_API_URL) }, ) .header( @@ -355,13 +349,10 @@ impl PayoutsQueue { ) -> Result { let client = reqwest::Client::new(); let mut request = client - .request( - method, - format!("{}{path}", dotenvy::var("TREMENDOUS_API_URL")?), - ) + .request(method, format!("{}{path}", ENV.TREMENDOUS_API_URL)) .header( "Authorization", - format!("Bearer {}", dotenvy::var("TREMENDOUS_API_KEY")?), + format!("Bearer {}", ENV.TREMENDOUS_API_KEY), ); if let Some(body) = body { @@ -511,8 +502,8 @@ impl PayoutsQueue { let client = reqwest::Client::new(); let res = client - .get(format!("{}accounts/cash", dotenvy::var("BREX_API_URL")?)) - .bearer_auth(&dotenvy::var("BREX_API_KEY")?) + .get(format!("{}accounts/cash", ENV.BREX_API_URL)) + .bearer_auth(&ENV.BREX_API_KEY) .send() .await? .json::() @@ -538,16 +529,16 @@ impl PayoutsQueue { pub async fn get_paypal_balance() -> Result, ApiError> { - let api_username = dotenvy::var("PAYPAL_NVP_USERNAME")?; - let api_password = dotenvy::var("PAYPAL_NVP_PASSWORD")?; - let api_signature = dotenvy::var("PAYPAL_NVP_SIGNATURE")?; + let api_username = &ENV.PAYPAL_NVP_USERNAME; + let api_password = &ENV.PAYPAL_NVP_PASSWORD; + let api_signature = &ENV.PAYPAL_NVP_SIGNATURE; let mut params = HashMap::new(); params.insert("METHOD", "GetBalance"); params.insert("VERSION", "204"); - params.insert("USER", &api_username); - params.insert("PWD", &api_password); - params.insert("SIGNATURE", &api_signature); + params.insert("USER", api_username); + params.insert("PWD", api_password); + params.insert("SIGNATURE", api_signature); params.insert("RETURNALLCURRENCIES", "1"); let endpoint = "https://api-3t.paypal.com/nvp"; @@ -870,7 +861,7 @@ pub async fn make_aditude_request( ) -> Result, ApiError> { let request = reqwest::Client::new() .post("https://cloud.aditude.io/api/public/insights/metrics") - .bearer_auth(&dotenvy::var("ADITUDE_API_KEY")?) + .bearer_auth(&ENV.ADITUDE_API_KEY) .json(&serde_json::json!({ "metrics": metrics, "range": range, @@ -1326,25 +1317,25 @@ pub async fn insert_bank_balances_and_webhook( if inserted { check_balance_with_webhook( "paypal", - "PAYPAL_BALANCE_ALERT_THRESHOLD", + ENV.PAYPAL_BALANCE_ALERT_THRESHOLD, paypal_result, ) .await?; check_balance_with_webhook( "brex", - "BREX_BALANCE_ALERT_THRESHOLD", + ENV.BREX_BALANCE_ALERT_THRESHOLD, brex_result, ) .await?; check_balance_with_webhook( "tremendous", - "TREMENDOUS_BALANCE_ALERT_THRESHOLD", + ENV.TREMENDOUS_BALANCE_ALERT_THRESHOLD, tremendous_result, ) .await?; check_balance_with_webhook( "mural", - "MURAL_BALANCE_ALERT_THRESHOLD", + ENV.MURAL_BALANCE_ALERT_THRESHOLD, mural_result, ) .await?; @@ -1357,14 +1348,11 @@ pub async fn insert_bank_balances_and_webhook( async fn check_balance_with_webhook( source: &str, - threshold_env_var_name: &str, + threshold: u64, result: Result, ApiError>, ) -> Result, ApiError> { - let maybe_threshold = dotenvy::var(threshold_env_var_name) - .ok() - .and_then(|x| x.parse::().ok()) - .filter(|x| *x != 0); - let payout_alert_webhook = dotenvy::var("PAYOUT_ALERT_SLACK_WEBHOOK")?; + let maybe_threshold = if threshold > 0 { Some(threshold) } else { None }; + let payout_alert_webhook = &ENV.PAYOUT_ALERT_SLACK_WEBHOOK; match &result { Ok(Some(account_balance)) => { @@ -1379,7 +1367,7 @@ async fn check_balance_with_webhook( threshold, current_balance: available, }, - &payout_alert_webhook, + payout_alert_webhook, ) .await?; } @@ -1394,7 +1382,7 @@ async fn check_balance_with_webhook( source: source.to_owned(), display_error: error.to_string(), }, - &payout_alert_webhook, + payout_alert_webhook, ) .await?; } diff --git a/apps/labrinth/src/routes/analytics.rs b/apps/labrinth/src/routes/analytics.rs index 09e95335ac..8e99fa430a 100644 --- a/apps/labrinth/src/routes/analytics.rs +++ b/apps/labrinth/src/routes/analytics.rs @@ -1,13 +1,13 @@ use crate::auth::get_user_from_headers; use crate::database::PgPool; use crate::database::redis::RedisPool; +use crate::env::ENV; use crate::models::analytics::{PageView, Playtime}; use crate::models::pats::Scopes; use crate::queue::analytics::AnalyticsQueue; use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::util::date::get_current_tenths_of_ms; -use crate::util::env::parse_strings_from_var; use actix_web::{HttpRequest, HttpResponse}; use actix_web::{post, web}; use serde::Deserialize; @@ -73,11 +73,10 @@ pub async fn page_view_ingest( })?; let url_origin = url.origin().ascii_serialization(); - let is_valid_url_origin = - parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS") - .unwrap_or_default() - .iter() - .any(|origin| origin == "*" || url_origin == *origin); + let is_valid_url_origin = ENV + .ANALYTICS_ALLOWED_ORIGINS + .iter() + .any(|origin| origin == "*" || url_origin == *origin); if !is_valid_url_origin { return Err(ApiError::InvalidInput( diff --git a/apps/labrinth/src/routes/internal/affiliate.rs b/apps/labrinth/src/routes/internal/affiliate.rs index 8f1e54fe62..5dca27b38f 100644 --- a/apps/labrinth/src/routes/internal/affiliate.rs +++ b/apps/labrinth/src/routes/internal/affiliate.rs @@ -1,6 +1,7 @@ use std::{collections::HashMap, net::Ipv4Addr, sync::Arc}; use crate::database::PgPool; +use crate::env::ENV; use crate::{ auth::get_user_from_headers, database::{ @@ -13,10 +14,7 @@ use crate::{ }, queue::{analytics::AnalyticsQueue, session::AuthQueue}, routes::analytics::FILTERED_HEADERS, - util::{ - date::get_current_tenths_of_ms, env::parse_strings_from_var, - error::Context, - }, + util::{date::get_current_tenths_of_ms, error::Context}, }; use actix_web::{HttpRequest, delete, get, patch, post, put, web}; use ariadne::ids::UserId; @@ -70,11 +68,10 @@ async fn ingest_click( })?; let url_origin = url.origin().ascii_serialization(); - let is_valid_url_origin = - parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS") - .unwrap_or_default() - .iter() - .any(|origin| origin == "*" || url_origin == *origin); + let is_valid_url_origin = ENV + .ANALYTICS_ALLOWED_ORIGINS + .iter() + .any(|origin| origin == "*" || url_origin == *origin); if !is_valid_url_origin { return Err(ApiError::InvalidInput( diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs index e454634c03..f53d40778a 100644 --- a/apps/labrinth/src/routes/internal/billing.rs +++ b/apps/labrinth/src/routes/internal/billing.rs @@ -12,6 +12,7 @@ use crate::database::models::{ }; use crate::database::redis::RedisPool; use crate::database::{PgPool, PgTransaction}; +use crate::env::ENV; use crate::models::billing::{ Charge, ChargeStatus, ChargeType, PaymentPlatform, Price, PriceDuration, Product, ProductMetadata, ProductPrice, SubscriptionMetadata, @@ -1437,7 +1438,7 @@ pub async fn active_servers( pool: web::Data, query: web::Query, ) -> Result { - let master_key = dotenvy::var("PYRO_API_KEY")?; + let master_key = &ENV.PYRO_API_KEY; if req .head() @@ -1626,7 +1627,7 @@ pub async fn stripe_webhook( if let Ok(event) = Webhook::construct_event( &payload, stripe_signature, - &dotenvy::var("STRIPE_WEBHOOK_SECRET")?, + &ENV.STRIPE_WEBHOOK_SECRET, ) { struct PaymentIntentMetadata { pub user_item: crate::database::models::user_item::DBUser, @@ -2036,23 +2037,23 @@ pub async fn stripe_webhook( client .post(format!( "{}/modrinth/v0/servers/{}/unsuspend", - dotenvy::var("ARCHON_URL")?, + ENV.ARCHON_URL, id )) - .header("X-Master-Key", dotenvy::var("PYRO_API_KEY")?) + .header("X-Master-Key", &ENV.PYRO_API_KEY) .send() .await? .error_for_status()?; client .post(format!( - "{}/modrinth/v0/servers/{}/reallocate", - dotenvy::var("ARCHON_URL")?, - id - )) + "{}/modrinth/v0/servers/{}/reallocate", + ENV.ARCHON_URL, + id + )) .header( "X-Master-Key", - dotenvy::var("PYRO_API_KEY")?, + &ENV.PYRO_API_KEY, ) .json(&body) .send() @@ -2114,9 +2115,9 @@ pub async fn stripe_webhook( let res = client .post(format!( "{}/modrinth/v0/servers/create", - dotenvy::var("ARCHON_URL")?, + ENV.ARCHON_URL, )) - .header("X-Master-Key", dotenvy::var("PYRO_API_KEY")?) + .header("X-Master-Key", &ENV.PYRO_API_KEY) .json(&serde_json::json!({ "user_id": to_base62(metadata.user_item.id.0 as u64), "name": server_name, diff --git a/apps/labrinth/src/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 8103a91945..0f0815567d 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -1,6 +1,7 @@ use std::{collections::HashMap, fmt::Write, sync::LazyLock, time::Instant}; use crate::database::PgPool; +use crate::env::ENV; use actix_web::{HttpRequest, HttpResponse, get, post, web}; use chrono::{DateTime, Utc}; use eyre::eyre; @@ -89,7 +90,7 @@ impl DelphiReport { pool: &PgPool, redis: &RedisPool, ) -> Result<(), ApiError> { - let webhook_url = dotenvy::var("DELPHI_SLACK_WEBHOOK")?; + let webhook_url = ENV.DELPHI_SLACK_WEBHOOK.clone(); let mut message_header = format!("⚠️ Suspicious traces found at {}", self.url); @@ -115,7 +116,7 @@ impl DelphiReport { self.project_id, pool, redis, - webhook_url, + &webhook_url, Some(message_header), ) .await @@ -317,7 +318,7 @@ pub async fn run( ); DELPHI_CLIENT - .post(dotenvy::var("DELPHI_URL")?) + .post(&ENV.DELPHI_URL) .json(&serde_json::json!({ "url": file_data.url, "project_id": ProjectId(file_data.project_id.0 as u64), @@ -407,7 +408,7 @@ async fn issue_type_schema( &cache_entry .insert(( DELPHI_CLIENT - .get(format!("{}/schema", dotenvy::var("DELPHI_URL")?)) + .get(format!("{}/schema", ENV.DELPHI_URL)) .send() .await .and_then(|res| res.error_for_status()) diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index 84510685bf..cd89d786bd 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -8,6 +8,7 @@ use crate::database::models::flow_item::DBFlow; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::{DBUser, DBUserId}; use crate::database::redis::RedisPool; +use crate::env::ENV; use crate::file_hosting::{FileHost, FileHostPublicity}; use crate::models::notifications::NotificationBody; use crate::models::pats::Scopes; @@ -17,7 +18,6 @@ use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::routes::internal::session::issue_session; use crate::util::captcha::check_hcaptcha; -use crate::util::env::parse_strings_from_var; use crate::util::error::Context; use crate::util::ext::get_image_ext; use crate::util::img::upload_image_optimized; @@ -257,41 +257,41 @@ impl AuthProvider { &self, state: String, ) -> Result { - let self_addr = dotenvy::var("SELF_ADDR")?; + let self_addr = &ENV.SELF_ADDR; let raw_redirect_uri = format!("{self_addr}/v2/auth/callback"); let redirect_uri = urlencoding::encode(&raw_redirect_uri); Ok(match self { AuthProvider::GitHub => { - let client_id = dotenvy::var("GITHUB_CLIENT_ID")?; + let client_id = &ENV.GITHUB_CLIENT_ID; format!( "https://github.com/login/oauth/authorize?client_id={client_id}&prompt=select_account&state={state}&scope=read%3Auser%20user%3Aemail&redirect_uri={redirect_uri}", ) } AuthProvider::Discord => { - let client_id = dotenvy::var("DISCORD_CLIENT_ID")?; + let client_id = &ENV.DISCORD_CLIENT_ID; format!( "https://discord.com/api/oauth2/authorize?client_id={client_id}&state={state}&response_type=code&scope=identify%20email&redirect_uri={redirect_uri}" ) } AuthProvider::Microsoft => { - let client_id = dotenvy::var("MICROSOFT_CLIENT_ID")?; + let client_id = &ENV.MICROSOFT_CLIENT_ID; format!( "https://login.live.com/oauth20_authorize.srf?client_id={client_id}&response_type=code&scope=user.read&state={state}&prompt=select_account&redirect_uri={redirect_uri}" ) } AuthProvider::GitLab => { - let client_id = dotenvy::var("GITLAB_CLIENT_ID")?; + let client_id = &ENV.GITLAB_CLIENT_ID; format!( "https://gitlab.com/oauth/authorize?client_id={client_id}&state={state}&scope=read_user+profile+email&response_type=code&redirect_uri={redirect_uri}", ) } AuthProvider::Google => { - let client_id = dotenvy::var("GOOGLE_CLIENT_ID")?; + let client_id = &ENV.GOOGLE_CLIENT_ID; format!( "https://accounts.google.com/o/oauth2/v2/auth?client_id={}&state={}&scope={}&response_type=code&redirect_uri={}", @@ -317,8 +317,8 @@ impl AuthProvider { ) } AuthProvider::PayPal => { - let api_url = dotenvy::var("PAYPAL_API_URL")?; - let client_id = dotenvy::var("PAYPAL_CLIENT_ID")?; + let api_url = &ENV.PAYPAL_API_URL; + let client_id = &ENV.PAYPAL_CLIENT_ID; let auth_url = if api_url.contains("sandbox") { "sandbox.paypal.com" @@ -340,8 +340,7 @@ impl AuthProvider { &self, query: HashMap, ) -> Result { - let redirect_uri = - format!("{}/v2/auth/callback", dotenvy::var("SELF_ADDR")?); + let redirect_uri = format!("{}/v2/auth/callback", &ENV.SELF_ADDR); #[derive(Deserialize)] struct AccessToken { @@ -353,8 +352,8 @@ impl AuthProvider { let code = query .get("code") .ok_or_else(|| AuthenticationError::InvalidCredentials)?; - let client_id = dotenvy::var("GITHUB_CLIENT_ID")?; - let client_secret = dotenvy::var("GITHUB_CLIENT_SECRET")?; + let client_id = ENV.GITHUB_CLIENT_ID.as_str(); + let client_secret = ENV.GITHUB_CLIENT_SECRET.as_str(); let url = format!( "https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={client_secret}&code={code}&redirect_uri={redirect_uri}" @@ -374,12 +373,12 @@ impl AuthProvider { let code = query .get("code") .ok_or_else(|| AuthenticationError::InvalidCredentials)?; - let client_id = dotenvy::var("DISCORD_CLIENT_ID")?; - let client_secret = dotenvy::var("DISCORD_CLIENT_SECRET")?; + let client_id = ENV.DISCORD_CLIENT_ID.as_str(); + let client_secret = ENV.DISCORD_CLIENT_SECRET.as_str(); let mut map = HashMap::new(); - map.insert("client_id", &*client_id); - map.insert("client_secret", &*client_secret); + map.insert("client_id", client_id); + map.insert("client_secret", client_secret); map.insert("code", code); map.insert("grant_type", "authorization_code"); map.insert("redirect_uri", &redirect_uri); @@ -399,12 +398,12 @@ impl AuthProvider { let code = query .get("code") .ok_or_else(|| AuthenticationError::InvalidCredentials)?; - let client_id = dotenvy::var("MICROSOFT_CLIENT_ID")?; - let client_secret = dotenvy::var("MICROSOFT_CLIENT_SECRET")?; + let client_id = ENV.MICROSOFT_CLIENT_ID.as_str(); + let client_secret = ENV.MICROSOFT_CLIENT_SECRET.as_str(); let mut map = HashMap::new(); - map.insert("client_id", &*client_id); - map.insert("client_secret", &*client_secret); + map.insert("client_id", client_id); + map.insert("client_secret", client_secret); map.insert("code", code); map.insert("grant_type", "authorization_code"); map.insert("redirect_uri", &redirect_uri); @@ -424,12 +423,12 @@ impl AuthProvider { let code = query .get("code") .ok_or_else(|| AuthenticationError::InvalidCredentials)?; - let client_id = dotenvy::var("GITLAB_CLIENT_ID")?; - let client_secret = dotenvy::var("GITLAB_CLIENT_SECRET")?; + let client_id = ENV.GITLAB_CLIENT_ID.as_str(); + let client_secret = ENV.GITLAB_CLIENT_SECRET.as_str(); let mut map = HashMap::new(); - map.insert("client_id", &*client_id); - map.insert("client_secret", &*client_secret); + map.insert("client_id", client_id); + map.insert("client_secret", client_secret); map.insert("code", code); map.insert("grant_type", "authorization_code"); map.insert("redirect_uri", &redirect_uri); @@ -449,12 +448,12 @@ impl AuthProvider { let code = query .get("code") .ok_or_else(|| AuthenticationError::InvalidCredentials)?; - let client_id = dotenvy::var("GOOGLE_CLIENT_ID")?; - let client_secret = dotenvy::var("GOOGLE_CLIENT_SECRET")?; + let client_id = ENV.GOOGLE_CLIENT_ID.as_str(); + let client_secret = ENV.GOOGLE_CLIENT_SECRET.as_str(); let mut map = HashMap::new(); - map.insert("client_id", &*client_id); - map.insert("client_secret", &*client_secret); + map.insert("client_id", client_id); + map.insert("client_secret", client_secret); map.insert("code", code); map.insert("grant_type", "authorization_code"); map.insert("redirect_uri", &redirect_uri); @@ -529,9 +528,9 @@ impl AuthProvider { let code = query .get("code") .ok_or_else(|| AuthenticationError::InvalidCredentials)?; - let api_url = dotenvy::var("PAYPAL_API_URL")?; - let client_id = dotenvy::var("PAYPAL_CLIENT_ID")?; - let client_secret = dotenvy::var("PAYPAL_CLIENT_SECRET")?; + let api_url = ENV.PAYPAL_API_URL.as_str(); + let client_id = ENV.PAYPAL_CLIENT_ID.as_str(); + let client_secret = ENV.PAYPAL_CLIENT_SECRET.as_str(); let mut map = HashMap::new(); map.insert("code", code.as_str()); @@ -580,9 +579,7 @@ impl AuthProvider { .get("x-oauth-client-id") .and_then(|x| x.to_str().ok()); - if client_id - != Some(&*dotenvy::var("GITHUB_CLIENT_ID").unwrap()) - { + if client_id != Some(ENV.GITHUB_CLIENT_ID.as_str()) { return Err(AuthenticationError::InvalidClientId); } } @@ -732,7 +729,7 @@ impl AuthProvider { } } AuthProvider::Steam => { - let api_key = dotenvy::var("STEAM_API_KEY")?; + let api_key = &ENV.STEAM_API_KEY; #[derive(Deserialize)] struct SteamResponse { @@ -797,7 +794,7 @@ impl AuthProvider { pub country: String, } - let api_url = dotenvy::var("PAYPAL_API_URL")?; + let api_url = &ENV.PAYPAL_API_URL; let paypal_user: PayPalUser = reqwest::Client::new() .get(format!( @@ -1100,10 +1097,11 @@ pub async fn init( let url = url::Url::parse(&info.url).map_err(|_| AuthenticationError::Url)?; - let allowed_callback_urls = - parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default(); let domain = url.host_str().ok_or(AuthenticationError::Url)?; - if !allowed_callback_urls.iter().any(|x| domain.ends_with(x)) + if !ENV + .ALLOWED_CALLBACK_URLS + .iter() + .any(|x| domain.ends_with(x)) && domain != "modrinth.com" { return Err(AuthenticationError::Url); @@ -1396,9 +1394,9 @@ pub async fn delete_auth_provider( pub async fn check_sendy_subscription( email: &str, ) -> Result { - let url = dotenvy::var("SENDY_URL")?; - let id = dotenvy::var("SENDY_LIST_ID")?; - let api_key = dotenvy::var("SENDY_API_KEY")?; + let url = &ENV.SENDY_URL; + let id = &ENV.SENDY_LIST_ID; + let api_key = &ENV.SENDY_API_KEY; if url.is_empty() || url == "none" { tracing::info!( @@ -1408,9 +1406,9 @@ pub async fn check_sendy_subscription( } let mut form = HashMap::new(); - form.insert("api_key", &*api_key); + form.insert("api_key", api_key.as_str()); form.insert("email", email); - form.insert("list_id", &*id); + form.insert("list_id", id.as_str()); let client = reqwest::Client::new(); let response = client diff --git a/apps/labrinth/src/routes/internal/globals.rs b/apps/labrinth/src/routes/internal/globals.rs index 092af9ecc2..0317c5a943 100644 --- a/apps/labrinth/src/routes/internal/globals.rs +++ b/apps/labrinth/src/routes/internal/globals.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, sync::LazyLock}; +use crate::env::ENV; use actix_web::{get, web}; use serde::{Deserialize, Serialize}; @@ -28,7 +29,8 @@ static GLOBALS: LazyLock = LazyLock::new(|| Globals { tax_compliance_thresholds: [(2025, 600), (2026, 2000)] .into_iter() .collect(), - captcha_enabled: dotenvy::var("HCAPTCHA_SECRET").is_ok_and(|x| x != "none"), + captcha_enabled: !ENV.HCAPTCHA_SECRET.is_empty() + && ENV.HCAPTCHA_SECRET != "none", }); /// Gets configured global non-secret variables for this backend instance. diff --git a/apps/labrinth/src/routes/internal/session.rs b/apps/labrinth/src/routes/internal/session.rs index d41cfd9edb..e6f29945e2 100644 --- a/apps/labrinth/src/routes/internal/session.rs +++ b/apps/labrinth/src/routes/internal/session.rs @@ -4,11 +4,11 @@ use crate::database::models::session_item::DBSession; use crate::database::models::session_item::SessionBuilder; use crate::database::redis::RedisPool; use crate::database::{PgPool, PgTransaction}; +use crate::env::ENV; use crate::models::pats::Scopes; use crate::models::sessions::Session; use crate::queue::session::AuthQueue; use crate::routes::ApiError; -use crate::util::env::parse_var; use actix_web::http::header::AUTHORIZATION; use actix_web::web::{Data, ServiceConfig, scope}; use actix_web::{HttpRequest, HttpResponse, delete, get, post, web}; @@ -41,7 +41,7 @@ pub async fn get_session_metadata( req: &HttpRequest, ) -> Result { let conn_info = req.connection_info().clone(); - let ip_addr = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) { + let ip_addr = if ENV.CLOUDFLARE_INTEGRATION { if let Some(header) = req.headers().get("CF-Connecting-IP") { header.to_str().ok() } else { diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs index f3696a3ff5..7b0ffab92e 100644 --- a/apps/labrinth/src/routes/mod.rs +++ b/apps/labrinth/src/routes/mod.rs @@ -1,8 +1,8 @@ use crate::database::models::DelphiReportIssueDetailsId; +use crate::env::ENV; use crate::file_hosting::FileHostingError; use crate::routes::analytics::{page_view_ingest, playtime_ingest}; use crate::util::cors::default_cors; -use crate::util::env::parse_strings_from_var; use actix_cors::Cors; use actix_files::Files; use actix_web::http::StatusCode; @@ -40,10 +40,7 @@ pub fn root_config(cfg: &mut web::ServiceConfig) { .wrap( Cors::default() .allowed_origin_fn(|origin, _req_head| { - let allowed_origins = - parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS") - .unwrap_or_default(); - + let allowed_origins = &ENV.ANALYTICS_ALLOWED_ORIGINS; allowed_origins.contains(&"*".to_string()) || allowed_origins.contains( &origin diff --git a/apps/labrinth/src/routes/updates.rs b/apps/labrinth/src/routes/updates.rs index 0478c706ab..044b8dc7b5 100644 --- a/apps/labrinth/src/routes/updates.rs +++ b/apps/labrinth/src/routes/updates.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use crate::database::PgPool; +use crate::env::ENV; use actix_web::{HttpRequest, HttpResponse, get, web}; use serde::{Deserialize, Serialize}; @@ -94,11 +95,7 @@ pub async fn forge_updates( } let mut response = ForgeUpdates { - homepage: format!( - "{}/mod/{}", - dotenvy::var("SITE_URL").unwrap_or_default(), - id - ), + homepage: format!("{}/mod/{}", ENV.SITE_URL, id), promos: HashMap::new(), }; diff --git a/apps/labrinth/src/routes/v3/payouts.rs b/apps/labrinth/src/routes/v3/payouts.rs index d0a2dbdb39..bf5c72c14d 100644 --- a/apps/labrinth/src/routes/v3/payouts.rs +++ b/apps/labrinth/src/routes/v3/payouts.rs @@ -4,6 +4,7 @@ use crate::database::PgPool; use crate::database::models::DBUserId; use crate::database::models::{generate_payout_id, users_compliance}; use crate::database::redis::RedisPool; +use crate::env::ENV; use crate::models::ids::PayoutId; use crate::models::pats::Scopes; use crate::models::payouts::{PayoutMethodType, PayoutStatus, Withdrawal}; @@ -212,7 +213,7 @@ pub async fn paypal_webhook( \"webhook_id\": \"{}\", \"webhook_event\": {body} }}", - dotenvy::var("PAYPAL_WEBHOOK_ID")? + ENV.PAYPAL_WEBHOOK_ID, )), None, ) @@ -322,7 +323,7 @@ pub async fn tremendous_webhook( })?; let mut mac: Hmac = Hmac::new_from_slice( - dotenvy::var("TREMENDOUS_PRIVATE_KEY")?.as_bytes(), + ENV.TREMENDOUS_PRIVATE_KEY.as_bytes(), ) .map_err(|_| ApiError::Payments("error initializing HMAC".to_string()))?; mac.update(body.as_bytes()); @@ -1114,9 +1115,7 @@ async fn update_compliance_status( } fn tax_compliance_payout_threshold() -> Option { - dotenvy::var("COMPLIANCE_PAYOUT_THRESHOLD") - .ok() - .and_then(|s| s.parse().ok()) + ENV.COMPLIANCE_PAYOUT_THRESHOLD.parse().ok() } #[derive(Deserialize)] diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index 60590e8f31..b497fd157d 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -12,6 +12,7 @@ use crate::database::models::{ use crate::database::redis::RedisPool; use crate::database::{self, models as db_models}; use crate::database::{PgPool, PgTransaction}; +use crate::env::ENV; use crate::file_hosting::{FileHost, FileHostPublicity}; use crate::models; use crate::models::ids::{ProjectId, VersionId}; @@ -425,13 +426,13 @@ pub async fn project_edit( if status.is_searchable() && !project_item.inner.webhook_sent - && let Ok(webhook_url) = dotenvy::var("PUBLIC_DISCORD_WEBHOOK") + && !ENV.PUBLIC_DISCORD_WEBHOOK.is_empty() { crate::util::webhook::send_discord_webhook( project_item.inner.id.into(), &pool, &redis, - webhook_url, + &ENV.PUBLIC_DISCORD_WEBHOOK, None, ) .await @@ -449,18 +450,16 @@ pub async fn project_edit( .await?; } - if user.role.is_mod() - && let Ok(webhook_url) = dotenvy::var("MODERATION_SLACK_WEBHOOK") - { + if user.role.is_mod() && !ENV.MODERATION_SLACK_WEBHOOK.is_empty() { crate::util::webhook::send_slack_project_webhook( project_item.inner.id.into(), &pool, &redis, - webhook_url, + &ENV.MODERATION_SLACK_WEBHOOK, Some( format!( "*<{}/user/{}|{}>* changed project status from *{}* to *{}*", - dotenvy::var("SITE_URL")?, + ENV.SITE_URL, user.username, user.username, &project_item.inner.status.as_friendly_str(), diff --git a/apps/labrinth/src/routes/v3/threads.rs b/apps/labrinth/src/routes/v3/threads.rs index fe38b55275..136313cdfe 100644 --- a/apps/labrinth/src/routes/v3/threads.rs +++ b/apps/labrinth/src/routes/v3/threads.rs @@ -7,6 +7,7 @@ use crate::database::models::image_item; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::thread_item::ThreadMessageBuilder; use crate::database::redis::RedisPool; +use crate::env::ENV; use crate::file_hosting::{FileHost, FileHostPublicity}; use crate::models::ids::{ThreadId, ThreadMessageId}; use crate::models::images::{Image, ImageContext}; @@ -631,9 +632,8 @@ pub async fn message_delete( let images = database::DBImage::get_many_contexted(context, &mut transaction) .await?; - let cdn_url = dotenvy::var("CDN_URL")?; for image in images { - let name = image.url.split(&format!("{cdn_url}/")).nth(1); + let name = image.url.split(&format!("{}/", ENV.CDN_URL)).nth(1); if let Some(icon_path) = name { file_host .delete_file( diff --git a/apps/labrinth/src/routes/v3/version_creation.rs b/apps/labrinth/src/routes/v3/version_creation.rs index 9b91a6bf81..7998dfdd43 100644 --- a/apps/labrinth/src/routes/v3/version_creation.rs +++ b/apps/labrinth/src/routes/v3/version_creation.rs @@ -11,6 +11,7 @@ use crate::database::models::version_item::{ }; use crate::database::models::{self, DBOrganization, image_item}; use crate::database::redis::RedisPool; +use crate::env::ENV; use crate::file_hosting::{FileHost, FileHostPublicity}; use crate::models::ids::{ImageId, ProjectId, VersionId}; use crate::models::images::{Image, ImageContext}; @@ -974,7 +975,7 @@ pub async fn upload_file( version_files.push(VersionFileBuilder { filename: file_name.to_string(), - url: format!("{}/{file_path_encode}", dotenvy::var("CDN_URL")?), + url: format!("{}/{file_path_encode}", ENV.CDN_URL), hashes: vec![ models::version_item::HashBuilder { algorithm: "sha1".to_string(), diff --git a/apps/labrinth/src/search/indexing/mod.rs b/apps/labrinth/src/search/indexing/mod.rs index 2da327ce14..37ae99a2e3 100644 --- a/apps/labrinth/src/search/indexing/mod.rs +++ b/apps/labrinth/src/search/indexing/mod.rs @@ -5,6 +5,7 @@ use std::time::Duration; use crate::database::PgPool; use crate::database::redis::RedisPool; +use crate::env::ENV; use crate::search::{SearchConfig, UploadSearchProject}; use ariadne::ids::base62_impl::to_base62; use futures::StreamExt; @@ -41,12 +42,7 @@ pub enum IndexingError { const MEILISEARCH_CHUNK_SIZE: usize = 50000; // 10_000_000 fn search_operation_timeout() -> std::time::Duration { - let default_ms = 5 * 60 * 1000; // 5 minutes - let ms = dotenvy::var("SEARCH_OPERATION_TIMEOUT") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(default_ms); - std::time::Duration::from_millis(ms) + std::time::Duration::from_millis(ENV.SEARCH_OPERATION_TIMEOUT) } pub async fn remove_documents( diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index e64aa07071..636abd9de7 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -1,3 +1,4 @@ +use crate::env::ENV; use crate::models::projects::SearchRequest; use crate::{models::error::ApiError, search::indexing::IndexingError}; use actix_web::HttpResponse; @@ -134,26 +135,11 @@ impl SearchConfig { // Panics if the environment variables are not set, // but these are already checked for on startup. pub fn new(meta_namespace: Option) -> Self { - let address_many = dotenvy::var("MEILISEARCH_WRITE_ADDRS") - .expect("MEILISEARCH_WRITE_ADDRS not set"); - - let read_lb_address = dotenvy::var("MEILISEARCH_READ_ADDR") - .expect("MEILISEARCH_READ_ADDR not set"); - - let addresses = address_many - .split(',') - .filter(|s| !s.trim().is_empty()) - .map(|s| s.to_string()) - .collect::>(); - - let key = - dotenvy::var("MEILISEARCH_KEY").expect("MEILISEARCH_KEY not set"); - Self { - addresses, - key, + addresses: ENV.MEILISEARCH_WRITE_ADDRS.0.clone(), + key: ENV.MEILISEARCH_KEY.clone(), meta_namespace: meta_namespace.unwrap_or_default(), - read_lb_address, + read_lb_address: ENV.MEILISEARCH_READ_ADDR.clone(), } } diff --git a/apps/labrinth/src/test/api_v2/mod.rs b/apps/labrinth/src/test/api_v2/mod.rs index 67b7ecb865..6ce5151881 100644 --- a/apps/labrinth/src/test/api_v2/mod.rs +++ b/apps/labrinth/src/test/api_v2/mod.rs @@ -3,6 +3,7 @@ use super::{ environment::LocalService, }; use crate::LabrinthConfig; +use crate::env::ENV; use actix_web::{App, dev::ServiceResponse, test}; use async_trait::async_trait; use std::rc::Rc; @@ -46,10 +47,7 @@ impl Api for ApiV2 { async fn reset_search_index(&self) -> ServiceResponse { let req = actix_web::test::TestRequest::post() .uri("/v2/admin/_force_reindex") - .append_header(( - "Modrinth-Admin", - dotenvy::var("LABRINTH_ADMIN_KEY").unwrap(), - )) + .append_header(("Modrinth-Admin", ENV.LABRINTH_ADMIN_KEY.clone())) .to_request(); self.call(req).await } diff --git a/apps/labrinth/src/test/api_v3/mod.rs b/apps/labrinth/src/test/api_v3/mod.rs index fa99718fdb..3cfd373699 100644 --- a/apps/labrinth/src/test/api_v3/mod.rs +++ b/apps/labrinth/src/test/api_v3/mod.rs @@ -3,6 +3,7 @@ use super::{ environment::LocalService, }; use crate::LabrinthConfig; +use crate::env::ENV; use actix_web::{App, dev::ServiceResponse, test}; use async_trait::async_trait; use std::rc::Rc; @@ -51,10 +52,7 @@ impl Api for ApiV3 { async fn reset_search_index(&self) -> ServiceResponse { let req = actix_web::test::TestRequest::post() .uri("/_internal/admin/_force_reindex") - .append_header(( - "Modrinth-Admin", - dotenvy::var("LABRINTH_ADMIN_KEY").unwrap(), - )) + .append_header(("Modrinth-Admin", ENV.LABRINTH_ADMIN_KEY.clone())) .to_request(); self.call(req).await } diff --git a/apps/labrinth/src/test/database.rs b/apps/labrinth/src/test/database.rs index 02b6f556f9..3c97a7a2f1 100644 --- a/apps/labrinth/src/test/database.rs +++ b/apps/labrinth/src/test/database.rs @@ -1,6 +1,7 @@ use crate::database::PgPool; use crate::database::redis::RedisPool; use crate::database::{MIGRATOR, ReadOnlyPgPool}; +use crate::env::ENV; use crate::search; use sqlx::postgres::PgPoolOptions; use std::time::Duration; @@ -57,15 +58,14 @@ impl TemporaryDatabase { let temp_database_name = generate_random_name("labrinth_tests_db_"); println!("Creating temporary database: {}", &temp_database_name); - let database_url = - dotenvy::var("DATABASE_URL").expect("No database URL"); + let database_url = &ENV.DATABASE_URL; // Create the temporary (and template database, if needed) - Self::create_temporary(&database_url, &temp_database_name).await; + Self::create_temporary(database_url, &temp_database_name).await; // Pool to the temporary database let mut temporary_url = - Url::parse(&database_url).expect("Invalid database URL"); + Url::parse(database_url).expect("Invalid database URL"); temporary_url.set_path(&format!("/{}", &temp_database_name)); let temp_db_url = temporary_url.to_string(); @@ -139,10 +139,8 @@ impl TemporaryDatabase { } // Switch to template - let url = - dotenvy::var("DATABASE_URL").expect("No database URL"); - let mut template_url = - Url::parse(&url).expect("Invalid database URL"); + let mut template_url = Url::parse(&ENV.DATABASE_URL) + .expect("Invalid database URL"); template_url.set_path(&format!("/{TEMPLATE_DATABASE_NAME}")); let pool = sqlx::PgPool::connect(template_url.as_str()) @@ -234,11 +232,10 @@ impl TemporaryDatabase { // If a temporary db is created, it must be cleaned up with cleanup. // This means that dbs will only 'remain' if a test fails (for examination of the db), and will be cleaned up otherwise. pub async fn cleanup(mut self) { - let database_url = - dotenvy::var("DATABASE_URL").expect("No database URL"); + let database_url = &ENV.DATABASE_URL; self.pool.close().await; - self.pool = sqlx::PgPool::connect(&database_url) + self.pool = sqlx::PgPool::connect(database_url) .await .map(PgPool::from) .expect("Connection to main database failed"); diff --git a/apps/labrinth/src/test/mod.rs b/apps/labrinth/src/test/mod.rs index dd49b297b5..435a51c426 100644 --- a/apps/labrinth/src/test/mod.rs +++ b/apps/labrinth/src/test/mod.rs @@ -1,8 +1,9 @@ +use crate::env::ENV; use crate::queue::email::EmailQueue; use crate::util::anrok; use crate::util::gotenberg::GotenbergClient; use crate::{LabrinthConfig, file_hosting}; -use crate::{check_env_vars, clickhouse}; +use crate::{clickhouse, env}; use std::sync::Arc; pub mod api_common; @@ -22,12 +23,8 @@ pub mod search; // If making a test, you should probably use environment::TestEnvironment::build() (which calls this) pub async fn setup(db: &database::TemporaryDatabase) -> LabrinthConfig { println!("Setting up labrinth config"); - dotenvy::dotenv().ok(); - - if check_env_vars() { - println!("Some environment variables are missing!"); - } + env::init().expect("failed to initialize environment variables"); let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); @@ -39,8 +36,7 @@ pub async fn setup(db: &database::TemporaryDatabase) -> LabrinthConfig { Arc::new(file_hosting::MockHost::new()); let mut clickhouse = clickhouse::init_client().await.unwrap(); - let stripe_client = - stripe::Client::new(dotenvy::var("STRIPE_API_KEY").unwrap()); + let stripe_client = stripe::Client::new(ENV.STRIPE_API_KEY.clone()); let anrok_client = anrok::Client::from_env().unwrap(); let email_queue = diff --git a/apps/labrinth/src/util/anrok.rs b/apps/labrinth/src/util/anrok.rs index b9ed799801..451d0e5b8f 100644 --- a/apps/labrinth/src/util/anrok.rs +++ b/apps/labrinth/src/util/anrok.rs @@ -7,6 +7,9 @@ use serde_with::{DisplayFromStr, serde_as}; use thiserror::Error; use tracing::trace; +use crate::env::ENV; +use crate::routes::ApiError; + pub fn transaction_id_stripe_pi(pi: &stripe::PaymentIntentId) -> String { format!("stripe:charge:{pi}") } @@ -154,19 +157,14 @@ pub struct Client { } impl Client { - pub fn from_env() -> Result { - let api_key = dotenvy::var("ANROK_API_KEY")?; - let api_url = dotenvy::var("ANROK_API_URL")? - .trim_start_matches('/') - .to_owned(); - + pub fn from_env() -> Result { Ok(Self { client: reqwest::Client::builder() .user_agent("Modrinth") .build() .expect("AnrokClient to build"), - api_key, - api_url, + api_key: ENV.ANROK_API_KEY.clone(), + api_url: ENV.ANROK_API_URL.trim_start_matches('/').to_owned(), }) } diff --git a/apps/labrinth/src/util/archon.rs b/apps/labrinth/src/util/archon.rs index 621e923fba..39c7da6833 100644 --- a/apps/labrinth/src/util/archon.rs +++ b/apps/labrinth/src/util/archon.rs @@ -2,6 +2,7 @@ use reqwest::header::HeaderName; use serde::{Deserialize, Serialize}; use uuid::Uuid; +use crate::env::ENV; use crate::routes::ApiError; const X_MASTER_KEY: HeaderName = HeaderName::from_static("x-master-key"); @@ -42,13 +43,12 @@ impl ArchonClient { pub fn from_env() -> Result { let client = reqwest::Client::new(); - let base_url = - dotenvy::var("ARCHON_URL")?.trim_end_matches('/').to_owned(); + let base_url = ENV.ARCHON_URL.trim_end_matches('/').to_owned(); Ok(Self { client, base_url, - pyro_api_key: dotenvy::var("PYRO_API_KEY")?, + pyro_api_key: ENV.PYRO_API_KEY.clone(), }) } diff --git a/apps/labrinth/src/util/avalara1099.rs b/apps/labrinth/src/util/avalara1099.rs index 5caa01c8c3..d36e5edf62 100644 --- a/apps/labrinth/src/util/avalara1099.rs +++ b/apps/labrinth/src/util/avalara1099.rs @@ -1,4 +1,5 @@ use crate::database::models::{DBUserId, users_compliance::FormType}; +use crate::env::ENV; use crate::routes::ApiError; use ariadne::ids::base62_impl::to_base62; use chrono::Datelike; @@ -131,10 +132,10 @@ fn team_request( method: reqwest::Method, route: &str, ) -> Result<(reqwest::RequestBuilder, String), ApiError> { - let key = dotenvy::var("AVALARA_1099_API_KEY")?; - let url = dotenvy::var("AVALARA_1099_API_URL")?; - let team = dotenvy::var("AVALARA_1099_API_TEAM_ID")?; - let company = dotenvy::var("AVALARA_1099_COMPANY_ID")?; + let key = &ENV.AVALARA_1099_API_KEY; + let url = &ENV.AVALARA_1099_API_URL; + let team = &ENV.AVALARA_1099_API_TEAM_ID; + let company = &ENV.AVALARA_1099_COMPANY_ID; let url = url.trim_end_matches('/'); @@ -144,8 +145,8 @@ fn team_request( client .request(method, format!("{url}/v1/{team}{route}")) .header(reqwest::header::USER_AGENT, "Modrinth") - .bearer_auth(&key), - company, + .bearer_auth(key), + company.to_string(), )) } diff --git a/apps/labrinth/src/util/captcha.rs b/apps/labrinth/src/util/captcha.rs index e59d5ae5d7..af32e51927 100644 --- a/apps/labrinth/src/util/captcha.rs +++ b/apps/labrinth/src/util/captcha.rs @@ -1,5 +1,5 @@ +use crate::env::ENV; use crate::routes::ApiError; -use crate::util::env::parse_var; use actix_web::HttpRequest; use serde::Deserialize; use std::collections::HashMap; @@ -8,7 +8,7 @@ pub async fn check_hcaptcha( req: &HttpRequest, challenge: &str, ) -> Result { - let secret = dotenvy::var("HCAPTCHA_SECRET")?; + let secret = &ENV.HCAPTCHA_SECRET; if secret.is_empty() || secret == "none" { tracing::info!("hCaptcha secret not set, skipping check"); @@ -16,7 +16,7 @@ pub async fn check_hcaptcha( } let conn_info = req.connection_info().clone(); - let ip_addr = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) { + let ip_addr = if ENV.CLOUDFLARE_INTEGRATION { if let Some(header) = req.headers().get("CF-Connecting-IP") { header.to_str().ok() } else { @@ -38,7 +38,7 @@ pub async fn check_hcaptcha( let mut form = HashMap::new(); form.insert("response", challenge); - form.insert("secret", &*secret); + form.insert("secret", secret); form.insert("remoteip", ip_addr); let val: Response = client diff --git a/apps/labrinth/src/util/env.rs b/apps/labrinth/src/util/env.rs deleted file mode 100644 index 0bcb6bde6d..0000000000 --- a/apps/labrinth/src/util/env.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::str::FromStr; - -use eyre::{Context, eyre}; - -pub fn env_var(key: &str) -> eyre::Result { - dotenvy::var(key) - .wrap_err_with(|| eyre!("missing environment variable `{key}`")) -} - -pub fn parse_var(var: &str) -> Option { - dotenvy::var(var).ok().and_then(|i| i.parse().ok()) -} -pub fn parse_strings_from_var(var: &'static str) -> Option> { - dotenvy::var(var) - .ok() - .and_then(|s| serde_json::from_str::>(&s).ok()) -} diff --git a/apps/labrinth/src/util/gotenberg.rs b/apps/labrinth/src/util/gotenberg.rs index 8fd55362ae..854c27e124 100644 --- a/apps/labrinth/src/util/gotenberg.rs +++ b/apps/labrinth/src/util/gotenberg.rs @@ -1,8 +1,8 @@ use crate::database::redis::RedisPool; +use crate::env::ENV; use crate::models::ids::PayoutId; use crate::routes::ApiError; use crate::routes::internal::gotenberg::{GotenbergDocument, GotenbergError}; -use crate::util::env::env_var; use crate::util::error::Context; use actix_web::http::header::HeaderName; use chrono::{DateTime, Datelike, Utc}; @@ -71,15 +71,14 @@ impl GotenbergClient { .build() .wrap_err("failed to build reqwest client")?; - let gotenberg_url = env_var("GOTENBERG_URL")?; - let site_url = env_var("SITE_URL")?; - let callback_base = env_var("GOTENBERG_CALLBACK_BASE")?; - Ok(Self { client, - gotenberg_url: gotenberg_url.trim_end_matches('/').to_owned(), - site_url: site_url.trim_end_matches('/').to_owned(), - callback_base: callback_base.trim_end_matches('/').to_owned(), + gotenberg_url: ENV.GOTENBERG_URL.trim_end_matches('/').to_owned(), + site_url: ENV.SITE_URL.trim_end_matches('/').to_owned(), + callback_base: ENV + .GOTENBERG_CALLBACK_BASE + .trim_end_matches('/') + .to_owned(), redis, }) } @@ -189,12 +188,7 @@ impl GotenbergClient { self.generate_payment_statement(statement).await?; - let timeout_ms = env_var("GOTENBERG_TIMEOUT") - .map_err(ApiError::Internal)? - .parse::() - .wrap_internal_err( - "`GOTENBERG_TIMEOUT` is not a valid number of milliseconds", - )?; + let timeout_ms = ENV.GOTENBERG_TIMEOUT; let [_key, document] = tokio::time::timeout( Duration::from_millis(timeout_ms), diff --git a/apps/labrinth/src/util/guards.rs b/apps/labrinth/src/util/guards.rs index 08b3df4688..08388baffb 100644 --- a/apps/labrinth/src/util/guards.rs +++ b/apps/labrinth/src/util/guards.rs @@ -1,47 +1,33 @@ use actix_web::guard::GuardContext; use actix_web::http::header::X_FORWARDED_FOR; +use crate::env::ENV; + pub const ADMIN_KEY_HEADER: &str = "Modrinth-Admin"; pub const MEDAL_KEY_HEADER: &str = "X-Medal-Access-Key"; pub const EXTERNAL_NOTIFICATION_KEY_HEADER: &str = "External-Notification-Key"; pub fn admin_key_guard(ctx: &GuardContext) -> bool { - let admin_key = std::env::var("LABRINTH_ADMIN_KEY").expect( - "No admin key provided, this should have been caught by check_env_vars", - ); ctx.head() .headers() .get(ADMIN_KEY_HEADER) - .is_some_and(|it| it.as_bytes() == admin_key.as_bytes()) + .is_some_and(|it| it.as_bytes() == ENV.LABRINTH_ADMIN_KEY.as_bytes()) } pub fn medal_key_guard(ctx: &GuardContext) -> bool { - let maybe_medal_key = dotenvy::var("LABRINTH_MEDAL_KEY").ok(); - - match maybe_medal_key { - None => false, - Some(medal_key) => ctx - .head() - .headers() - .get(MEDAL_KEY_HEADER) - .is_some_and(|it| it.as_bytes() == medal_key.as_bytes()), - } + ctx.head() + .headers() + .get(MEDAL_KEY_HEADER) + .is_some_and(|it| it.as_bytes() == ENV.LABRINTH_MEDAL_KEY.as_bytes()) } pub fn external_notification_key_guard(ctx: &GuardContext) -> bool { - let maybe_external_notification_key = - dotenvy::var("LABRINTH_EXTERNAL_NOTIFICATION_KEY").ok(); - - match maybe_external_notification_key { - None => false, - Some(external_notification_key) => ctx - .head() - .headers() - .get(EXTERNAL_NOTIFICATION_KEY_HEADER) - .is_some_and(|it| { - it.as_bytes() == external_notification_key.as_bytes() - }), - } + ctx.head() + .headers() + .get(EXTERNAL_NOTIFICATION_KEY_HEADER) + .is_some_and(|it| { + it.as_bytes() == ENV.LABRINTH_EXTERNAL_NOTIFICATION_KEY.as_bytes() + }) } pub fn internal_network_guard(ctx: &GuardContext) -> bool { diff --git a/apps/labrinth/src/util/img.rs b/apps/labrinth/src/util/img.rs index 1c5339e5e7..8e8af67aad 100644 --- a/apps/labrinth/src/util/img.rs +++ b/apps/labrinth/src/util/img.rs @@ -1,6 +1,7 @@ use crate::database::models::image_item; use crate::database::redis::RedisPool; use crate::database::{self, PgTransaction}; +use crate::env::ENV; use crate::file_hosting::{FileHost, FileHostPublicity}; use crate::models::images::ImageContext; use crate::routes::ApiError; @@ -59,7 +60,7 @@ pub async fn upload_image_optimized( )) })?; - let cdn_url = dotenvy::var("CDN_URL")?; + let cdn_url = &ENV.CDN_URL; let hash = sha1::Sha1::digest(&bytes).encode_hex::(); let (processed_image, processed_image_ext) = process_image( @@ -175,7 +176,7 @@ pub async fn delete_old_images( publicity: FileHostPublicity, file_host: &dyn FileHost, ) -> Result<(), ApiError> { - let cdn_url = dotenvy::var("CDN_URL")?; + let cdn_url = &ENV.CDN_URL; let cdn_url_start = format!("{cdn_url}/"); if let Some(image_url) = image_url { let name = image_url.split(&cdn_url_start).nth(1); diff --git a/apps/labrinth/src/util/mod.rs b/apps/labrinth/src/util/mod.rs index cf44795acf..b712e26789 100644 --- a/apps/labrinth/src/util/mod.rs +++ b/apps/labrinth/src/util/mod.rs @@ -6,7 +6,6 @@ pub mod bitflag; pub mod captcha; pub mod cors; pub mod date; -pub mod env; pub mod error; pub mod ext; pub mod gotenberg; diff --git a/apps/labrinth/src/util/ratelimit.rs b/apps/labrinth/src/util/ratelimit.rs index c64c801fe0..a2534cd205 100644 --- a/apps/labrinth/src/util/ratelimit.rs +++ b/apps/labrinth/src/util/ratelimit.rs @@ -1,6 +1,5 @@ -use crate::database::redis::RedisPool; use crate::routes::ApiError; -use crate::util::env::parse_var; +use crate::{database::redis::RedisPool, env::ENV}; use actix_web::{ Error, ResponseError, body::{EitherBody, MessageBody}, @@ -134,14 +133,13 @@ pub async fn rate_limit_middleware( .clone(); if let Some(key) = req.headers().get("x-ratelimit-key") - && key.to_str().ok() - == dotenvy::var("RATE_LIMIT_IGNORE_KEY").ok().as_deref() + && key.to_str().ok() == Some(&ENV.RATE_LIMIT_IGNORE_KEY) { return Ok(next.call(req).await?.map_into_left_body()); } let conn_info = req.connection_info().clone(); - let ip = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) { + let ip = if ENV.CLOUDFLARE_INTEGRATION { if let Some(header) = req.headers().get("CF-Connecting-IP") { header.to_str().ok() } else { diff --git a/apps/labrinth/src/util/webhook.rs b/apps/labrinth/src/util/webhook.rs index 13d2970a02..e27b9e8e72 100644 --- a/apps/labrinth/src/util/webhook.rs +++ b/apps/labrinth/src/util/webhook.rs @@ -1,8 +1,8 @@ -use crate::database::PgPool; use crate::database::models::legacy_loader_fields::MinecraftGameVersion; use crate::database::redis::RedisPool; use crate::models::ids::ProjectId; use crate::routes::ApiError; +use crate::{database::PgPool, env::ENV}; use ariadne::ids::base62_impl::to_base62; use chrono::{DateTime, Utc}; use serde::Serialize; @@ -69,7 +69,7 @@ async fn get_webhook_metadata( name: organization.name, url: format!( "{}/organization/{}", - dotenvy::var("SITE_URL").unwrap_or_default(), + ENV.SITE_URL, to_base62(organization.id.0 as u64) ), icon_url: organization.icon_url, @@ -95,7 +95,7 @@ async fn get_webhook_metadata( owner = Some(WebhookAuthor { url: format!( "{}/user/{}", - dotenvy::var("SITE_URL").unwrap_or_default(), + ENV.SITE_URL, to_base62(user.id.0 as u64) ), name: user.username, @@ -142,7 +142,7 @@ async fn get_webhook_metadata( Ok(Some(WebhookMetadata { project_url: format!( "{}/{}/{}", - dotenvy::var("SITE_URL").unwrap_or_default(), + ENV.SITE_URL, project_type, to_base62(project.inner.id.0 as u64) ), @@ -251,7 +251,7 @@ pub async fn send_slack_project_webhook( project_id: ProjectId, pool: &PgPool, redis: &RedisPool, - webhook_url: String, + webhook_url: &str, message: Option, ) -> Result<(), ApiError> { let metadata = get_webhook_metadata(project_id, pool, redis).await?; @@ -350,7 +350,7 @@ pub async fn send_slack_project_webhook( let client = reqwest::Client::new(); client - .post(&webhook_url) + .post(webhook_url) .json(&serde_json::json!({ "blocks": blocks, })) @@ -422,7 +422,7 @@ pub async fn send_discord_webhook( project_id: ProjectId, pool: &PgPool, redis: &RedisPool, - webhook_url: String, + webhook_url: &str, message: Option, ) -> Result<(), ApiError> { let metadata = get_webhook_metadata(project_id, pool, redis).await?; @@ -482,7 +482,7 @@ pub async fn send_discord_webhook( let client = reqwest::Client::new(); client - .post(&webhook_url) + .post(webhook_url) .json(&DiscordWebhook { avatar_url: Some( "https://cdn.modrinth.com/Modrinth_Dark_Logo.png"