From 45cd2ed42f482f0010643efa86128b113db60df6 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 18 Feb 2026 03:14:08 +0000 Subject: [PATCH 01/12] wip: better env var reading --- apps/labrinth/src/env.rs | 78 ++++++++++++++++++++++++++++++++++++++++ apps/labrinth/src/lib.rs | 4 +++ 2 files changed, 82 insertions(+) create mode 100644 apps/labrinth/src/env.rs diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs new file mode 100644 index 0000000000..07f4db1b6b --- /dev/null +++ b/apps/labrinth/src/env.rs @@ -0,0 +1,78 @@ +use std::{any::type_name, str::FromStr, sync::LazyLock}; + +use eyre::{Context, eyre}; + +macro_rules! vars { + ( + $( + $field:ident: $ty:ty + ),* $(,)? + ) => { + #[derive(Debug)] + pub struct EnvVars { + $( + #[expect(non_snake_case, reason = "environment variables are UPPER_SNAKE_CASE")] + pub $field: $ty, + )* + } + + pub static ENV_VARS: LazyLock = LazyLock::new(|| { + let mut err = eyre!("failed to read environment variables"); + + $( + let $field: Option<$ty> = match parse_value::<$ty>(stringify!($field)) { + Ok(value) => Some(value), + Err(source) => { + err = err.wrap_err(source); + None + } + }; + )* + + EnvVars { + $( + $field: $field.unwrap_or_else(|| panic!("{err:?}")), + )* + } + }); + }; +} + +fn parse_value(key: &str) -> eyre::Result +where + T: FromStr, + T::Err: std::error::Error + Send + Sync + 'static, +{ + dotenvy::var(key) + .wrap_err_with(|| eyre!("`{key}` missing"))? + .parse::() + .wrap_err_with(|| { + eyre!("`{key}` is not a valid `{}`", type_name::()) + }) +} + +vars! { + 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, +} diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs index dce5404992..2290c89893 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -16,6 +16,7 @@ use util::gotenberg::GotenbergClient; use crate::background_task::update_versions; use crate::database::{PgPool, ReadOnlyPgPool}; +use crate::env::ENV_VARS; use crate::queue::billing::{index_billing, index_subscriptions}; use crate::queue::moderation::AutomatedModerationQueue; use crate::util::anrok; @@ -28,6 +29,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; @@ -352,6 +354,8 @@ pub fn utoipa_app_config( // This is so that env vars not used immediately don't panic at runtime pub fn check_env_vars() -> bool { + _ = *ENV_VARS; + let mut failed = false; fn check_var(var: &str) -> bool { From 6a1da11526814594dc938dd7f6e3e4982cf9882c Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 18 Feb 2026 03:57:46 +0000 Subject: [PATCH 02/12] move most env vars to env.rs --- apps/labrinth/src/env.rs | 130 +++++++++++++-- apps/labrinth/src/lib.rs | 178 ++++++++++----------- apps/labrinth/src/routes/internal/flows.rs | 15 +- 3 files changed, 211 insertions(+), 112 deletions(-) diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs index 07f4db1b6b..92614d4e3a 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -1,6 +1,7 @@ use std::{any::type_name, str::FromStr, sync::LazyLock}; use eyre::{Context, eyre}; +use rust_decimal::Decimal; macro_rules! vars { ( @@ -9,35 +10,51 @@ macro_rules! vars { ),* $(,)? ) => { #[derive(Debug)] + #[allow( + non_snake_case, + reason = "environment variables are UPPER_SNAKE_CASE", + )] pub struct EnvVars { $( - #[expect(non_snake_case, reason = "environment variables are UPPER_SNAKE_CASE")] pub $field: $ty, )* } - pub static ENV_VARS: LazyLock = LazyLock::new(|| { - let mut err = eyre!("failed to read environment variables"); + impl EnvVars { + pub fn from_env() -> eyre::Result { + let mut err = eyre!("failed to read environment variables"); - $( - let $field: Option<$ty> = match parse_value::<$ty>(stringify!($field)) { - Ok(value) => Some(value), - Err(source) => { - err = err.wrap_err(source); - None - } - }; - )* - - EnvVars { $( - $field: $field.unwrap_or_else(|| panic!("{err:?}")), + #[expect( + non_snake_case, + reason = "environment variables are UPPER_SNAKE_CASE", + )] + let $field: Option<$ty> = match parse_value::<$ty>(stringify!($field)) { + Ok(value) => Some(value), + Err(source) => { + err = err.wrap_err(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) -> eyre::Result where T: FromStr, @@ -52,6 +69,24 @@ where } vars! { + SENTRY_ENVIRONMENT: String, + SENTRY_TRACES_SAMPLE_RATE: String, + SITE_URL: String, + CDN_URL: String, + LABRINTH_ADMIN_KEY: String, + LABRINTH_EXTERNAL_NOTIFICATION_KEY: String, + RATE_LIMIT_IGNORE_KEY: String, + DATABASE_URL: String, + MEILISEARCH_READ_ADDR: String, + MEILISEARCH_WRITE_ADDRS: String, + MEILISEARCH_KEY: String, + REDIS_URL: String, + BIND_ADDR: String, + SELF_ADDR: String, + + LOCAL_INDEX_INTERVAL: usize, + VERSION_INDEX_INTERVAL: usize, + GITHUB_CLIENT_ID: String, GITHUB_CLIENT_SECRET: String, GITLAB_CLIENT_ID: String, @@ -75,4 +110,67 @@ vars! { PAYPAL_NVP_USERNAME: String, PAYPAL_NVP_PASSWORD: String, PAYPAL_NVP_SIGNATURE: String, + + 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, + + ARCHON_URL: String, + + MURALPAY_API_URL: String, + MURALPAY_API_KEY: String, + MURALPAY_TRANSFER_API_KEY: String, + MURALPAY_SOURCE_ACCOUNT_ID: String, + + DEFAULT_AFFILIATE_REVENUE_SPLIT: Decimal, } diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs index 2290c89893..a5a6a50f87 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -16,7 +16,7 @@ use util::gotenberg::GotenbergClient; use crate::background_task::update_versions; use crate::database::{PgPool, ReadOnlyPgPool}; -use crate::env::ENV_VARS; +use crate::env::ENV; use crate::queue::billing::{index_billing, index_subscriptions}; use crate::queue::moderation::AutomatedModerationQueue; use crate::util::anrok; @@ -354,7 +354,7 @@ pub fn utoipa_app_config( // This is so that env vars not used immediately don't panic at runtime pub fn check_env_vars() -> bool { - _ = *ENV_VARS; + _ = *ENV; let mut failed = false; @@ -370,20 +370,20 @@ pub fn check_env_vars() -> bool { 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::("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"); @@ -425,8 +425,8 @@ pub fn check_env_vars() -> bool { } } - failed |= check_var::("LOCAL_INDEX_INTERVAL"); - failed |= check_var::("VERSION_INDEX_INTERVAL"); + // failed |= check_var::("LOCAL_INDEX_INTERVAL"); + // failed |= check_var::("VERSION_INDEX_INTERVAL"); if parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS").is_none() { warn!( @@ -442,47 +442,47 @@ pub fn check_env_vars() -> bool { 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"); + // 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!( @@ -491,50 +491,50 @@ pub fn check_env_vars() -> bool { 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::("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::("FLAME_ANVIL_URL"); - failed |= check_var::("GOTENBERG_URL"); - failed |= check_var::("GOTENBERG_CALLBACK_BASE"); - failed |= check_var::("GOTENBERG_TIMEOUT"); + // 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::("STRIPE_API_KEY"); + // failed |= check_var::("STRIPE_WEBHOOK_SECRET"); - failed |= check_var::("ADITUDE_API_KEY"); + // failed |= check_var::("ADITUDE_API_KEY"); - failed |= check_var::("PYRO_API_KEY"); + // failed |= check_var::("PYRO_API_KEY"); - failed |= check_var::("BREX_API_URL"); - failed |= check_var::("BREX_API_KEY"); + // failed |= check_var::("BREX_API_URL"); + // failed |= check_var::("BREX_API_KEY"); - failed |= check_var::("DELPHI_URL"); + // 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::("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::("ANROK_API_URL"); + // failed |= check_var::("ANROK_API_KEY"); - failed |= check_var::("COMPLIANCE_PAYOUT_THRESHOLD"); + // failed |= check_var::("COMPLIANCE_PAYOUT_THRESHOLD"); - failed |= check_var::("PAYOUT_ALERT_SLACK_WEBHOOK"); + // failed |= check_var::("PAYOUT_ALERT_SLACK_WEBHOOK"); - failed |= check_var::("ARCHON_URL"); + // 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::("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 |= check_var::("DEFAULT_AFFILIATE_REVENUE_SPLIT"); failed } diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index 84510685bf..4762ed29cd 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; @@ -263,35 +264,35 @@ impl AuthProvider { 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 +318,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" From d2873b73ff599dcafc082fa974fcc5a2b1f28c2e Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 18 Feb 2026 05:35:08 +0000 Subject: [PATCH 03/12] migrate more env vars --- apps/labrinth/src/auth/validate.rs | 3 +- .../src/database/postgres_database.rs | 8 +- apps/labrinth/src/database/redis/mod.rs | 6 +- apps/labrinth/src/env.rs | 87 ++++++++--- apps/labrinth/src/lib.rs | 136 +----------------- apps/labrinth/src/main.rs | 15 +- apps/labrinth/src/models/v3/pack.rs | 8 +- apps/labrinth/src/queue/email/templates.rs | 10 +- apps/labrinth/src/queue/moderation.rs | 3 +- apps/labrinth/src/queue/payouts/mod.rs | 17 +-- apps/labrinth/src/routes/analytics.rs | 11 +- .../labrinth/src/routes/internal/affiliate.rs | 15 +- apps/labrinth/src/routes/internal/flows.rs | 13 +- apps/labrinth/src/routes/mod.rs | 7 +- apps/labrinth/src/routes/updates.rs | 7 +- apps/labrinth/src/routes/v3/projects.rs | 3 +- apps/labrinth/src/search/mod.rs | 22 +-- apps/labrinth/src/test/database.rs | 10 +- apps/labrinth/src/util/env.rs | 14 +- apps/labrinth/src/util/gotenberg.rs | 22 ++- apps/labrinth/src/util/guards.rs | 40 ++---- apps/labrinth/src/util/ratelimit.rs | 5 +- apps/labrinth/src/util/webhook.rs | 8 +- 23 files changed, 164 insertions(+), 306 deletions(-) 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/database/postgres_database.rs b/apps/labrinth/src/database/postgres_database.rs index a0f4d45739..7bd1e6089a 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,8 +52,7 @@ 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") @@ -112,8 +113,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..f6d399d329 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}; @@ -54,7 +56,7 @@ impl RedisPool { }, ); - let url = dotenvy::var("REDIS_URL").expect("Redis URL not set"); + let url = &ENV.REDIS_URL; let pool = Config::from_url(url.clone()) .builder() .expect("Error building Redis pool") @@ -70,7 +72,7 @@ impl RedisPool { .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(), diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs index 92614d4e3a..760013206d 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -1,12 +1,14 @@ -use std::{any::type_name, str::FromStr, sync::LazyLock}; +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 + $field:ident: $ty:ty $(= $default:expr)? ),* $(,)? ) => { #[derive(Debug)] @@ -29,11 +31,21 @@ macro_rules! vars { non_snake_case, reason = "environment variables are UPPER_SNAKE_CASE", )] - let $field: Option<$ty> = match parse_value::<$ty>(stringify!($field)) { - Ok(value) => Some(value), - Err(source) => { - err = err.wrap_err(source); - None + #[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(source); + None + } } }; )* @@ -55,37 +67,74 @@ pub static ENV: LazyLock = LazyLock::new(|| { EnvVars::from_env().unwrap_or_else(|err| panic!("{err:?}")) }); -fn parse_value(key: &str) -> eyre::Result +fn parse_value(key: &str, default: Option) -> eyre::Result where T: FromStr, T::Err: std::error::Error + Send + Sync + 'static, { - dotenvy::var(key) - .wrap_err_with(|| eyre!("`{key}` missing"))? - .parse::() - .wrap_err_with(|| { + 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")), + } +} + +#[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: String, + SENTRY_TRACES_SAMPLE_RATE: f32, SITE_URL: String, CDN_URL: String, LABRINTH_ADMIN_KEY: String, - LABRINTH_EXTERNAL_NOTIFICATION_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: String, + MEILISEARCH_WRITE_ADDRS: StringCsv, MEILISEARCH_KEY: String, REDIS_URL: String, BIND_ADDR: String, SELF_ADDR: String, - LOCAL_INDEX_INTERVAL: usize, - VERSION_INDEX_INTERVAL: usize, + LOCAL_INDEX_INTERVAL: u64, + VERSION_INDEX_INTERVAL: u64, + + WHITELISTED_MODPACK_DOMAINS: Json>, + ALLOWED_CALLBACK_URLS: Json>, + ANALYTICS_ALLOWED_ORIGINS: Json>, GITHUB_CLIENT_ID: String, GITHUB_CLIENT_SECRET: String, @@ -170,7 +219,7 @@ vars! { MURALPAY_API_URL: String, MURALPAY_API_KEY: String, MURALPAY_TRANSFER_API_KEY: String, - MURALPAY_SOURCE_ACCOUNT_ID: String, + MURALPAY_SOURCE_ACCOUNT_ID: muralpay::AccountId, DEFAULT_AFFILIATE_REVENUE_SPLIT: Decimal, } diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs index a5a6a50f87..6378c6f9e7 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -21,7 +21,7 @@ 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::env::parse_var; use crate::util::ratelimit::{AsyncRateLimiter, GCRAParameters}; use sync::friends::handle_pubsub; @@ -85,10 +85,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()); @@ -114,9 +111,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(); @@ -144,9 +140,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 || { @@ -370,21 +365,6 @@ pub fn check_env_vars() -> bool { 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(); @@ -425,65 +405,6 @@ pub fn check_env_vars() -> bool { } } - // 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" @@ -491,50 +412,5 @@ pub fn check_env_vars() -> bool { 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..63769acb88 100644 --- a/apps/labrinth/src/main.rs +++ b/apps/labrinth/src/main.rs @@ -7,6 +7,7 @@ use clap::Parser; use labrinth::app_config; use labrinth::background_task::BackgroundTask; use labrinth::database::redis::RedisPool; +use labrinth::env::ENV; use labrinth::file_hosting::{S3BucketConfig, S3Host}; use labrinth::queue::email::EmailQueue; use labrinth::search; @@ -70,11 +71,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 +97,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() @@ -272,7 +267,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/email/templates.rs b/apps/labrinth/src/queue/email/templates.rs index 3815db40d3..4f0399990f 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; @@ -558,7 +559,7 @@ async fn collect_template_variables( NotificationBody::ResetPassword { flow } => { let url = format!( "{}/{}?flow={}", - dotenvy::var("SITE_URL")?, + ENV.SITE_URL, dotenvy::var("SITE_RESET_PASSWORD_PATH")?, flow ); @@ -571,7 +572,7 @@ async fn collect_template_variables( NotificationBody::VerifyEmail { flow } => { let url = format!( "{}/{}?flow={}", - dotenvy::var("SITE_URL")?, + ENV.SITE_URL, dotenvy::var("SITE_VERIFY_EMAIL_PATH")?, flow ); @@ -605,7 +606,7 @@ async fn collect_template_variables( NotificationBody::PaymentFailed { amount, service } => { let url = format!( "{}/{}", - dotenvy::var("SITE_URL")?, + ENV.SITE_URL, dotenvy::var("SITE_BILLING_PATH")?, ); @@ -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..5f04154ae5 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}; @@ -673,7 +674,7 @@ impl AutomatedModerationQueue { 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/mod.rs b/apps/labrinth/src/queue/payouts/mod.rs index cd6c38b8a6..8785a183f0 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, }) } 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/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index 4762ed29cd..28d3977d51 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -18,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; @@ -258,7 +257,7 @@ 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); @@ -341,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 { @@ -1101,10 +1099,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); 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/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index 60590e8f31..d041ea61f2 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}; @@ -460,7 +461,7 @@ pub async fn project_edit( 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/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/database.rs b/apps/labrinth/src/test/database.rs index 02b6f556f9..5e8181f2d2 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,8 +58,7 @@ 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; @@ -139,8 +139,7 @@ impl TemporaryDatabase { } // Switch to template - let url = - dotenvy::var("DATABASE_URL").expect("No database URL"); + let url = &ENV.DATABASE_URL; let mut template_url = Url::parse(&url).expect("Invalid database URL"); template_url.set_path(&format!("/{TEMPLATE_DATABASE_NAME}")); @@ -234,8 +233,7 @@ 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) diff --git a/apps/labrinth/src/util/env.rs b/apps/labrinth/src/util/env.rs index 0bcb6bde6d..38f26d8f48 100644 --- a/apps/labrinth/src/util/env.rs +++ b/apps/labrinth/src/util/env.rs @@ -1,17 +1,5 @@ -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}`")) -} +use std::{convert::Infallible, str::FromStr}; 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/ratelimit.rs b/apps/labrinth/src/util/ratelimit.rs index c64c801fe0..6d9e1aef57 100644 --- a/apps/labrinth/src/util/ratelimit.rs +++ b/apps/labrinth/src/util/ratelimit.rs @@ -1,6 +1,6 @@ -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,8 +134,7 @@ 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()); } diff --git a/apps/labrinth/src/util/webhook.rs b/apps/labrinth/src/util/webhook.rs index 13d2970a02..6ec804b97e 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) ), From cc6c87acb008b25a32df3b99c9183c147b2f3648 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 18 Feb 2026 05:44:16 +0000 Subject: [PATCH 04/12] more migration --- .../src/database/postgres_database.rs | 2 +- apps/labrinth/src/env.rs | 3 ++ apps/labrinth/src/file_hosting/mod.rs | 24 ++++++++++++++ apps/labrinth/src/lib.rs | 7 ---- apps/labrinth/src/main.rs | 32 ++++++++++--------- apps/labrinth/src/routes/internal/session.rs | 4 +-- apps/labrinth/src/util/captcha.rs | 4 +-- apps/labrinth/src/util/env.rs | 2 +- apps/labrinth/src/util/ratelimit.rs | 3 +- 9 files changed, 51 insertions(+), 30 deletions(-) diff --git a/apps/labrinth/src/database/postgres_database.rs b/apps/labrinth/src/database/postgres_database.rs index 7bd1e6089a..1366fc1e7a 100644 --- a/apps/labrinth/src/database/postgres_database.rs +++ b/apps/labrinth/src/database/postgres_database.rs @@ -81,7 +81,7 @@ pub async fn connect_all() -> Result<(PgPool, ReadOnlyPgPool), sqlx::Error> { .unwrap_or(16), ) .max_lifetime(Some(Duration::from_secs(60 * 60))) - .connect(&database_url) + .connect(database_url) .await?; let pool = PgPool::from(pool); diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs index 760013206d..b71327e5a2 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -136,6 +136,8 @@ vars! { ALLOWED_CALLBACK_URLS: Json>, ANALYTICS_ALLOWED_ORIGINS: Json>, + STORAGE_BACKEND: crate::file_hosting::FileHostKind, + GITHUB_CLIENT_ID: String, GITHUB_CLIENT_SECRET: String, GITLAB_CLIENT_ID: String, @@ -213,6 +215,7 @@ vars! { COMPLIANCE_PAYOUT_THRESHOLD: String, PAYOUT_ALERT_SLACK_WEBHOOK: String, + CLOUDFLARE_INTEGRATION: bool = false, ARCHON_URL: String, 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 6378c6f9e7..3e736d3159 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -405,12 +405,5 @@ pub fn check_env_vars() -> bool { } } - 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 } diff --git a/apps/labrinth/src/main.rs b/apps/labrinth/src/main.rs index 63769acb88..07c62b1329 100644 --- a/apps/labrinth/src/main.rs +++ b/apps/labrinth/src/main.rs @@ -8,7 +8,7 @@ use labrinth::app_config; use labrinth::background_task::BackgroundTask; use labrinth::database::redis::RedisPool; use labrinth::env::ENV; -use labrinth::file_hosting::{S3BucketConfig, S3Host}; +use labrinth::file_hosting::{FileHostKind, S3BucketConfig, S3Host}; use labrinth::queue::email::EmailQueue; use labrinth::search; use labrinth::util::anrok; @@ -114,27 +114,30 @@ 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" => { + match storage_backend { + FileHostKind::S3 => { let config_from_env = |bucket_type| S3BucketConfig { - name: parse_var(&format!("S3_{bucket_type}_BUCKET_NAME")) - .unwrap(), - uses_path_style: parse_var(&format!( + name: dotenvy::var(&format!( + "S3_{bucket_type}_BUCKET_NAME" + )) + .unwrap(), + uses_path_style: dotenvy::var(&format!( "S3_{bucket_type}_USES_PATH_STYLE_BUCKET" )) + .unwrap() + .parse::() .unwrap(), - region: parse_var(&format!("S3_{bucket_type}_REGION")) + region: dotenvy::var(&format!("S3_{bucket_type}_REGION")) + .unwrap(), + url: dotenvy::var(&format!("S3_{bucket_type}_URL")) .unwrap(), - url: parse_var(&format!("S3_{bucket_type}_URL")).unwrap(), - access_token: parse_var(&format!( + access_token: dotenvy::var(&format!( "S3_{bucket_type}_ACCESS_TOKEN" )) .unwrap(), - secret: parse_var(&format!("S3_{bucket_type}_SECRET")) + secret: dotenvy::var(&format!("S3_{bucket_type}_SECRET")) .unwrap(), }; @@ -146,8 +149,7 @@ async fn app() -> std::io::Result<()> { .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"); 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/util/captcha.rs b/apps/labrinth/src/util/captcha.rs index e59d5ae5d7..f80d8d770d 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; @@ -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 { diff --git a/apps/labrinth/src/util/env.rs b/apps/labrinth/src/util/env.rs index 38f26d8f48..b3fae3c4c0 100644 --- a/apps/labrinth/src/util/env.rs +++ b/apps/labrinth/src/util/env.rs @@ -1,4 +1,4 @@ -use std::{convert::Infallible, str::FromStr}; +use std::str::FromStr; pub fn parse_var(var: &str) -> Option { dotenvy::var(var).ok().and_then(|i| i.parse().ok()) diff --git a/apps/labrinth/src/util/ratelimit.rs b/apps/labrinth/src/util/ratelimit.rs index 6d9e1aef57..a2534cd205 100644 --- a/apps/labrinth/src/util/ratelimit.rs +++ b/apps/labrinth/src/util/ratelimit.rs @@ -1,5 +1,4 @@ use crate::routes::ApiError; -use crate::util::env::parse_var; use crate::{database::redis::RedisPool, env::ENV}; use actix_web::{ Error, ResponseError, @@ -140,7 +139,7 @@ pub async fn rate_limit_middleware( } 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 { From 41e8d03e39d327fefb0fc68acdd9b8049af15d4e Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 18 Feb 2026 13:36:41 +0000 Subject: [PATCH 05/12] more migrations --- .../src/database/postgres_database.rs | 43 +++---------- apps/labrinth/src/database/redis/mod.rs | 24 +------- apps/labrinth/src/env.rs | 22 +++++++ apps/labrinth/src/lib.rs | 24 +++----- apps/labrinth/src/main.rs | 21 +++---- apps/labrinth/src/queue/email.rs | 19 +++--- apps/labrinth/src/queue/email/templates.rs | 16 +++-- apps/labrinth/src/queue/payouts/affiliate.rs | 7 +-- .../src/queue/payouts/flow/tremendous.rs | 3 +- apps/labrinth/src/routes/internal/delphi.rs | 7 ++- apps/labrinth/src/routes/internal/flows.rs | 60 +++++++++---------- apps/labrinth/src/routes/internal/globals.rs | 4 +- apps/labrinth/src/search/indexing/mod.rs | 8 +-- apps/labrinth/src/test/database.rs | 9 ++- apps/labrinth/src/util/anrok.rs | 14 ++--- apps/labrinth/src/util/archon.rs | 6 +- apps/labrinth/src/util/avalara1099.rs | 13 ++-- apps/labrinth/src/util/captcha.rs | 4 +- apps/labrinth/src/util/img.rs | 5 +- 19 files changed, 137 insertions(+), 172 deletions(-) diff --git a/apps/labrinth/src/database/postgres_database.rs b/apps/labrinth/src/database/postgres_database.rs index 1366fc1e7a..726621848d 100644 --- a/apps/labrinth/src/database/postgres_database.rs +++ b/apps/labrinth/src/database/postgres_database.rs @@ -55,53 +55,24 @@ pub async fn connect_all() -> Result<(PgPool, ReadOnlyPgPool), sqlx::Error> { 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) .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); diff --git a/apps/labrinth/src/database/redis/mod.rs b/apps/labrinth/src/database/redis/mod.rs index f6d399d329..1b8ac30c01 100644 --- a/apps/labrinth/src/database/redis/mod.rs +++ b/apps/labrinth/src/database/redis/mod.rs @@ -44,28 +44,13 @@ 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 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() @@ -78,10 +63,7 @@ impl RedisPool { 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 index b71327e5a2..0b05c410c3 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -225,4 +225,26 @@ vars! { MURALPAY_SOURCE_ACCOUNT_ID: muralpay::AccountId, 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/lib.rs b/apps/labrinth/src/lib.rs index 3e736d3159..13f5427960 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -17,11 +17,11 @@ use util::gotenberg::GotenbergClient; use crate::background_task::update_versions; use crate::database::{PgPool, ReadOnlyPgPool}; use crate::env::ENV; +use crate::file_hosting::FileHostKind; 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_var; use crate::util::ratelimit::{AsyncRateLimiter, GCRAParameters}; use sync::friends::handle_pubsub; @@ -354,7 +354,10 @@ pub fn check_env_vars() -> bool { let mut failed = false; fn check_var(var: &str) -> bool { - let check = parse_var::(var).is_none(); + let check = dotenvy::var(var) + .ok() + .and_then(|v| v.parse::().ok()) + .is_none(); if check { warn!( "Variable `{}` missing in dotenv or not of type `{}`", @@ -367,9 +370,8 @@ pub fn check_env_vars() -> bool { failed |= check_var::("STORAGE_BACKEND"); - let storage_backend = dotenvy::var("STORAGE_BACKEND").ok(); - match storage_backend.as_deref() { - Some("s3") => { + match ENV.STORAGE_BACKEND { + FileHostKind::S3 => { let mut check_var_set = |var_prefix| { failed |= check_var::(&format!( "S3_{var_prefix}_BUCKET_NAME" @@ -390,19 +392,9 @@ pub fn check_env_vars() -> bool { check_var_set("PUBLIC"); check_var_set("PRIVATE"); } - Some("local") => { + FileHostKind::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 diff --git a/apps/labrinth/src/main.rs b/apps/labrinth/src/main.rs index 07c62b1329..4213ad528a 100644 --- a/apps/labrinth/src/main.rs +++ b/apps/labrinth/src/main.rs @@ -12,7 +12,6 @@ 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; @@ -119,25 +118,22 @@ async fn app() -> std::io::Result<()> { match storage_backend { FileHostKind::S3 => { let config_from_env = |bucket_type| S3BucketConfig { - name: dotenvy::var(&format!( - "S3_{bucket_type}_BUCKET_NAME" - )) - .unwrap(), - uses_path_style: dotenvy::var(&format!( + name: dotenvy::var(format!("S3_{bucket_type}_BUCKET_NAME")) + .unwrap(), + uses_path_style: dotenvy::var(format!( "S3_{bucket_type}_USES_PATH_STYLE_BUCKET" )) .unwrap() .parse::() .unwrap(), - region: dotenvy::var(&format!("S3_{bucket_type}_REGION")) - .unwrap(), - url: dotenvy::var(&format!("S3_{bucket_type}_URL")) + region: dotenvy::var(format!("S3_{bucket_type}_REGION")) .unwrap(), - access_token: dotenvy::var(&format!( + url: dotenvy::var(format!("S3_{bucket_type}_URL")).unwrap(), + access_token: dotenvy::var(format!( "S3_{bucket_type}_ACCESS_TOKEN" )) .unwrap(), - secret: dotenvy::var(&format!("S3_{bucket_type}_SECRET")) + secret: dotenvy::var(format!("S3_{bucket_type}_SECRET")) .unwrap(), }; @@ -157,8 +153,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 = 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 4f0399990f..4f64cf9d6f 100644 --- a/apps/labrinth/src/queue/email/templates.rs +++ b/apps/labrinth/src/queue/email/templates.rs @@ -97,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()) + }, }) } } 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/routes/internal/delphi.rs b/apps/labrinth/src/routes/internal/delphi.rs index 8103a91945..68a3ca0445 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); @@ -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 28d3977d51..cd89d786bd 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -352,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}" @@ -373,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); @@ -398,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); @@ -423,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); @@ -448,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); @@ -528,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()); @@ -579,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); } } @@ -731,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 { @@ -796,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!( @@ -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/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/test/database.rs b/apps/labrinth/src/test/database.rs index 5e8181f2d2..4a63361154 100644 --- a/apps/labrinth/src/test/database.rs +++ b/apps/labrinth/src/test/database.rs @@ -61,11 +61,11 @@ impl TemporaryDatabase { 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,9 +139,8 @@ impl TemporaryDatabase { } // Switch to template - let url = &ENV.DATABASE_URL; let mut template_url = - Url::parse(&url).expect("Invalid database 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()) @@ -236,7 +235,7 @@ impl TemporaryDatabase { 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/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 f80d8d770d..af32e51927 100644 --- a/apps/labrinth/src/util/captcha.rs +++ b/apps/labrinth/src/util/captcha.rs @@ -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"); @@ -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/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); From 6043968a62ad61790c08273f3d84e819bea38bc0 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 18 Feb 2026 14:30:29 +0000 Subject: [PATCH 06/12] More migration --- apps/labrinth/src/clickhouse/mod.rs | 14 +++---- apps/labrinth/src/env.rs | 4 +- apps/labrinth/src/file_hosting/mock.rs | 7 ++-- apps/labrinth/src/queue/billing.rs | 5 ++- apps/labrinth/src/queue/email/templates.rs | 14 ++----- apps/labrinth/src/queue/moderation.rs | 16 +++---- apps/labrinth/src/queue/payouts/mod.rs | 42 ++++++++----------- apps/labrinth/src/routes/internal/billing.rs | 23 +++++----- apps/labrinth/src/routes/internal/delphi.rs | 2 +- apps/labrinth/src/routes/v3/payouts.rs | 9 ++-- apps/labrinth/src/routes/v3/projects.rs | 10 ++--- apps/labrinth/src/routes/v3/threads.rs | 4 +- .../src/routes/v3/version_creation.rs | 3 +- apps/labrinth/src/test/api_v2/mod.rs | 6 +-- apps/labrinth/src/test/api_v3/mod.rs | 6 +-- apps/labrinth/src/test/database.rs | 4 +- apps/labrinth/src/test/mod.rs | 4 +- apps/labrinth/src/util/env.rs | 5 --- apps/labrinth/src/util/mod.rs | 1 - apps/labrinth/src/util/webhook.rs | 8 ++-- 20 files changed, 83 insertions(+), 104 deletions(-) delete mode 100644 apps/labrinth/src/util/env.rs 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/env.rs b/apps/labrinth/src/env.rs index 0b05c410c3..107a270e3d 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -117,7 +117,7 @@ vars! { SENTRY_TRACES_SAMPLE_RATE: f32, SITE_URL: String, CDN_URL: String, - LABRINTH_ADMIN_KEY: String, + LABRINTH_ADMIN_KEY: String = "", LABRINTH_MEDAL_KEY: String = "", LABRINTH_EXTERNAL_NOTIFICATION_KEY: String = "", RATE_LIMIT_IGNORE_KEY: String, @@ -247,4 +247,6 @@ vars! { DELPHI_SLACK_WEBHOOK: String = "", TREMENDOUS_CAMPAIGN_ID: String = "", + + MOCK_FILE_PATH: 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/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/templates.rs b/apps/labrinth/src/queue/email/templates.rs index 4f64cf9d6f..e5cecdb3cc 100644 --- a/apps/labrinth/src/queue/email/templates.rs +++ b/apps/labrinth/src/queue/email/templates.rs @@ -567,9 +567,7 @@ async fn collect_template_variables( NotificationBody::ResetPassword { flow } => { let url = format!( "{}/{}?flow={}", - ENV.SITE_URL, - dotenvy::var("SITE_RESET_PASSWORD_PATH")?, - flow + ENV.SITE_URL, ENV.SITE_RESET_PASSWORD_PATH, flow ); map.insert(RESETPASSWORD_URL, url); @@ -580,9 +578,7 @@ async fn collect_template_variables( NotificationBody::VerifyEmail { flow } => { let url = format!( "{}/{}?flow={}", - ENV.SITE_URL, - dotenvy::var("SITE_VERIFY_EMAIL_PATH")?, - flow + ENV.SITE_URL, ENV.SITE_VERIFY_EMAIL_PATH, flow ); map.insert(VERIFYEMAIL_URL, url); @@ -612,11 +608,7 @@ async fn collect_template_variables( } NotificationBody::PaymentFailed { amount, service } => { - let url = format!( - "{}/{}", - ENV.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()); diff --git a/apps/labrinth/src/queue/moderation.rs b/apps/labrinth/src/queue/moderation.rs index 5f04154ae5..7c8340d9db 100644 --- a/apps/labrinth/src/queue/moderation.rs +++ b/apps/labrinth/src/queue/moderation.rs @@ -455,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::>() })) @@ -554,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::>() })) @@ -665,12 +665,12 @@ 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*", diff --git a/apps/labrinth/src/queue/payouts/mod.rs b/apps/labrinth/src/queue/payouts/mod.rs index 8785a183f0..f9f3179430 100644 --- a/apps/labrinth/src/queue/payouts/mod.rs +++ b/apps/labrinth/src/queue/payouts/mod.rs @@ -182,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) @@ -203,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) @@ -271,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( @@ -352,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 { @@ -508,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::() @@ -535,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"; @@ -867,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, @@ -1361,7 +1355,7 @@ async fn check_balance_with_webhook( .ok() .and_then(|x| x.parse::().ok()) .filter(|x| *x != 0); - let payout_alert_webhook = dotenvy::var("PAYOUT_ALERT_SLACK_WEBHOOK")?; + let payout_alert_webhook = &ENV.PAYOUT_ALERT_SLACK_WEBHOOK; match &result { Ok(Some(account_balance)) => { @@ -1376,7 +1370,7 @@ async fn check_balance_with_webhook( threshold, current_balance: available, }, - &payout_alert_webhook, + payout_alert_webhook, ) .await?; } @@ -1391,7 +1385,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/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 68a3ca0445..0f0815567d 100644 --- a/apps/labrinth/src/routes/internal/delphi.rs +++ b/apps/labrinth/src/routes/internal/delphi.rs @@ -116,7 +116,7 @@ impl DelphiReport { self.project_id, pool, redis, - webhook_url, + &webhook_url, Some(message_header), ) .await 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 d041ea61f2..b497fd157d 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -426,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 @@ -450,14 +450,12 @@ 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 *{}*", 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/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 4a63361154..3c97a7a2f1 100644 --- a/apps/labrinth/src/test/database.rs +++ b/apps/labrinth/src/test/database.rs @@ -139,8 +139,8 @@ impl TemporaryDatabase { } // Switch to template - let mut template_url = - Url::parse(&ENV.DATABASE_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()) diff --git a/apps/labrinth/src/test/mod.rs b/apps/labrinth/src/test/mod.rs index dd49b297b5..ba3db486aa 100644 --- a/apps/labrinth/src/test/mod.rs +++ b/apps/labrinth/src/test/mod.rs @@ -1,3 +1,4 @@ +use crate::env::ENV; use crate::queue::email::EmailQueue; use crate::util::anrok; use crate::util::gotenberg::GotenbergClient; @@ -39,8 +40,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/env.rs b/apps/labrinth/src/util/env.rs deleted file mode 100644 index b3fae3c4c0..0000000000 --- a/apps/labrinth/src/util/env.rs +++ /dev/null @@ -1,5 +0,0 @@ -use std::str::FromStr; - -pub fn parse_var(var: &str) -> Option { - dotenvy::var(var).ok().and_then(|i| i.parse().ok()) -} 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/webhook.rs b/apps/labrinth/src/util/webhook.rs index 6ec804b97e..e27b9e8e72 100644 --- a/apps/labrinth/src/util/webhook.rs +++ b/apps/labrinth/src/util/webhook.rs @@ -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" From faae6c4e0d42ed63046a5073f5518b228306b3a6 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 18 Feb 2026 14:36:08 +0000 Subject: [PATCH 07/12] =?UTF-8?q?=F0=9F=A6=80=20dotenvy=20is=20gone=20(alm?= =?UTF-8?q?ost)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/labrinth/src/env.rs | 5 +++++ apps/labrinth/src/queue/payouts/mod.rs | 15 ++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs index 107a270e3d..74c1103c0f 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -162,6 +162,11 @@ vars! { 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, diff --git a/apps/labrinth/src/queue/payouts/mod.rs b/apps/labrinth/src/queue/payouts/mod.rs index f9f3179430..ffd2ee94a6 100644 --- a/apps/labrinth/src/queue/payouts/mod.rs +++ b/apps/labrinth/src/queue/payouts/mod.rs @@ -1317,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?; @@ -1348,13 +1348,10 @@ 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 maybe_threshold = if threshold > 0 { Some(threshold) } else { None }; let payout_alert_webhook = &ENV.PAYOUT_ALERT_SLACK_WEBHOOK; match &result { From 5109b84f2c5a5eae4051ad25fb26cee42a0c41bb Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 18 Feb 2026 14:53:33 +0000 Subject: [PATCH 08/12] =?UTF-8?q?=F0=9F=A6=80=20dotenvy=20is=20gone=20?= =?UTF-8?q?=F0=9F=A6=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/labrinth/src/env.rs | 297 ++++++++++++++++++---------------- apps/labrinth/src/lib.rs | 54 ------- apps/labrinth/src/main.rs | 57 +++---- apps/labrinth/src/test/mod.rs | 8 +- 4 files changed, 191 insertions(+), 225 deletions(-) diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs index 74c1103c0f..5084a9c489 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -8,8 +8,8 @@ use serde::de::DeserializeOwned; macro_rules! vars { ( $( - $field:ident: $ty:ty $(= $default:expr)? - ),* $(,)? + $field:ident: $ty:ty $(= $default:expr)?; + )* ) => { #[derive(Debug)] #[allow( @@ -81,6 +81,12 @@ where } } +pub fn init() -> eyre::Result<()> { + EnvVars::from_env()?; + LazyLock::force(&ENV); + Ok(()) +} + #[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Deref, DerefMut, )] @@ -113,145 +119,162 @@ impl FromStr for StringCsv { } 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_BACKEND: crate::file_hosting::FileHostKind, - - 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, - - 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, + 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; + + DEFAULT_AFFILIATE_REVENUE_SPLIT: Decimal; - REDIS_WAIT_TIMEOUT_MS: u64 = 15000u64, - REDIS_MAX_CONNECTIONS: u32 = 10000u32, - REDIS_MIN_CONNECTIONS: usize = 0usize, + 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; - SEARCH_OPERATION_TIMEOUT: u64 = 300000u64, + REDIS_WAIT_TIMEOUT_MS: u64 = 15000u64; + REDIS_MAX_CONNECTIONS: u32 = 10000u32; + REDIS_MIN_CONNECTIONS: usize = 0usize; - SMTP_REPLY_TO_NAME: String = "", - SMTP_REPLY_TO_ADDRESS: String = "", + SEARCH_OPERATION_TIMEOUT: u64 = 300000u64; - PUBLIC_DISCORD_WEBHOOK: String = "", - MODERATION_SLACK_WEBHOOK: String = "", - DELPHI_SLACK_WEBHOOK: String = "", + SMTP_REPLY_TO_NAME: String = ""; + SMTP_REPLY_TO_ADDRESS: String = ""; - TREMENDOUS_CAMPAIGN_ID: String = "", + PUBLIC_DISCORD_WEBHOOK: String = ""; + MODERATION_SLACK_WEBHOOK: String = ""; + DELPHI_SLACK_WEBHOOK: String = ""; - MOCK_FILE_PATH: String = "", + TREMENDOUS_CAMPAIGN_ID: String = ""; } diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs index 13f5427960..4b6f484a95 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -17,7 +17,6 @@ use util::gotenberg::GotenbergClient; use crate::background_task::update_versions; use crate::database::{PgPool, ReadOnlyPgPool}; use crate::env::ENV; -use crate::file_hosting::FileHostKind; use crate::queue::billing::{index_billing, index_subscriptions}; use crate::queue::moderation::AutomatedModerationQueue; use crate::util::anrok; @@ -346,56 +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 { - _ = *ENV; - - let mut failed = false; - - fn check_var(var: &str) -> bool { - let check = dotenvy::var(var) - .ok() - .and_then(|v| v.parse::().ok()) - .is_none(); - if check { - warn!( - "Variable `{}` missing in dotenv or not of type `{}`", - var, - std::any::type_name::() - ); - } - check - } - - failed |= check_var::("STORAGE_BACKEND"); - - match ENV.STORAGE_BACKEND { - FileHostKind::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"); - } - FileHostKind::Local => { - failed |= check_var::("MOCK_FILE_PATH"); - } - } - - failed -} diff --git a/apps/labrinth/src/main.rs b/apps/labrinth/src/main.rs index 4213ad528a..d18984391c 100644 --- a/apps/labrinth/src/main.rs +++ b/apps/labrinth/src/main.rs @@ -4,7 +4,6 @@ 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::env::ENV; @@ -15,10 +14,11 @@ use labrinth::util::anrok; 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 // @@ -117,30 +113,35 @@ async fn app() -> std::io::Result<()> { let file_host: Arc = match storage_backend { FileHostKind::S3 => { - let config_from_env = |bucket_type| S3BucketConfig { - name: dotenvy::var(format!("S3_{bucket_type}_BUCKET_NAME")) - .unwrap(), - uses_path_style: dotenvy::var(format!( - "S3_{bucket_type}_USES_PATH_STYLE_BUCKET" - )) - .unwrap() - .parse::() - .unwrap(), - region: dotenvy::var(format!("S3_{bucket_type}_REGION")) - .unwrap(), - url: dotenvy::var(format!("S3_{bucket_type}_URL")).unwrap(), - access_token: dotenvy::var(format!( - "S3_{bucket_type}_ACCESS_TOKEN" - )) - .unwrap(), - secret: dotenvy::var(format!("S3_{bucket_type}_SECRET")) - .unwrap(), + 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(), ) diff --git a/apps/labrinth/src/test/mod.rs b/apps/labrinth/src/test/mod.rs index ba3db486aa..435a51c426 100644 --- a/apps/labrinth/src/test/mod.rs +++ b/apps/labrinth/src/test/mod.rs @@ -3,7 +3,7 @@ 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; @@ -23,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(); From 2c13b393fa17aa765280cd907a832ce25a49225d Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 18 Feb 2026 20:21:24 +0000 Subject: [PATCH 09/12] Fix mural source account env var handling --- apps/labrinth/.env.docker-compose | 2 +- apps/labrinth/.env.local | 2 +- apps/labrinth/src/env.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/labrinth/.env.docker-compose b/apps/labrinth/.env.docker-compose index 49ddff31c9..89fcdb0843 100644 --- a/apps/labrinth/.env.docker-compose +++ b/apps/labrinth/.env.docker-compose @@ -152,6 +152,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..8547b697e1 100644 --- a/apps/labrinth/.env.local +++ b/apps/labrinth/.env.local @@ -163,6 +163,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/env.rs b/apps/labrinth/src/env.rs index 5084a9c489..0263021407 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -43,7 +43,7 @@ macro_rules! vars { match parse_value::<$ty>(stringify!($field), default) { Ok(value) => Some(value), Err(source) => { - err = err.wrap_err(source); + err = err.wrap_err(eyre!("{source:#}")); None } } @@ -252,7 +252,7 @@ vars! { MURALPAY_API_URL: String; MURALPAY_API_KEY: String; MURALPAY_TRANSFER_API_KEY: String; - MURALPAY_SOURCE_ACCOUNT_ID: muralpay::AccountId; + MURALPAY_SOURCE_ACCOUNT_ID: muralpay::AccountId = muralpay::AccountId(uuid::Uuid::nil()); DEFAULT_AFFILIATE_REVENUE_SPLIT: Decimal; From 1ea4f245c05b510d636256d812c43491109ef9de Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 19 Feb 2026 16:30:36 +0000 Subject: [PATCH 10/12] Remove defaults from admin key vars --- apps/labrinth/src/env.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs index 0263021407..a78b167799 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -123,9 +123,9 @@ vars! { SENTRY_TRACES_SAMPLE_RATE: f32; SITE_URL: String; CDN_URL: String; - LABRINTH_ADMIN_KEY: String = ""; - LABRINTH_MEDAL_KEY: String = ""; - LABRINTH_EXTERNAL_NOTIFICATION_KEY: 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; From 9993e4198ace6666a40645d424130b1a8f42798d Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 19 Feb 2026 16:41:55 +0000 Subject: [PATCH 11/12] dummy commit to update github pr From a8d817fdf351c261f74f81775f6fea5a96f22afc Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 19 Feb 2026 17:15:38 +0000 Subject: [PATCH 12/12] fix ci --- apps/labrinth/.env.docker-compose | 2 ++ apps/labrinth/.env.local | 1 + 2 files changed, 3 insertions(+) diff --git a/apps/labrinth/.env.docker-compose b/apps/labrinth/.env.docker-compose index 89fcdb0843..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 diff --git a/apps/labrinth/.env.local b/apps/labrinth/.env.local index 8547b697e1..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