Improve environment variable handling and reading (#5389)

* wip: better env var reading

* move most env vars to env.rs

* migrate more env vars

* more migration

* more migrations

* More migration

* 🦀 dotenvy is gone (almost)

* 🦀 dotenvy is gone 🦀

* Fix mural source account env var handling

* Remove defaults from admin key vars

* dummy commit to update github pr

* fix ci
This commit is contained in:
aecsocket
2026-02-19 17:33:41 +00:00
committed by GitHub
parent b6b4bc21f1
commit ec81bcb13c
49 changed files with 636 additions and 661 deletions

View File

@@ -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"

View File

@@ -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<Arc<AsyncSmtpTransport<Tokio1Executor>>, 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::<u16>().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::<Tokio1Executor>::relay(&host)?
AsyncSmtpTransport::<Tokio1Executor>::relay(host)?
.port(port)
.tls(tls_setting);

View File

@@ -8,6 +8,7 @@ use crate::database::models::{
DBOrganization, DBProject, DBUser, DatabaseError,
};
use crate::database::redis::RedisPool;
use crate::env::ENV;
use crate::models::v3::notifications::NotificationBody;
use crate::routes::ApiError;
use crate::util::error::Context;
@@ -96,10 +97,18 @@ pub struct MailingIdentity {
impl MailingIdentity {
pub fn from_env() -> dotenvy::Result<Self> {
Ok(Self {
from_name: dotenvy::var("SMTP_FROM_NAME")?,
from_address: dotenvy::var("SMTP_FROM_ADDRESS")?,
reply_name: dotenvy::var("SMTP_REPLY_TO_NAME").ok(),
reply_address: dotenvy::var("SMTP_REPLY_TO_ADDRESS").ok(),
from_name: ENV.SMTP_FROM_NAME.clone(),
from_address: ENV.SMTP_FROM_ADDRESS.clone(),
reply_name: if ENV.SMTP_REPLY_TO_NAME.is_empty() {
None
} else {
Some(ENV.SMTP_REPLY_TO_NAME.clone())
},
reply_address: if ENV.SMTP_REPLY_TO_ADDRESS.is_empty() {
None
} else {
Some(ENV.SMTP_REPLY_TO_ADDRESS.clone())
},
})
}
}
@@ -558,9 +567,7 @@ async fn collect_template_variables(
NotificationBody::ResetPassword { flow } => {
let url = format!(
"{}/{}?flow={}",
dotenvy::var("SITE_URL")?,
dotenvy::var("SITE_RESET_PASSWORD_PATH")?,
flow
ENV.SITE_URL, ENV.SITE_RESET_PASSWORD_PATH, flow
);
map.insert(RESETPASSWORD_URL, url);
@@ -571,9 +578,7 @@ async fn collect_template_variables(
NotificationBody::VerifyEmail { flow } => {
let url = format!(
"{}/{}?flow={}",
dotenvy::var("SITE_URL")?,
dotenvy::var("SITE_VERIFY_EMAIL_PATH")?,
flow
ENV.SITE_URL, ENV.SITE_VERIFY_EMAIL_PATH, flow
);
map.insert(VERIFYEMAIL_URL, url);
@@ -603,11 +608,7 @@ async fn collect_template_variables(
}
NotificationBody::PaymentFailed { amount, service } => {
let url = format!(
"{}/{}",
dotenvy::var("SITE_URL")?,
dotenvy::var("SITE_BILLING_PATH")?,
);
let url = format!("{}/{}", ENV.SITE_URL, ENV.SITE_BILLING_PATH,);
let mut map = HashMap::new();
map.insert(PAYMENTFAILED_AMOUNT, amount.clone());
@@ -748,8 +749,7 @@ async fn dynamic_email_body(
key: &str,
) -> Result<String, ApiError> {
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");

View File

@@ -4,6 +4,7 @@ use crate::database::PgPool;
use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::thread_item::ThreadMessageBuilder;
use crate::database::redis::RedisPool;
use crate::env::ENV;
use crate::models::ids::ProjectId;
use crate::models::notifications::NotificationBody;
use crate::models::pack::{PackFile, PackFileHash, PackFormat};
@@ -454,7 +455,7 @@ impl AutomatedModerationQueue {
let client = reqwest::Client::new();
let res = client
.post(format!("{}/v1/fingerprints", dotenvy::var("FLAME_ANVIL_URL")?))
.post(format!("{}/v1/fingerprints", ENV.FLAME_ANVIL_URL))
.json(&serde_json::json!({
"fingerprints": hashes.iter().filter_map(|x| x.3).collect::<Vec<u32>>()
}))
@@ -553,11 +554,11 @@ impl AutomatedModerationQueue {
continue;
}
let flame_projects = if flame_files.is_empty() {
Vec::new()
} else {
let res = client
.post(format!("{}v1/mods", dotenvy::var("FLAME_ANVIL_URL")?))
let flame_projects = if flame_files.is_empty() {
Vec::new()
} else {
let res = client
.post(format!("{}v1/mods", ENV.FLAME_ANVIL_URL))
.json(&serde_json::json!({
"modIds": flame_files.iter().map(|x| x.1).collect::<Vec<_>>()
}))
@@ -664,16 +665,16 @@ impl AutomatedModerationQueue {
.insert_many(members.into_iter().map(|x| x.user_id).collect(), &mut transaction, &redis)
.await?;
if let Ok(webhook_url) = dotenvy::var("MODERATION_SLACK_WEBHOOK") {
if !ENV.MODERATION_SLACK_WEBHOOK.is_empty() {
crate::util::webhook::send_slack_project_webhook(
project.inner.id.into(),
&pool,
&redis,
webhook_url,
&ENV.MODERATION_SLACK_WEBHOOK,
Some(
format!(
"*<{}/user/AutoMod|AutoMod>* changed project status from *{}* to *Rejected*",
dotenvy::var("SITE_URL")?,
ENV.SITE_URL,
&project.inner.status.as_friendly_str(),
)
.to_string(),

View File

@@ -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::<Decimal>()
.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,

View File

@@ -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(),
}]
});

View File

@@ -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<muralpay::Client> {
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<MuralPayConfig> {
let client = create_muralpay_client()?;
let source_account_id = env_var("MURALPAY_SOURCE_ACCOUNT_ID")?
.parse::<muralpay::AccountId>()
.wrap_err("failed to parse source account ID")?;
Ok(MuralPayConfig {
client,
source_account_id,
source_account_id: ENV.MURALPAY_SOURCE_ACCOUNT_ID,
})
}
@@ -185,11 +182,8 @@ impl PayoutsQueue {
let mut creds = self.credential.write().await;
let client = reqwest::Client::new();
let combined_key = format!(
"{}:{}",
dotenvy::var("PAYPAL_CLIENT_ID")?,
dotenvy::var("PAYPAL_CLIENT_SECRET")?
);
let combined_key =
format!("{}:{}", ENV.PAYPAL_CLIENT_ID, ENV.PAYPAL_CLIENT_SECRET);
let formatted_key = format!(
"Basic {}",
base64::engine::general_purpose::STANDARD.encode(combined_key)
@@ -206,7 +200,7 @@ impl PayoutsQueue {
}
let credential: PaypalCredential = client
.post(format!("{}oauth2/token", dotenvy::var("PAYPAL_API_URL")?))
.post(format!("{}oauth2/token", ENV.PAYPAL_API_URL))
.header("Accept", "application/json")
.header("Accept-Language", "en_US")
.header("Authorization", formatted_key)
@@ -274,7 +268,7 @@ impl PayoutsQueue {
if no_api_prefix.unwrap_or(false) {
path.to_string()
} else {
format!("{}{path}", dotenvy::var("PAYPAL_API_URL")?)
format!("{}{path}", ENV.PAYPAL_API_URL)
},
)
.header(
@@ -355,13 +349,10 @@ impl PayoutsQueue {
) -> Result<X, ApiError> {
let client = reqwest::Client::new();
let mut request = client
.request(
method,
format!("{}{path}", dotenvy::var("TREMENDOUS_API_URL")?),
)
.request(method, format!("{}{path}", ENV.TREMENDOUS_API_URL))
.header(
"Authorization",
format!("Bearer {}", dotenvy::var("TREMENDOUS_API_KEY")?),
format!("Bearer {}", ENV.TREMENDOUS_API_KEY),
);
if let Some(body) = body {
@@ -511,8 +502,8 @@ impl PayoutsQueue {
let client = reqwest::Client::new();
let res = client
.get(format!("{}accounts/cash", dotenvy::var("BREX_API_URL")?))
.bearer_auth(&dotenvy::var("BREX_API_KEY")?)
.get(format!("{}accounts/cash", ENV.BREX_API_URL))
.bearer_auth(&ENV.BREX_API_KEY)
.send()
.await?
.json::<BrexResponse>()
@@ -538,16 +529,16 @@ impl PayoutsQueue {
pub async fn get_paypal_balance() -> Result<Option<AccountBalance>, ApiError>
{
let api_username = dotenvy::var("PAYPAL_NVP_USERNAME")?;
let api_password = dotenvy::var("PAYPAL_NVP_PASSWORD")?;
let api_signature = dotenvy::var("PAYPAL_NVP_SIGNATURE")?;
let api_username = &ENV.PAYPAL_NVP_USERNAME;
let api_password = &ENV.PAYPAL_NVP_PASSWORD;
let api_signature = &ENV.PAYPAL_NVP_SIGNATURE;
let mut params = HashMap::new();
params.insert("METHOD", "GetBalance");
params.insert("VERSION", "204");
params.insert("USER", &api_username);
params.insert("PWD", &api_password);
params.insert("SIGNATURE", &api_signature);
params.insert("USER", api_username);
params.insert("PWD", api_password);
params.insert("SIGNATURE", api_signature);
params.insert("RETURNALLCURRENCIES", "1");
let endpoint = "https://api-3t.paypal.com/nvp";
@@ -870,7 +861,7 @@ pub async fn make_aditude_request(
) -> Result<Vec<AditudePoints>, ApiError> {
let request = reqwest::Client::new()
.post("https://cloud.aditude.io/api/public/insights/metrics")
.bearer_auth(&dotenvy::var("ADITUDE_API_KEY")?)
.bearer_auth(&ENV.ADITUDE_API_KEY)
.json(&serde_json::json!({
"metrics": metrics,
"range": range,
@@ -1326,25 +1317,25 @@ pub async fn insert_bank_balances_and_webhook(
if inserted {
check_balance_with_webhook(
"paypal",
"PAYPAL_BALANCE_ALERT_THRESHOLD",
ENV.PAYPAL_BALANCE_ALERT_THRESHOLD,
paypal_result,
)
.await?;
check_balance_with_webhook(
"brex",
"BREX_BALANCE_ALERT_THRESHOLD",
ENV.BREX_BALANCE_ALERT_THRESHOLD,
brex_result,
)
.await?;
check_balance_with_webhook(
"tremendous",
"TREMENDOUS_BALANCE_ALERT_THRESHOLD",
ENV.TREMENDOUS_BALANCE_ALERT_THRESHOLD,
tremendous_result,
)
.await?;
check_balance_with_webhook(
"mural",
"MURAL_BALANCE_ALERT_THRESHOLD",
ENV.MURAL_BALANCE_ALERT_THRESHOLD,
mural_result,
)
.await?;
@@ -1357,14 +1348,11 @@ pub async fn insert_bank_balances_and_webhook(
async fn check_balance_with_webhook(
source: &str,
threshold_env_var_name: &str,
threshold: u64,
result: Result<Option<AccountBalance>, ApiError>,
) -> Result<Option<AccountBalance>, ApiError> {
let maybe_threshold = dotenvy::var(threshold_env_var_name)
.ok()
.and_then(|x| x.parse::<u64>().ok())
.filter(|x| *x != 0);
let payout_alert_webhook = dotenvy::var("PAYOUT_ALERT_SLACK_WEBHOOK")?;
let maybe_threshold = if threshold > 0 { Some(threshold) } else { None };
let payout_alert_webhook = &ENV.PAYOUT_ALERT_SLACK_WEBHOOK;
match &result {
Ok(Some(account_balance)) => {
@@ -1379,7 +1367,7 @@ async fn check_balance_with_webhook(
threshold,
current_balance: available,
},
&payout_alert_webhook,
payout_alert_webhook,
)
.await?;
}
@@ -1394,7 +1382,7 @@ async fn check_balance_with_webhook(
source: source.to_owned(),
display_error: error.to_string(),
},
&payout_alert_webhook,
payout_alert_webhook,
)
.await?;
}