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

@@ -8,6 +8,8 @@ SITE_URL=http://localhost:3000
# This CDN URL matches the local storage backend set below, which uses MOCK_FILE_PATH # This CDN URL matches the local storage backend set below, which uses MOCK_FILE_PATH
CDN_URL=file:///tmp/modrinth CDN_URL=file:///tmp/modrinth
LABRINTH_ADMIN_KEY=feedbeef LABRINTH_ADMIN_KEY=feedbeef
LABRINTH_MEDAL_KEY=
LABRINTH_EXTERNAL_NOTIFICATION_KEY=beeffeed
RATE_LIMIT_IGNORE_KEY=feedbeef RATE_LIMIT_IGNORE_KEY=feedbeef
DATABASE_URL=postgresql://labrinth:labrinth@labrinth-postgres/labrinth DATABASE_URL=postgresql://labrinth:labrinth@labrinth-postgres/labrinth
@@ -152,6 +154,6 @@ ARCHON_URL=none
MURALPAY_API_URL=https://api.muralpay.com MURALPAY_API_URL=https://api.muralpay.com
MURALPAY_API_KEY=none MURALPAY_API_KEY=none
MURALPAY_TRANSFER_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 DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1

View File

@@ -8,6 +8,7 @@ SITE_URL=http://localhost:3000
# This CDN URL matches the local storage backend set below, which uses MOCK_FILE_PATH # This CDN URL matches the local storage backend set below, which uses MOCK_FILE_PATH
CDN_URL=file:///tmp/modrinth CDN_URL=file:///tmp/modrinth
LABRINTH_ADMIN_KEY=feedbeef LABRINTH_ADMIN_KEY=feedbeef
LABRINTH_MEDAL_KEY=
LABRINTH_EXTERNAL_NOTIFICATION_KEY=beeffeed LABRINTH_EXTERNAL_NOTIFICATION_KEY=beeffeed
RATE_LIMIT_IGNORE_KEY=feedbeef RATE_LIMIT_IGNORE_KEY=feedbeef
@@ -163,6 +164,6 @@ ARCHON_URL=none
MURALPAY_API_URL=https://api-staging.muralpay.com MURALPAY_API_URL=https://api-staging.muralpay.com
MURALPAY_API_KEY=none MURALPAY_API_KEY=none
MURALPAY_TRANSFER_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 DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1

View File

@@ -2,6 +2,7 @@ use super::AuthProvider;
use crate::auth::AuthenticationError; use crate::auth::AuthenticationError;
use crate::database::models::{DBUser, user_item}; use crate::database::models::{DBUser, user_item};
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::env::ENV;
use crate::models::pats::Scopes; use crate::models::pats::Scopes;
use crate::models::users::User; use crate::models::users::User;
use crate::queue::session::AuthQueue; use crate::queue::session::AuthQueue;
@@ -146,7 +147,7 @@ where
user_item::DBUser::get_id(session.user_id, executor, redis) user_item::DBUser::get_id(session.user_id, executor, redis)
.await?; .await?;
let rate_limit_ignore = dotenvy::var("RATE_LIMIT_IGNORE_KEY")?; let rate_limit_ignore = &ENV.RATE_LIMIT_IGNORE_KEY;
if req if req
.headers() .headers()
.get("x-ratelimit-key") .get("x-ratelimit-key")

View File

@@ -5,9 +5,10 @@ mod fetch;
pub use fetch::*; pub use fetch::*;
use crate::env::ENV;
pub async fn init_client() -> clickhouse::error::Result<clickhouse::Client> { pub async fn init_client() -> clickhouse::error::Result<clickhouse::Client> {
init_client_with_database(&dotenvy::var("CLICKHOUSE_DATABASE").unwrap()) init_client_with_database(&ENV.CLICKHOUSE_DATABASE).await
.await
} }
pub async fn init_client_with_database( pub async fn init_client_with_database(
@@ -24,9 +25,9 @@ pub async fn init_client_with_database(
.build(https_connector); .build(https_connector);
clickhouse::Client::with_http_client(hyper_client) clickhouse::Client::with_http_client(hyper_client)
.with_url(dotenvy::var("CLICKHOUSE_URL").unwrap()) .with_url(&ENV.CLICKHOUSE_URL)
.with_user(dotenvy::var("CLICKHOUSE_USER").unwrap()) .with_user(&ENV.CLICKHOUSE_USER)
.with_password(dotenvy::var("CLICKHOUSE_PASSWORD").unwrap()) .with_password(&ENV.CLICKHOUSE_PASSWORD)
.with_validation(false) .with_validation(false)
}; };
@@ -35,8 +36,7 @@ pub async fn init_client_with_database(
.execute() .execute()
.await?; .await?;
let clickhouse_replicated = let clickhouse_replicated = ENV.CLICKHOUSE_REPLICATED;
dotenvy::var("CLICKHOUSE_REPLICATED").unwrap() == "true";
let cluster_line = if clickhouse_replicated { let cluster_line = if clickhouse_replicated {
"ON cluster '{cluster}'" "ON cluster '{cluster}'"
} else { } else {

View File

@@ -13,6 +13,8 @@ pub type PgTransaction<'c> = sqlx_tracing::Transaction<'c, Postgres>;
pub use sqlx_tracing::Acquire; pub use sqlx_tracing::Acquire;
pub use sqlx_tracing::Executor; pub use sqlx_tracing::Executor;
use crate::env::ENV;
// pub type PgPool = sqlx::PgPool; // pub type PgPool = sqlx::PgPool;
// pub type PgTransaction<'c> = sqlx::Transaction<'c, Postgres>; // pub type PgTransaction<'c> = sqlx::Transaction<'c, Postgres>;
// pub use sqlx::Acquire; // pub use sqlx::Acquire;
@@ -50,57 +52,27 @@ impl DerefMut for ReadOnlyPgPool {
pub async fn connect_all() -> Result<(PgPool, ReadOnlyPgPool), sqlx::Error> { pub async fn connect_all() -> Result<(PgPool, ReadOnlyPgPool), sqlx::Error> {
info!("Initializing database connection"); info!("Initializing database connection");
let database_url = let database_url = &ENV.DATABASE_URL;
dotenvy::var("DATABASE_URL").expect("`DATABASE_URL` not in .env");
let acquire_timeout = let acquire_timeout =
dotenvy::var("DATABASE_ACQUIRE_TIMEOUT_MS") Duration::from_millis(ENV.DATABASE_ACQUIRE_TIMEOUT_MS);
.ok()
.map_or_else(
|| Duration::from_millis(30000),
|x| {
Duration::from_millis(x.parse::<u64>().expect(
"DATABASE_ACQUIRE_TIMEOUT_MS must be a valid u64",
))
},
);
let pool = PgPoolOptions::new() let pool = PgPoolOptions::new()
.acquire_timeout(acquire_timeout) .acquire_timeout(acquire_timeout)
.min_connections( .min_connections(ENV.DATABASE_MIN_CONNECTIONS)
dotenvy::var("DATABASE_MIN_CONNECTIONS") .max_connections(ENV.DATABASE_MAX_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),
)
.max_lifetime(Some(Duration::from_secs(60 * 60))) .max_lifetime(Some(Duration::from_secs(60 * 60)))
.connect(&database_url) .connect(database_url)
.await?; .await?;
let pool = PgPool::from(pool); 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() let ro_pool = PgPoolOptions::new()
.acquire_timeout(acquire_timeout) .acquire_timeout(acquire_timeout)
.min_connections( .min_connections(ENV.READONLY_DATABASE_MIN_CONNECTIONS)
dotenvy::var("READONLY_DATABASE_MIN_CONNECTIONS") .max_connections(ENV.READONLY_DATABASE_MAX_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),
)
.max_lifetime(Some(Duration::from_secs(60 * 60))) .max_lifetime(Some(Duration::from_secs(60 * 60)))
.connect(&url) .connect(&ENV.READONLY_DATABASE_URL)
.await?; .await?;
let ro_pool = PgPool::from(ro_pool); let ro_pool = PgPool::from(ro_pool);
@@ -112,8 +84,7 @@ pub async fn connect_all() -> Result<(PgPool, ReadOnlyPgPool), sqlx::Error> {
} }
pub async fn check_for_migrations() -> eyre::Result<()> { pub async fn check_for_migrations() -> eyre::Result<()> {
let uri = let uri = &ENV.DATABASE_URL;
dotenvy::var("DATABASE_URL").wrap_err("`DATABASE_URL` not in .env")?;
let uri = uri.as_str(); let uri = uri.as_str();
if !Postgres::database_exists(uri) if !Postgres::database_exists(uri)
.await .await

View File

@@ -1,3 +1,5 @@
use crate::env::ENV;
use super::models::DatabaseError; use super::models::DatabaseError;
use ariadne::ids::base62_impl::{parse_base62, to_base62}; use ariadne::ids::base62_impl::{parse_base62, to_base62};
use chrono::{TimeZone, Utc}; use chrono::{TimeZone, Utc};
@@ -42,44 +44,26 @@ impl RedisPool {
// testing pool uses a hashmap to mimic redis behaviour for very small data sizes (ie: tests) // 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 // PANICS: production pool will panic if redis url is not set
pub fn new(meta_namespace: impl Into<Arc<str>>) -> Self { pub fn new(meta_namespace: impl Into<Arc<str>>) -> Self {
let wait_timeout = let wait_timeout = Duration::from_millis(ENV.REDIS_WAIT_TIMEOUT_MS);
dotenvy::var("REDIS_WAIT_TIMEOUT_MS").ok().map_or_else(
|| Duration::from_millis(15000),
|x| {
Duration::from_millis(
x.parse::<u64>().expect(
"REDIS_WAIT_TIMEOUT_MS must be a valid u64",
),
)
},
);
let url = dotenvy::var("REDIS_URL").expect("Redis URL not set"); let url = &ENV.REDIS_URL;
let pool = Config::from_url(url.clone()) let pool = Config::from_url(url.clone())
.builder() .builder()
.expect("Error building Redis pool") .expect("Error building Redis pool")
.max_size( .max_size(ENV.REDIS_MAX_CONNECTIONS as usize)
dotenvy::var("REDIS_MAX_CONNECTIONS")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(10000),
)
.wait_timeout(Some(wait_timeout)) .wait_timeout(Some(wait_timeout))
.runtime(Runtime::Tokio1) .runtime(Runtime::Tokio1)
.build() .build()
.expect("Redis connection failed"); .expect("Redis connection failed");
let pool = RedisPool { let pool = RedisPool {
url, url: url.clone(),
pool, pool,
cache_list: Arc::new(DashMap::with_capacity(2048)), cache_list: Arc::new(DashMap::with_capacity(2048)),
meta_namespace: meta_namespace.into(), meta_namespace: meta_namespace.into(),
}; };
let redis_min_connections = dotenvy::var("REDIS_MIN_CONNECTIONS") let redis_min_connections = ENV.REDIS_MIN_CONNECTIONS;
.ok()
.and_then(|x| x.parse::<usize>().ok())
.unwrap_or(0);
let spawn_min_connections = (0..redis_min_connections) let spawn_min_connections = (0..redis_min_connections)
.map(|_| { .map(|_| {
let pool = pool.clone(); let pool = pool.clone();

280
apps/labrinth/src/env.rs Normal file
View File

@@ -0,0 +1,280 @@
use std::{any::type_name, convert::Infallible, str::FromStr, sync::LazyLock};
use derive_more::{Deref, DerefMut};
use eyre::{Context, eyre};
use rust_decimal::Decimal;
use serde::de::DeserializeOwned;
macro_rules! vars {
(
$(
$field:ident: $ty:ty $(= $default:expr)?;
)*
) => {
#[derive(Debug)]
#[allow(
non_snake_case,
reason = "environment variables are UPPER_SNAKE_CASE",
)]
pub struct EnvVars {
$(
pub $field: $ty,
)*
}
impl EnvVars {
pub fn from_env() -> eyre::Result<Self> {
let mut err = eyre!("failed to read environment variables");
$(
#[expect(
non_snake_case,
reason = "environment variables are UPPER_SNAKE_CASE",
)]
#[allow(
unused_assignments,
unused_mut,
reason = "`default` is not used if there is no default",
)]
let $field: Option<$ty> = {
let mut default = None::<$ty>;
$( default = Some({ $default }.into()); )?
match parse_value::<$ty>(stringify!($field), default) {
Ok(value) => Some(value),
Err(source) => {
err = err.wrap_err(eyre!("{source:#}"));
None
}
}
};
)*
Ok(EnvVars {
$(
$field: match $field {
Some(value) => value,
None => return Err(err),
},
)*
})
}
}
};
}
pub static ENV: LazyLock<EnvVars> = LazyLock::new(|| {
EnvVars::from_env().unwrap_or_else(|err| panic!("{err:?}"))
});
fn parse_value<T>(key: &str, default: Option<T>) -> eyre::Result<T>
where
T: FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
{
match (dotenvy::var(key), default) {
(Ok(value), _) => value.parse::<T>().wrap_err_with(|| {
eyre!("`{key}` is not a valid `{}`", type_name::<T>())
}),
(Err(_), Some(default)) => Ok(default),
(Err(_), None) => Err(eyre!("`{key}` missing")),
}
}
pub fn init() -> eyre::Result<()> {
EnvVars::from_env()?;
LazyLock::force(&ENV);
Ok(())
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Deref, DerefMut,
)]
pub struct Json<T: DeserializeOwned>(pub T);
impl<T: DeserializeOwned> FromStr for Json<T> {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s).map(Self)
}
}
#[derive(
Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deref, DerefMut,
)]
pub struct StringCsv(pub Vec<String>);
impl FromStr for StringCsv {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let v = s
.split(',')
.filter(|s| !s.trim().is_empty())
.map(|s| s.to_string())
.collect::<Vec<_>>();
Ok(Self(v))
}
}
vars! {
SENTRY_ENVIRONMENT: String;
SENTRY_TRACES_SAMPLE_RATE: f32;
SITE_URL: String;
CDN_URL: String;
LABRINTH_ADMIN_KEY: String;
LABRINTH_MEDAL_KEY: String;
LABRINTH_EXTERNAL_NOTIFICATION_KEY: String;
RATE_LIMIT_IGNORE_KEY: String;
DATABASE_URL: String;
MEILISEARCH_READ_ADDR: String;
MEILISEARCH_WRITE_ADDRS: StringCsv;
MEILISEARCH_KEY: String;
REDIS_URL: String;
BIND_ADDR: String;
SELF_ADDR: String;
LOCAL_INDEX_INTERVAL: u64;
VERSION_INDEX_INTERVAL: u64;
WHITELISTED_MODPACK_DOMAINS: Json<Vec<String>>;
ALLOWED_CALLBACK_URLS: Json<Vec<String>>;
ANALYTICS_ALLOWED_ORIGINS: Json<Vec<String>>;
// storage
STORAGE_BACKEND: crate::file_hosting::FileHostKind;
// s3
S3_PUBLIC_BUCKET_NAME: String = "";
S3_PUBLIC_USES_PATH_STYLE_BUCKET: bool = false;
S3_PUBLIC_REGION: String = "";
S3_PUBLIC_URL: String = "";
S3_PUBLIC_ACCESS_TOKEN: String = "";
S3_PUBLIC_SECRET: String = "";
S3_PRIVATE_BUCKET_NAME: String = "";
S3_PRIVATE_USES_PATH_STYLE_BUCKET: bool = false;
S3_PRIVATE_REGION: String = "";
S3_PRIVATE_URL: String = "";
S3_PRIVATE_ACCESS_TOKEN: String = "";
S3_PRIVATE_SECRET: String = "";
// local
MOCK_FILE_PATH: String = "";
GITHUB_CLIENT_ID: String;
GITHUB_CLIENT_SECRET: String;
GITLAB_CLIENT_ID: String;
GITLAB_CLIENT_SECRET: String;
DISCORD_CLIENT_ID: String;
DISCORD_CLIENT_SECRET: String;
MICROSOFT_CLIENT_ID: String;
MICROSOFT_CLIENT_SECRET: String;
GOOGLE_CLIENT_ID: String;
GOOGLE_CLIENT_SECRET: String;
STEAM_API_KEY: String;
TREMENDOUS_API_URL: String;
TREMENDOUS_API_KEY: String;
TREMENDOUS_PRIVATE_KEY: String;
PAYPAL_API_URL: String;
PAYPAL_WEBHOOK_ID: String;
PAYPAL_CLIENT_ID: String;
PAYPAL_CLIENT_SECRET: String;
PAYPAL_NVP_USERNAME: String;
PAYPAL_NVP_PASSWORD: String;
PAYPAL_NVP_SIGNATURE: String;
PAYPAL_BALANCE_ALERT_THRESHOLD: u64 = 0u64;
BREX_BALANCE_ALERT_THRESHOLD: u64 = 0u64;
TREMENDOUS_BALANCE_ALERT_THRESHOLD: u64 = 0u64;
MURAL_BALANCE_ALERT_THRESHOLD: u64 = 0u64;
HCAPTCHA_SECRET: String;
SMTP_USERNAME: String;
SMTP_PASSWORD: String;
SMTP_HOST: String;
SMTP_PORT: u16;
SMTP_TLS: String;
SMTP_FROM_NAME: String;
SMTP_FROM_ADDRESS: String;
SITE_VERIFY_EMAIL_PATH: String;
SITE_RESET_PASSWORD_PATH: String;
SITE_BILLING_PATH: String;
SENDY_URL: String;
SENDY_LIST_ID: String;
SENDY_API_KEY: String;
CLICKHOUSE_REPLICATED: bool;
CLICKHOUSE_URL: String;
CLICKHOUSE_USER: String;
CLICKHOUSE_PASSWORD: String;
CLICKHOUSE_DATABASE: String;
FLAME_ANVIL_URL: String;
GOTENBERG_URL: String;
GOTENBERG_CALLBACK_BASE: String;
GOTENBERG_TIMEOUT: u64;
STRIPE_API_KEY: String;
STRIPE_WEBHOOK_SECRET: String;
ADITUDE_API_KEY: String;
PYRO_API_KEY: String;
BREX_API_URL: String;
BREX_API_KEY: String;
DELPHI_URL: String;
AVALARA_1099_API_URL: String;
AVALARA_1099_API_KEY: String;
AVALARA_1099_API_TEAM_ID: String;
AVALARA_1099_COMPANY_ID: String;
ANROK_API_URL: String;
ANROK_API_KEY: String;
COMPLIANCE_PAYOUT_THRESHOLD: String;
PAYOUT_ALERT_SLACK_WEBHOOK: String;
CLOUDFLARE_INTEGRATION: bool = false;
ARCHON_URL: String;
MURALPAY_API_URL: String;
MURALPAY_API_KEY: String;
MURALPAY_TRANSFER_API_KEY: String;
MURALPAY_SOURCE_ACCOUNT_ID: muralpay::AccountId = muralpay::AccountId(uuid::Uuid::nil());
DEFAULT_AFFILIATE_REVENUE_SPLIT: Decimal;
DATABASE_ACQUIRE_TIMEOUT_MS: u64 = 30000u64;
DATABASE_MIN_CONNECTIONS: u32 = 0u32;
DATABASE_MAX_CONNECTIONS: u32 = 16u32;
READONLY_DATABASE_URL: String = "";
READONLY_DATABASE_MIN_CONNECTIONS: u32 = 0u32;
READONLY_DATABASE_MAX_CONNECTIONS: u32 = 1u32;
REDIS_WAIT_TIMEOUT_MS: u64 = 15000u64;
REDIS_MAX_CONNECTIONS: u32 = 10000u32;
REDIS_MIN_CONNECTIONS: usize = 0usize;
SEARCH_OPERATION_TIMEOUT: u64 = 300000u64;
SMTP_REPLY_TO_NAME: String = "";
SMTP_REPLY_TO_ADDRESS: String = "";
PUBLIC_DISCORD_WEBHOOK: String = "";
MODERATION_SLACK_WEBHOOK: String = "";
DELPHI_SLACK_WEBHOOK: String = "";
TREMENDOUS_CAMPAIGN_ID: String = "";
}

View File

@@ -9,6 +9,8 @@ use hex::ToHex;
use sha2::Digest; use sha2::Digest;
use std::path::PathBuf; use std::path::PathBuf;
use crate::env::ENV;
#[derive(Default)] #[derive(Default)]
pub struct MockHost(()); pub struct MockHost(());
@@ -54,8 +56,7 @@ impl FileHost for MockHost {
file_name: &str, file_name: &str,
_expiry_secs: u32, _expiry_secs: u32,
) -> Result<String, FileHostingError> { ) -> Result<String, FileHostingError> {
let cdn_url = dotenvy::var("CDN_URL").unwrap(); Ok(format!("{}/private/{file_name}", ENV.CDN_URL))
Ok(format!("{cdn_url}/private/{file_name}"))
} }
async fn delete_file( async fn delete_file(
@@ -77,7 +78,7 @@ fn get_file_path(
file_name: &str, file_name: &str,
file_publicity: FileHostPublicity, file_publicity: FileHostPublicity,
) -> PathBuf { ) -> 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) { if matches!(file_publicity, FileHostPublicity::Private) {
path.push("private"); path.push("private");

View File

@@ -1,3 +1,5 @@
use std::str::FromStr;
use async_trait::async_trait; use async_trait::async_trait;
use thiserror::Error; use thiserror::Error;
@@ -63,3 +65,25 @@ pub trait FileHost {
file_publicity: FileHostPublicity, file_publicity: FileHostPublicity,
) -> Result<DeleteFileData, FileHostingError>; ) -> Result<DeleteFileData, FileHostingError>;
} }
#[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<Self, Self::Err> {
Ok(match s {
"s3" => Self::S3,
"local" => Self::Local,
_ => return Err(InvalidFileHostKind),
})
}
}

View File

@@ -16,11 +16,11 @@ use util::gotenberg::GotenbergClient;
use crate::background_task::update_versions; use crate::background_task::update_versions;
use crate::database::{PgPool, ReadOnlyPgPool}; use crate::database::{PgPool, ReadOnlyPgPool};
use crate::env::ENV;
use crate::queue::billing::{index_billing, index_subscriptions}; use crate::queue::billing::{index_billing, index_subscriptions};
use crate::queue::moderation::AutomatedModerationQueue; use crate::queue::moderation::AutomatedModerationQueue;
use crate::util::anrok; use crate::util::anrok;
use crate::util::archon::ArchonClient; use crate::util::archon::ArchonClient;
use crate::util::env::{parse_strings_from_var, parse_var};
use crate::util::ratelimit::{AsyncRateLimiter, GCRAParameters}; use crate::util::ratelimit::{AsyncRateLimiter, GCRAParameters};
use sync::friends::handle_pubsub; use sync::friends::handle_pubsub;
@@ -28,6 +28,7 @@ pub mod auth;
pub mod background_task; pub mod background_task;
pub mod clickhouse; pub mod clickhouse;
pub mod database; pub mod database;
pub mod env;
pub mod file_hosting; pub mod file_hosting;
pub mod models; pub mod models;
pub mod queue; pub mod queue;
@@ -83,10 +84,7 @@ pub fn app_setup(
gotenberg_client: GotenbergClient, gotenberg_client: GotenbergClient,
enable_background_tasks: bool, enable_background_tasks: bool,
) -> LabrinthConfig { ) -> LabrinthConfig {
info!( info!("Starting labrinth on {}", &ENV.BIND_ADDR);
"Starting labrinth on {}",
dotenvy::var("BIND_ADDR").unwrap()
);
let automated_moderation_queue = let automated_moderation_queue =
web::Data::new(AutomatedModerationQueue::default()); web::Data::new(AutomatedModerationQueue::default());
@@ -112,9 +110,8 @@ pub fn app_setup(
if enable_background_tasks { if enable_background_tasks {
// The interval in seconds at which the local database is indexed // The interval in seconds at which the local database is indexed
// for searching. Defaults to 1 hour if unset. // for searching. Defaults to 1 hour if unset.
let local_index_interval = Duration::from_secs( let local_index_interval =
parse_var("LOCAL_INDEX_INTERVAL").unwrap_or(3600), Duration::from_secs(ENV.LOCAL_INDEX_INTERVAL);
);
let pool_ref = pool.clone(); let pool_ref = pool.clone();
let search_config_ref = search_config.clone(); let search_config_ref = search_config.clone();
let redis_pool_ref = redis_pool.clone(); let redis_pool_ref = redis_pool.clone();
@@ -142,9 +139,8 @@ pub fn app_setup(
} }
}); });
let version_index_interval = Duration::from_secs( let version_index_interval =
parse_var("VERSION_INDEX_INTERVAL").unwrap_or(1800), Duration::from_secs(ENV.VERSION_INDEX_INTERVAL);
);
let pool_ref = pool.clone(); let pool_ref = pool.clone();
let redis_pool_ref = redis_pool.clone(); let redis_pool_ref = redis_pool.clone();
scheduler.run(version_index_interval, move || { scheduler.run(version_index_interval, move || {
@@ -349,188 +345,3 @@ pub fn utoipa_app_config(
.configure(routes::v3::utoipa_config) .configure(routes::v3::utoipa_config)
.configure(routes::internal::utoipa_config); .configure(routes::internal::utoipa_config);
} }
// This is so that env vars not used immediately don't panic at runtime
pub fn check_env_vars() -> bool {
let mut failed = false;
fn check_var<T: std::str::FromStr>(var: &str) -> bool {
let check = parse_var::<T>(var).is_none();
if check {
warn!(
"Variable `{}` missing in dotenv or not of type `{}`",
var,
std::any::type_name::<T>()
);
}
check
}
failed |= check_var::<String>("SENTRY_ENVIRONMENT");
failed |= check_var::<String>("SENTRY_TRACES_SAMPLE_RATE");
failed |= check_var::<String>("SITE_URL");
failed |= check_var::<String>("CDN_URL");
failed |= check_var::<String>("LABRINTH_ADMIN_KEY");
failed |= check_var::<String>("LABRINTH_EXTERNAL_NOTIFICATION_KEY");
failed |= check_var::<String>("RATE_LIMIT_IGNORE_KEY");
failed |= check_var::<String>("DATABASE_URL");
failed |= check_var::<String>("MEILISEARCH_READ_ADDR");
failed |= check_var::<String>("MEILISEARCH_WRITE_ADDRS");
failed |= check_var::<String>("MEILISEARCH_KEY");
failed |= check_var::<String>("REDIS_URL");
failed |= check_var::<String>("BIND_ADDR");
failed |= check_var::<String>("SELF_ADDR");
failed |= check_var::<String>("STORAGE_BACKEND");
let storage_backend = dotenvy::var("STORAGE_BACKEND").ok();
match storage_backend.as_deref() {
Some("s3") => {
let mut check_var_set = |var_prefix| {
failed |= check_var::<String>(&format!(
"S3_{var_prefix}_BUCKET_NAME"
));
failed |= check_var::<bool>(&format!(
"S3_{var_prefix}_USES_PATH_STYLE_BUCKET"
));
failed |=
check_var::<String>(&format!("S3_{var_prefix}_REGION"));
failed |= check_var::<String>(&format!("S3_{var_prefix}_URL"));
failed |= check_var::<String>(&format!(
"S3_{var_prefix}_ACCESS_TOKEN"
));
failed |=
check_var::<String>(&format!("S3_{var_prefix}_SECRET"));
};
check_var_set("PUBLIC");
check_var_set("PRIVATE");
}
Some("local") => {
failed |= check_var::<String>("MOCK_FILE_PATH");
}
Some(backend) => {
warn!(
"Variable `STORAGE_BACKEND` contains an invalid value: {backend}. Expected \"s3\" or \"local\"."
);
failed |= true;
}
_ => {
warn!("Variable `STORAGE_BACKEND` is not set!");
failed |= true;
}
}
failed |= check_var::<usize>("LOCAL_INDEX_INTERVAL");
failed |= check_var::<usize>("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::<String>("GITHUB_CLIENT_ID");
failed |= check_var::<String>("GITHUB_CLIENT_SECRET");
failed |= check_var::<String>("GITLAB_CLIENT_ID");
failed |= check_var::<String>("GITLAB_CLIENT_SECRET");
failed |= check_var::<String>("DISCORD_CLIENT_ID");
failed |= check_var::<String>("DISCORD_CLIENT_SECRET");
failed |= check_var::<String>("MICROSOFT_CLIENT_ID");
failed |= check_var::<String>("MICROSOFT_CLIENT_SECRET");
failed |= check_var::<String>("GOOGLE_CLIENT_ID");
failed |= check_var::<String>("GOOGLE_CLIENT_SECRET");
failed |= check_var::<String>("STEAM_API_KEY");
failed |= check_var::<String>("TREMENDOUS_API_URL");
failed |= check_var::<String>("TREMENDOUS_API_KEY");
failed |= check_var::<String>("TREMENDOUS_PRIVATE_KEY");
failed |= check_var::<String>("PAYPAL_API_URL");
failed |= check_var::<String>("PAYPAL_WEBHOOK_ID");
failed |= check_var::<String>("PAYPAL_CLIENT_ID");
failed |= check_var::<String>("PAYPAL_CLIENT_SECRET");
failed |= check_var::<String>("PAYPAL_NVP_USERNAME");
failed |= check_var::<String>("PAYPAL_NVP_PASSWORD");
failed |= check_var::<String>("PAYPAL_NVP_SIGNATURE");
failed |= check_var::<String>("HCAPTCHA_SECRET");
failed |= check_var::<String>("SMTP_USERNAME");
failed |= check_var::<String>("SMTP_PASSWORD");
failed |= check_var::<String>("SMTP_HOST");
failed |= check_var::<u16>("SMTP_PORT");
failed |= check_var::<String>("SMTP_TLS");
failed |= check_var::<String>("SMTP_FROM_NAME");
failed |= check_var::<String>("SMTP_FROM_ADDRESS");
failed |= check_var::<String>("SITE_VERIFY_EMAIL_PATH");
failed |= check_var::<String>("SITE_RESET_PASSWORD_PATH");
failed |= check_var::<String>("SITE_BILLING_PATH");
failed |= check_var::<String>("SENDY_URL");
failed |= check_var::<String>("SENDY_LIST_ID");
failed |= check_var::<String>("SENDY_API_KEY");
if parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS").is_none() {
warn!(
"Variable `ANALYTICS_ALLOWED_ORIGINS` missing in dotenv or not a json array of strings"
);
failed |= true;
}
failed |= check_var::<bool>("CLICKHOUSE_REPLICATED");
failed |= check_var::<String>("CLICKHOUSE_URL");
failed |= check_var::<String>("CLICKHOUSE_USER");
failed |= check_var::<String>("CLICKHOUSE_PASSWORD");
failed |= check_var::<String>("CLICKHOUSE_DATABASE");
failed |= check_var::<String>("FLAME_ANVIL_URL");
failed |= check_var::<String>("GOTENBERG_URL");
failed |= check_var::<String>("GOTENBERG_CALLBACK_BASE");
failed |= check_var::<String>("GOTENBERG_TIMEOUT");
failed |= check_var::<String>("STRIPE_API_KEY");
failed |= check_var::<String>("STRIPE_WEBHOOK_SECRET");
failed |= check_var::<String>("ADITUDE_API_KEY");
failed |= check_var::<String>("PYRO_API_KEY");
failed |= check_var::<String>("BREX_API_URL");
failed |= check_var::<String>("BREX_API_KEY");
failed |= check_var::<String>("DELPHI_URL");
failed |= check_var::<String>("AVALARA_1099_API_URL");
failed |= check_var::<String>("AVALARA_1099_API_KEY");
failed |= check_var::<String>("AVALARA_1099_API_TEAM_ID");
failed |= check_var::<String>("AVALARA_1099_COMPANY_ID");
failed |= check_var::<String>("ANROK_API_URL");
failed |= check_var::<String>("ANROK_API_KEY");
failed |= check_var::<String>("COMPLIANCE_PAYOUT_THRESHOLD");
failed |= check_var::<String>("PAYOUT_ALERT_SLACK_WEBHOOK");
failed |= check_var::<String>("ARCHON_URL");
failed |= check_var::<String>("MURALPAY_API_URL");
failed |= check_var::<String>("MURALPAY_API_KEY");
failed |= check_var::<String>("MURALPAY_TRANSFER_API_KEY");
failed |= check_var::<String>("MURALPAY_SOURCE_ACCOUNT_ID");
failed |= check_var::<String>("DEFAULT_AFFILIATE_REVENUE_SPLIT");
failed
}

View File

@@ -4,21 +4,21 @@ use actix_web::{App, HttpServer};
use actix_web_prom::PrometheusMetricsBuilder; use actix_web_prom::PrometheusMetricsBuilder;
use clap::Parser; use clap::Parser;
use labrinth::app_config;
use labrinth::background_task::BackgroundTask; use labrinth::background_task::BackgroundTask;
use labrinth::database::redis::RedisPool; use labrinth::database::redis::RedisPool;
use labrinth::file_hosting::{S3BucketConfig, S3Host}; use labrinth::env::ENV;
use labrinth::file_hosting::{FileHostKind, S3BucketConfig, S3Host};
use labrinth::queue::email::EmailQueue; use labrinth::queue::email::EmailQueue;
use labrinth::search; use labrinth::search;
use labrinth::util::anrok; use labrinth::util::anrok;
use labrinth::util::env::parse_var;
use labrinth::util::gotenberg::GotenbergClient; use labrinth::util::gotenberg::GotenbergClient;
use labrinth::util::ratelimit::rate_limit_middleware; use labrinth::util::ratelimit::rate_limit_middleware;
use labrinth::utoipa_app_config; 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::ffi::CStr;
use std::sync::Arc; use std::sync::Arc;
use tracing::{Instrument, error, info, info_span}; use tracing::{Instrument, info, info_span};
use tracing_actix_web::TracingLogger; use tracing_actix_web::TracingLogger;
use utoipa::OpenApi; use utoipa::OpenApi;
use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}; 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`"); color_eyre::install().expect("failed to install `color-eyre`");
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
modrinth_util::log::init().expect("failed to initialize logging"); modrinth_util::log::init().expect("failed to initialize logging");
env::init().expect("failed to initialize environment variables");
if check_env_vars() {
error!("Some environment variables are missing!");
std::process::exit(1);
}
// Sentry must be set up before the async runtime is started // Sentry must be set up before the async runtime is started
// <https://docs.sentry.io/platforms/rust/guides/actix-web/> // <https://docs.sentry.io/platforms/rust/guides/actix-web/>
@@ -70,11 +66,8 @@ fn main() -> std::io::Result<()> {
// Has no effect if not set. // Has no effect if not set.
let sentry = sentry::init(sentry::ClientOptions { let sentry = sentry::init(sentry::ClientOptions {
release: sentry::release_name!(), release: sentry::release_name!(),
traces_sample_rate: dotenvy::var("SENTRY_TRACES_SAMPLE_RATE") traces_sample_rate: ENV.SENTRY_TRACES_SAMPLE_RATE,
.unwrap() environment: Some((&ENV.SENTRY_ENVIRONMENT).into()),
.parse()
.expect("failed to parse `SENTRY_TRACES_SAMPLE_RATE` as number"),
environment: Some(dotenvy::var("SENTRY_ENVIRONMENT").unwrap().into()),
..Default::default() ..Default::default()
}); });
if sentry.is_enabled() { if sentry.is_enabled() {
@@ -99,10 +92,7 @@ async fn app() -> std::io::Result<()> {
.unwrap(); .unwrap();
if args.run_background_task.is_none() { if args.run_background_task.is_none() {
info!( info!("Starting labrinth on {}", &ENV.BIND_ADDR);
"Starting labrinth on {}",
dotenvy::var("BIND_ADDR").unwrap()
);
if !args.no_migrations { if !args.no_migrations {
database::check_for_migrations() database::check_for_migrations()
@@ -119,40 +109,44 @@ async fn app() -> std::io::Result<()> {
// Redis connector // Redis connector
let redis_pool = RedisPool::new(""); let redis_pool = RedisPool::new("");
let storage_backend = let storage_backend = ENV.STORAGE_BACKEND;
dotenvy::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string());
let file_host: Arc<dyn file_hosting::FileHost + Send + Sync> = let file_host: Arc<dyn file_hosting::FileHost + Send + Sync> =
match storage_backend.as_str() { match storage_backend {
"s3" => { FileHostKind::S3 => {
let config_from_env = |bucket_type| S3BucketConfig { let not_empty = |v: &str| -> String {
name: parse_var(&format!("S3_{bucket_type}_BUCKET_NAME")) assert!(!v.is_empty(), "S3 env var is empty");
.unwrap(), v.to_string()
uses_path_style: parse_var(&format!(
"S3_{bucket_type}_USES_PATH_STYLE_BUCKET"
))
.unwrap(),
region: parse_var(&format!("S3_{bucket_type}_REGION"))
.unwrap(),
url: parse_var(&format!("S3_{bucket_type}_URL")).unwrap(),
access_token: parse_var(&format!(
"S3_{bucket_type}_ACCESS_TOKEN"
))
.unwrap(),
secret: parse_var(&format!("S3_{bucket_type}_SECRET"))
.unwrap(),
}; };
Arc::new( Arc::new(
S3Host::new( S3Host::new(
config_from_env("PUBLIC"), S3BucketConfig {
config_from_env("PRIVATE"), 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(), .unwrap(),
) )
} }
"local" => Arc::new(file_hosting::MockHost::new()), FileHostKind::Local => Arc::new(file_hosting::MockHost::new()),
_ => panic!("Invalid storage backend specified. Aborting startup!"),
}; };
info!("Initializing clickhouse connection"); info!("Initializing clickhouse connection");
@@ -160,8 +154,7 @@ async fn app() -> std::io::Result<()> {
let search_config = search::SearchConfig::new(None); let search_config = search::SearchConfig::new(None);
let stripe_client = let stripe_client = stripe::Client::new(ENV.STRIPE_API_KEY.clone());
stripe::Client::new(dotenvy::var("STRIPE_API_KEY").unwrap());
let anrok_client = anrok::Client::from_env().unwrap(); let anrok_client = anrok::Client::from_env().unwrap();
let email_queue = let email_queue =
@@ -272,7 +265,7 @@ async fn app() -> std::io::Result<()> {
.into_app() .into_app()
.configure(|cfg| app_config(cfg, labrinth_config.clone())) .configure(|cfg| app_config(cfg, labrinth_config.clone()))
}) })
.bind(dotenvy::var("BIND_ADDR").unwrap())? .bind(&ENV.BIND_ADDR)?
.run() .run()
.await .await
} }

View File

@@ -1,6 +1,4 @@
use crate::{ use crate::{env::ENV, models::v2::projects::LegacySideType};
models::v2::projects::LegacySideType, util::env::parse_strings_from_var,
};
use path_util::SafeRelativeUtf8UnixPathBuf; use path_util::SafeRelativeUtf8UnixPathBuf;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
@@ -44,9 +42,7 @@ fn validate_download_url(
return Err(validator::ValidationError::new("invalid URL")); return Err(validator::ValidationError::new("invalid URL"));
} }
let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS") if !ENV.WHITELISTED_MODPACK_DOMAINS.contains(
.unwrap_or_default();
if !domains.contains(
&url.domain() &url.domain()
.ok_or_else(|| validator::ValidationError::new("invalid URL"))? .ok_or_else(|| validator::ValidationError::new("invalid URL"))?
.to_string(), .to_string(),

View File

@@ -12,6 +12,7 @@ use crate::database::models::{
}; };
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::database::{PgPool, PgTransaction}; use crate::database::{PgPool, PgTransaction};
use crate::env::ENV;
use crate::models::billing::{ use crate::models::billing::{
ChargeStatus, ChargeType, PaymentPlatform, Price, PriceDuration, ChargeStatus, ChargeType, PaymentPlatform, Price, PriceDuration,
ProductMetadata, SubscriptionMetadata, SubscriptionStatus, ProductMetadata, SubscriptionMetadata, SubscriptionStatus,
@@ -913,10 +914,10 @@ async fn unprovision_subscriptions(
let res = reqwest::Client::new() let res = reqwest::Client::new()
.post(format!( .post(format!(
"{}/modrinth/v0/servers/{}/suspend", "{}/modrinth/v0/servers/{}/suspend",
dotenvy::var("ARCHON_URL")?, ENV.ARCHON_URL,
server_id server_id
)) ))
.header("X-Master-Key", dotenvy::var("PYRO_API_KEY")?) .header("X-Master-Key", &ENV.PYRO_API_KEY)
.json(&serde_json::json!({ .json(&serde_json::json!({
"reason": if charge.status == ChargeStatus::Cancelled || charge.status == ChargeStatus::Expiring { "reason": if charge.status == ChargeStatus::Cancelled || charge.status == ChargeStatus::Expiring {
"cancelled" "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::models::user_item::DBUser;
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::database::{PgPool, PgTransaction}; use crate::database::{PgPool, PgTransaction};
use crate::env::ENV;
use crate::models::notifications::{NotificationBody, NotificationType}; use crate::models::notifications::{NotificationBody, NotificationType};
use crate::models::v3::notifications::{ use crate::models::v3::notifications::{
NotificationChannel, NotificationDeliveryStatus, NotificationChannel, NotificationDeliveryStatus,
@@ -36,16 +37,16 @@ impl Mailer {
) -> Result<Arc<AsyncSmtpTransport<Tokio1Executor>>, MailError> { ) -> Result<Arc<AsyncSmtpTransport<Tokio1Executor>>, MailError> {
let maybe_transport = match self { let maybe_transport = match self {
Mailer::Uninitialized => { Mailer::Uninitialized => {
let username = dotenvy::var("SMTP_USERNAME")?; let username = &ENV.SMTP_USERNAME;
let password = dotenvy::var("SMTP_PASSWORD")?; let password = &ENV.SMTP_PASSWORD;
let host = dotenvy::var("SMTP_HOST")?; let host = &ENV.SMTP_HOST;
let port = let port = ENV.SMTP_PORT;
dotenvy::var("SMTP_PORT")?.parse::<u16>().unwrap_or(465);
let creds = (!username.is_empty()) let creds = (!username.is_empty()).then(|| {
.then(|| Credentials::new(username, password)); 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, "none" => Tls::None,
"opportunistic_start_tls" => Tls::Opportunistic( "opportunistic_start_tls" => Tls::Opportunistic(
TlsParameters::new(host.to_string())?, TlsParameters::new(host.to_string())?,
@@ -65,7 +66,7 @@ impl Mailer {
}; };
let mut mailer = let mut mailer =
AsyncSmtpTransport::<Tokio1Executor>::relay(&host)? AsyncSmtpTransport::<Tokio1Executor>::relay(host)?
.port(port) .port(port)
.tls(tls_setting); .tls(tls_setting);

View File

@@ -8,6 +8,7 @@ use crate::database::models::{
DBOrganization, DBProject, DBUser, DatabaseError, DBOrganization, DBProject, DBUser, DatabaseError,
}; };
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::env::ENV;
use crate::models::v3::notifications::NotificationBody; use crate::models::v3::notifications::NotificationBody;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::util::error::Context; use crate::util::error::Context;
@@ -96,10 +97,18 @@ pub struct MailingIdentity {
impl MailingIdentity { impl MailingIdentity {
pub fn from_env() -> dotenvy::Result<Self> { pub fn from_env() -> dotenvy::Result<Self> {
Ok(Self { Ok(Self {
from_name: dotenvy::var("SMTP_FROM_NAME")?, from_name: ENV.SMTP_FROM_NAME.clone(),
from_address: dotenvy::var("SMTP_FROM_ADDRESS")?, from_address: ENV.SMTP_FROM_ADDRESS.clone(),
reply_name: dotenvy::var("SMTP_REPLY_TO_NAME").ok(), reply_name: if ENV.SMTP_REPLY_TO_NAME.is_empty() {
reply_address: dotenvy::var("SMTP_REPLY_TO_ADDRESS").ok(), 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 } => { NotificationBody::ResetPassword { flow } => {
let url = format!( let url = format!(
"{}/{}?flow={}", "{}/{}?flow={}",
dotenvy::var("SITE_URL")?, ENV.SITE_URL, ENV.SITE_RESET_PASSWORD_PATH, flow
dotenvy::var("SITE_RESET_PASSWORD_PATH")?,
flow
); );
map.insert(RESETPASSWORD_URL, url); map.insert(RESETPASSWORD_URL, url);
@@ -571,9 +578,7 @@ async fn collect_template_variables(
NotificationBody::VerifyEmail { flow } => { NotificationBody::VerifyEmail { flow } => {
let url = format!( let url = format!(
"{}/{}?flow={}", "{}/{}?flow={}",
dotenvy::var("SITE_URL")?, ENV.SITE_URL, ENV.SITE_VERIFY_EMAIL_PATH, flow
dotenvy::var("SITE_VERIFY_EMAIL_PATH")?,
flow
); );
map.insert(VERIFYEMAIL_URL, url); map.insert(VERIFYEMAIL_URL, url);
@@ -603,11 +608,7 @@ async fn collect_template_variables(
} }
NotificationBody::PaymentFailed { amount, service } => { NotificationBody::PaymentFailed { amount, service } => {
let url = format!( let url = format!("{}/{}", ENV.SITE_URL, ENV.SITE_BILLING_PATH,);
"{}/{}",
dotenvy::var("SITE_URL")?,
dotenvy::var("SITE_BILLING_PATH")?,
);
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert(PAYMENTFAILED_AMOUNT, amount.clone()); map.insert(PAYMENTFAILED_AMOUNT, amount.clone());
@@ -748,8 +749,7 @@ async fn dynamic_email_body(
key: &str, key: &str,
) -> Result<String, ApiError> { ) -> Result<String, ApiError> {
get_or_set_cached_dynamic_html(redis, key, || async { get_or_set_cached_dynamic_html(redis, key, || async {
let site_url = dotenvy::var("SITE_URL") let site_url = &ENV.SITE_URL;
.wrap_internal_err("SITE_URL is not set")?;
let site_url = site_url.trim_end_matches('/'); let site_url = site_url.trim_end_matches('/');
let url = format!("{site_url}/_internal/templates/email/dynamic"); 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::notification_item::NotificationBuilder;
use crate::database::models::thread_item::ThreadMessageBuilder; use crate::database::models::thread_item::ThreadMessageBuilder;
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::env::ENV;
use crate::models::ids::ProjectId; use crate::models::ids::ProjectId;
use crate::models::notifications::NotificationBody; use crate::models::notifications::NotificationBody;
use crate::models::pack::{PackFile, PackFileHash, PackFormat}; use crate::models::pack::{PackFile, PackFileHash, PackFormat};
@@ -454,7 +455,7 @@ impl AutomatedModerationQueue {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let res = client let res = client
.post(format!("{}/v1/fingerprints", dotenvy::var("FLAME_ANVIL_URL")?)) .post(format!("{}/v1/fingerprints", ENV.FLAME_ANVIL_URL))
.json(&serde_json::json!({ .json(&serde_json::json!({
"fingerprints": hashes.iter().filter_map(|x| x.3).collect::<Vec<u32>>() "fingerprints": hashes.iter().filter_map(|x| x.3).collect::<Vec<u32>>()
})) }))
@@ -553,11 +554,11 @@ impl AutomatedModerationQueue {
continue; continue;
} }
let flame_projects = if flame_files.is_empty() { let flame_projects = if flame_files.is_empty() {
Vec::new() Vec::new()
} else { } else {
let res = client let res = client
.post(format!("{}v1/mods", dotenvy::var("FLAME_ANVIL_URL")?)) .post(format!("{}v1/mods", ENV.FLAME_ANVIL_URL))
.json(&serde_json::json!({ .json(&serde_json::json!({
"modIds": flame_files.iter().map(|x| x.1).collect::<Vec<_>>() "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) .insert_many(members.into_iter().map(|x| x.user_id).collect(), &mut transaction, &redis)
.await?; .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( crate::util::webhook::send_slack_project_webhook(
project.inner.id.into(), project.inner.id.into(),
&pool, &pool,
&redis, &redis,
webhook_url, &ENV.MODERATION_SLACK_WEBHOOK,
Some( Some(
format!( format!(
"*<{}/user/AutoMod|AutoMod>* changed project status from *{}* to *Rejected*", "*<{}/user/AutoMod|AutoMod>* changed project status from *{}* to *Rejected*",
dotenvy::var("SITE_URL")?, ENV.SITE_URL,
&project.inner.status.as_friendly_str(), &project.inner.status.as_friendly_str(),
) )
.to_string(), .to_string(),

View File

@@ -1,4 +1,5 @@
use crate::database::PgPool; use crate::database::PgPool;
use crate::env::ENV;
use chrono::{Datelike, Duration, TimeZone, Utc}; use chrono::{Datelike, Duration, TimeZone, Utc};
use eyre::{Context, Result, eyre}; use eyre::{Context, Result, eyre};
use rust_decimal::{Decimal, dec}; use rust_decimal::{Decimal, dec};
@@ -62,11 +63,7 @@ pub async fn process_affiliate_payouts(postgres: &PgPool) -> Result<()> {
.await .await
.wrap_err("failed to fetch charges awaiting affiliate payout")?; .wrap_err("failed to fetch charges awaiting affiliate payout")?;
let default_affiliate_revenue_split = let default_affiliate_revenue_split = ENV.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 ( let (
mut insert_usap_charges, mut insert_usap_charges,

View File

@@ -8,6 +8,7 @@ use serde_json::json;
use crate::{ use crate::{
database::models::payout_item::DBPayout, database::models::payout_item::DBPayout,
env::ENV,
models::payouts::{ models::payouts::{
PayoutMethod, PayoutMethodFee, PayoutMethodType, PayoutStatus, PayoutMethod, PayoutMethodFee, PayoutMethodType, PayoutStatus,
TremendousCurrency, TremendousDetails, TremendousForexResponse, TremendousCurrency, TremendousDetails, TremendousForexResponse,
@@ -210,7 +211,7 @@ pub(super) async fn execute(
"products": [ "products": [
method_id, 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::models::payouts_values_notifications;
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::database::{PgPool, PgTransaction}; use crate::database::{PgPool, PgTransaction};
use crate::env::ENV;
use crate::models::payouts::{ use crate::models::payouts::{
PayoutDecimal, PayoutInterval, PayoutMethod, PayoutMethodType, PayoutDecimal, PayoutInterval, PayoutMethod, PayoutMethodType,
TremendousForexResponse, TremendousForexResponse,
}; };
use crate::models::projects::MonetizationStatus; use crate::models::projects::MonetizationStatus;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::util::env::env_var;
use crate::util::error::Context; use crate::util::error::Context;
use crate::util::webhook::{ use crate::util::webhook::{
PayoutSourceAlertType, send_slack_payout_source_alert_webhook, PayoutSourceAlertType, send_slack_payout_source_alert_webhook,
@@ -76,21 +76,18 @@ impl Default for PayoutsQueue {
} }
pub fn create_muralpay_client() -> Result<muralpay::Client> { pub fn create_muralpay_client() -> Result<muralpay::Client> {
let api_url = env_var("MURALPAY_API_URL")?; Ok(muralpay::Client::new(
let api_key = env_var("MURALPAY_API_KEY")?; &ENV.MURALPAY_API_URL,
let transfer_api_key = env_var("MURALPAY_TRANSFER_API_KEY")?; ENV.MURALPAY_API_KEY.as_str(),
Ok(muralpay::Client::new(api_url, api_key, transfer_api_key)) ENV.MURALPAY_TRANSFER_API_KEY.as_str(),
))
} }
pub fn create_muralpay() -> Result<MuralPayConfig> { pub fn create_muralpay() -> Result<MuralPayConfig> {
let client = create_muralpay_client()?; 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 { Ok(MuralPayConfig {
client, 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 mut creds = self.credential.write().await;
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let combined_key = format!( let combined_key =
"{}:{}", format!("{}:{}", ENV.PAYPAL_CLIENT_ID, ENV.PAYPAL_CLIENT_SECRET);
dotenvy::var("PAYPAL_CLIENT_ID")?,
dotenvy::var("PAYPAL_CLIENT_SECRET")?
);
let formatted_key = format!( let formatted_key = format!(
"Basic {}", "Basic {}",
base64::engine::general_purpose::STANDARD.encode(combined_key) base64::engine::general_purpose::STANDARD.encode(combined_key)
@@ -206,7 +200,7 @@ impl PayoutsQueue {
} }
let credential: PaypalCredential = client 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", "application/json")
.header("Accept-Language", "en_US") .header("Accept-Language", "en_US")
.header("Authorization", formatted_key) .header("Authorization", formatted_key)
@@ -274,7 +268,7 @@ impl PayoutsQueue {
if no_api_prefix.unwrap_or(false) { if no_api_prefix.unwrap_or(false) {
path.to_string() path.to_string()
} else { } else {
format!("{}{path}", dotenvy::var("PAYPAL_API_URL")?) format!("{}{path}", ENV.PAYPAL_API_URL)
}, },
) )
.header( .header(
@@ -355,13 +349,10 @@ impl PayoutsQueue {
) -> Result<X, ApiError> { ) -> Result<X, ApiError> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let mut request = client let mut request = client
.request( .request(method, format!("{}{path}", ENV.TREMENDOUS_API_URL))
method,
format!("{}{path}", dotenvy::var("TREMENDOUS_API_URL")?),
)
.header( .header(
"Authorization", "Authorization",
format!("Bearer {}", dotenvy::var("TREMENDOUS_API_KEY")?), format!("Bearer {}", ENV.TREMENDOUS_API_KEY),
); );
if let Some(body) = body { if let Some(body) = body {
@@ -511,8 +502,8 @@ impl PayoutsQueue {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let res = client let res = client
.get(format!("{}accounts/cash", dotenvy::var("BREX_API_URL")?)) .get(format!("{}accounts/cash", ENV.BREX_API_URL))
.bearer_auth(&dotenvy::var("BREX_API_KEY")?) .bearer_auth(&ENV.BREX_API_KEY)
.send() .send()
.await? .await?
.json::<BrexResponse>() .json::<BrexResponse>()
@@ -538,16 +529,16 @@ impl PayoutsQueue {
pub async fn get_paypal_balance() -> Result<Option<AccountBalance>, ApiError> pub async fn get_paypal_balance() -> Result<Option<AccountBalance>, ApiError>
{ {
let api_username = dotenvy::var("PAYPAL_NVP_USERNAME")?; let api_username = &ENV.PAYPAL_NVP_USERNAME;
let api_password = dotenvy::var("PAYPAL_NVP_PASSWORD")?; let api_password = &ENV.PAYPAL_NVP_PASSWORD;
let api_signature = dotenvy::var("PAYPAL_NVP_SIGNATURE")?; let api_signature = &ENV.PAYPAL_NVP_SIGNATURE;
let mut params = HashMap::new(); let mut params = HashMap::new();
params.insert("METHOD", "GetBalance"); params.insert("METHOD", "GetBalance");
params.insert("VERSION", "204"); params.insert("VERSION", "204");
params.insert("USER", &api_username); params.insert("USER", api_username);
params.insert("PWD", &api_password); params.insert("PWD", api_password);
params.insert("SIGNATURE", &api_signature); params.insert("SIGNATURE", api_signature);
params.insert("RETURNALLCURRENCIES", "1"); params.insert("RETURNALLCURRENCIES", "1");
let endpoint = "https://api-3t.paypal.com/nvp"; let endpoint = "https://api-3t.paypal.com/nvp";
@@ -870,7 +861,7 @@ pub async fn make_aditude_request(
) -> Result<Vec<AditudePoints>, ApiError> { ) -> Result<Vec<AditudePoints>, ApiError> {
let request = reqwest::Client::new() let request = reqwest::Client::new()
.post("https://cloud.aditude.io/api/public/insights/metrics") .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!({ .json(&serde_json::json!({
"metrics": metrics, "metrics": metrics,
"range": range, "range": range,
@@ -1326,25 +1317,25 @@ pub async fn insert_bank_balances_and_webhook(
if inserted { if inserted {
check_balance_with_webhook( check_balance_with_webhook(
"paypal", "paypal",
"PAYPAL_BALANCE_ALERT_THRESHOLD", ENV.PAYPAL_BALANCE_ALERT_THRESHOLD,
paypal_result, paypal_result,
) )
.await?; .await?;
check_balance_with_webhook( check_balance_with_webhook(
"brex", "brex",
"BREX_BALANCE_ALERT_THRESHOLD", ENV.BREX_BALANCE_ALERT_THRESHOLD,
brex_result, brex_result,
) )
.await?; .await?;
check_balance_with_webhook( check_balance_with_webhook(
"tremendous", "tremendous",
"TREMENDOUS_BALANCE_ALERT_THRESHOLD", ENV.TREMENDOUS_BALANCE_ALERT_THRESHOLD,
tremendous_result, tremendous_result,
) )
.await?; .await?;
check_balance_with_webhook( check_balance_with_webhook(
"mural", "mural",
"MURAL_BALANCE_ALERT_THRESHOLD", ENV.MURAL_BALANCE_ALERT_THRESHOLD,
mural_result, mural_result,
) )
.await?; .await?;
@@ -1357,14 +1348,11 @@ pub async fn insert_bank_balances_and_webhook(
async fn check_balance_with_webhook( async fn check_balance_with_webhook(
source: &str, source: &str,
threshold_env_var_name: &str, threshold: u64,
result: Result<Option<AccountBalance>, ApiError>, result: Result<Option<AccountBalance>, ApiError>,
) -> Result<Option<AccountBalance>, ApiError> { ) -> Result<Option<AccountBalance>, ApiError> {
let maybe_threshold = dotenvy::var(threshold_env_var_name) let maybe_threshold = if threshold > 0 { Some(threshold) } else { None };
.ok() let payout_alert_webhook = &ENV.PAYOUT_ALERT_SLACK_WEBHOOK;
.and_then(|x| x.parse::<u64>().ok())
.filter(|x| *x != 0);
let payout_alert_webhook = dotenvy::var("PAYOUT_ALERT_SLACK_WEBHOOK")?;
match &result { match &result {
Ok(Some(account_balance)) => { Ok(Some(account_balance)) => {
@@ -1379,7 +1367,7 @@ async fn check_balance_with_webhook(
threshold, threshold,
current_balance: available, current_balance: available,
}, },
&payout_alert_webhook, payout_alert_webhook,
) )
.await?; .await?;
} }
@@ -1394,7 +1382,7 @@ async fn check_balance_with_webhook(
source: source.to_owned(), source: source.to_owned(),
display_error: error.to_string(), display_error: error.to_string(),
}, },
&payout_alert_webhook, payout_alert_webhook,
) )
.await?; .await?;
} }

View File

@@ -1,13 +1,13 @@
use crate::auth::get_user_from_headers; use crate::auth::get_user_from_headers;
use crate::database::PgPool; use crate::database::PgPool;
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::env::ENV;
use crate::models::analytics::{PageView, Playtime}; use crate::models::analytics::{PageView, Playtime};
use crate::models::pats::Scopes; use crate::models::pats::Scopes;
use crate::queue::analytics::AnalyticsQueue; use crate::queue::analytics::AnalyticsQueue;
use crate::queue::session::AuthQueue; use crate::queue::session::AuthQueue;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::util::date::get_current_tenths_of_ms; 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::{HttpRequest, HttpResponse};
use actix_web::{post, web}; use actix_web::{post, web};
use serde::Deserialize; use serde::Deserialize;
@@ -73,11 +73,10 @@ pub async fn page_view_ingest(
})?; })?;
let url_origin = url.origin().ascii_serialization(); let url_origin = url.origin().ascii_serialization();
let is_valid_url_origin = let is_valid_url_origin = ENV
parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS") .ANALYTICS_ALLOWED_ORIGINS
.unwrap_or_default() .iter()
.iter() .any(|origin| origin == "*" || url_origin == *origin);
.any(|origin| origin == "*" || url_origin == *origin);
if !is_valid_url_origin { if !is_valid_url_origin {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(

View File

@@ -1,6 +1,7 @@
use std::{collections::HashMap, net::Ipv4Addr, sync::Arc}; use std::{collections::HashMap, net::Ipv4Addr, sync::Arc};
use crate::database::PgPool; use crate::database::PgPool;
use crate::env::ENV;
use crate::{ use crate::{
auth::get_user_from_headers, auth::get_user_from_headers,
database::{ database::{
@@ -13,10 +14,7 @@ use crate::{
}, },
queue::{analytics::AnalyticsQueue, session::AuthQueue}, queue::{analytics::AnalyticsQueue, session::AuthQueue},
routes::analytics::FILTERED_HEADERS, routes::analytics::FILTERED_HEADERS,
util::{ util::{date::get_current_tenths_of_ms, error::Context},
date::get_current_tenths_of_ms, env::parse_strings_from_var,
error::Context,
},
}; };
use actix_web::{HttpRequest, delete, get, patch, post, put, web}; use actix_web::{HttpRequest, delete, get, patch, post, put, web};
use ariadne::ids::UserId; use ariadne::ids::UserId;
@@ -70,11 +68,10 @@ async fn ingest_click(
})?; })?;
let url_origin = url.origin().ascii_serialization(); let url_origin = url.origin().ascii_serialization();
let is_valid_url_origin = let is_valid_url_origin = ENV
parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS") .ANALYTICS_ALLOWED_ORIGINS
.unwrap_or_default() .iter()
.iter() .any(|origin| origin == "*" || url_origin == *origin);
.any(|origin| origin == "*" || url_origin == *origin);
if !is_valid_url_origin { if !is_valid_url_origin {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(

View File

@@ -12,6 +12,7 @@ use crate::database::models::{
}; };
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::database::{PgPool, PgTransaction}; use crate::database::{PgPool, PgTransaction};
use crate::env::ENV;
use crate::models::billing::{ use crate::models::billing::{
Charge, ChargeStatus, ChargeType, PaymentPlatform, Price, PriceDuration, Charge, ChargeStatus, ChargeType, PaymentPlatform, Price, PriceDuration,
Product, ProductMetadata, ProductPrice, SubscriptionMetadata, Product, ProductMetadata, ProductPrice, SubscriptionMetadata,
@@ -1437,7 +1438,7 @@ pub async fn active_servers(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
query: web::Query<ActiveServersQuery>, query: web::Query<ActiveServersQuery>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let master_key = dotenvy::var("PYRO_API_KEY")?; let master_key = &ENV.PYRO_API_KEY;
if req if req
.head() .head()
@@ -1626,7 +1627,7 @@ pub async fn stripe_webhook(
if let Ok(event) = Webhook::construct_event( if let Ok(event) = Webhook::construct_event(
&payload, &payload,
stripe_signature, stripe_signature,
&dotenvy::var("STRIPE_WEBHOOK_SECRET")?, &ENV.STRIPE_WEBHOOK_SECRET,
) { ) {
struct PaymentIntentMetadata { struct PaymentIntentMetadata {
pub user_item: crate::database::models::user_item::DBUser, pub user_item: crate::database::models::user_item::DBUser,
@@ -2036,23 +2037,23 @@ pub async fn stripe_webhook(
client client
.post(format!( .post(format!(
"{}/modrinth/v0/servers/{}/unsuspend", "{}/modrinth/v0/servers/{}/unsuspend",
dotenvy::var("ARCHON_URL")?, ENV.ARCHON_URL,
id id
)) ))
.header("X-Master-Key", dotenvy::var("PYRO_API_KEY")?) .header("X-Master-Key", &ENV.PYRO_API_KEY)
.send() .send()
.await? .await?
.error_for_status()?; .error_for_status()?;
client client
.post(format!( .post(format!(
"{}/modrinth/v0/servers/{}/reallocate", "{}/modrinth/v0/servers/{}/reallocate",
dotenvy::var("ARCHON_URL")?, ENV.ARCHON_URL,
id id
)) ))
.header( .header(
"X-Master-Key", "X-Master-Key",
dotenvy::var("PYRO_API_KEY")?, &ENV.PYRO_API_KEY,
) )
.json(&body) .json(&body)
.send() .send()
@@ -2114,9 +2115,9 @@ pub async fn stripe_webhook(
let res = client let res = client
.post(format!( .post(format!(
"{}/modrinth/v0/servers/create", "{}/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!({ .json(&serde_json::json!({
"user_id": to_base62(metadata.user_item.id.0 as u64), "user_id": to_base62(metadata.user_item.id.0 as u64),
"name": server_name, "name": server_name,

View File

@@ -1,6 +1,7 @@
use std::{collections::HashMap, fmt::Write, sync::LazyLock, time::Instant}; use std::{collections::HashMap, fmt::Write, sync::LazyLock, time::Instant};
use crate::database::PgPool; use crate::database::PgPool;
use crate::env::ENV;
use actix_web::{HttpRequest, HttpResponse, get, post, web}; use actix_web::{HttpRequest, HttpResponse, get, post, web};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use eyre::eyre; use eyre::eyre;
@@ -89,7 +90,7 @@ impl DelphiReport {
pool: &PgPool, pool: &PgPool,
redis: &RedisPool, redis: &RedisPool,
) -> Result<(), ApiError> { ) -> Result<(), ApiError> {
let webhook_url = dotenvy::var("DELPHI_SLACK_WEBHOOK")?; let webhook_url = ENV.DELPHI_SLACK_WEBHOOK.clone();
let mut message_header = let mut message_header =
format!("⚠️ Suspicious traces found at {}", self.url); format!("⚠️ Suspicious traces found at {}", self.url);
@@ -115,7 +116,7 @@ impl DelphiReport {
self.project_id, self.project_id,
pool, pool,
redis, redis,
webhook_url, &webhook_url,
Some(message_header), Some(message_header),
) )
.await .await
@@ -317,7 +318,7 @@ pub async fn run(
); );
DELPHI_CLIENT DELPHI_CLIENT
.post(dotenvy::var("DELPHI_URL")?) .post(&ENV.DELPHI_URL)
.json(&serde_json::json!({ .json(&serde_json::json!({
"url": file_data.url, "url": file_data.url,
"project_id": ProjectId(file_data.project_id.0 as u64), "project_id": ProjectId(file_data.project_id.0 as u64),
@@ -407,7 +408,7 @@ async fn issue_type_schema(
&cache_entry &cache_entry
.insert(( .insert((
DELPHI_CLIENT DELPHI_CLIENT
.get(format!("{}/schema", dotenvy::var("DELPHI_URL")?)) .get(format!("{}/schema", ENV.DELPHI_URL))
.send() .send()
.await .await
.and_then(|res| res.error_for_status()) .and_then(|res| res.error_for_status())

View File

@@ -8,6 +8,7 @@ use crate::database::models::flow_item::DBFlow;
use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::{DBUser, DBUserId}; use crate::database::models::{DBUser, DBUserId};
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::env::ENV;
use crate::file_hosting::{FileHost, FileHostPublicity}; use crate::file_hosting::{FileHost, FileHostPublicity};
use crate::models::notifications::NotificationBody; use crate::models::notifications::NotificationBody;
use crate::models::pats::Scopes; use crate::models::pats::Scopes;
@@ -17,7 +18,6 @@ use crate::queue::session::AuthQueue;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::routes::internal::session::issue_session; use crate::routes::internal::session::issue_session;
use crate::util::captcha::check_hcaptcha; use crate::util::captcha::check_hcaptcha;
use crate::util::env::parse_strings_from_var;
use crate::util::error::Context; use crate::util::error::Context;
use crate::util::ext::get_image_ext; use crate::util::ext::get_image_ext;
use crate::util::img::upload_image_optimized; use crate::util::img::upload_image_optimized;
@@ -257,41 +257,41 @@ impl AuthProvider {
&self, &self,
state: String, state: String,
) -> Result<String, AuthenticationError> { ) -> Result<String, AuthenticationError> {
let self_addr = dotenvy::var("SELF_ADDR")?; let self_addr = &ENV.SELF_ADDR;
let raw_redirect_uri = format!("{self_addr}/v2/auth/callback"); let raw_redirect_uri = format!("{self_addr}/v2/auth/callback");
let redirect_uri = urlencoding::encode(&raw_redirect_uri); let redirect_uri = urlencoding::encode(&raw_redirect_uri);
Ok(match self { Ok(match self {
AuthProvider::GitHub => { AuthProvider::GitHub => {
let client_id = dotenvy::var("GITHUB_CLIENT_ID")?; let client_id = &ENV.GITHUB_CLIENT_ID;
format!( 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}", "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 => { AuthProvider::Discord => {
let client_id = dotenvy::var("DISCORD_CLIENT_ID")?; let client_id = &ENV.DISCORD_CLIENT_ID;
format!( format!(
"https://discord.com/api/oauth2/authorize?client_id={client_id}&state={state}&response_type=code&scope=identify%20email&redirect_uri={redirect_uri}" "https://discord.com/api/oauth2/authorize?client_id={client_id}&state={state}&response_type=code&scope=identify%20email&redirect_uri={redirect_uri}"
) )
} }
AuthProvider::Microsoft => { AuthProvider::Microsoft => {
let client_id = dotenvy::var("MICROSOFT_CLIENT_ID")?; let client_id = &ENV.MICROSOFT_CLIENT_ID;
format!( 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}" "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 => { AuthProvider::GitLab => {
let client_id = dotenvy::var("GITLAB_CLIENT_ID")?; let client_id = &ENV.GITLAB_CLIENT_ID;
format!( format!(
"https://gitlab.com/oauth/authorize?client_id={client_id}&state={state}&scope=read_user+profile+email&response_type=code&redirect_uri={redirect_uri}", "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 => { AuthProvider::Google => {
let client_id = dotenvy::var("GOOGLE_CLIENT_ID")?; let client_id = &ENV.GOOGLE_CLIENT_ID;
format!( format!(
"https://accounts.google.com/o/oauth2/v2/auth?client_id={}&state={}&scope={}&response_type=code&redirect_uri={}", "https://accounts.google.com/o/oauth2/v2/auth?client_id={}&state={}&scope={}&response_type=code&redirect_uri={}",
@@ -317,8 +317,8 @@ impl AuthProvider {
) )
} }
AuthProvider::PayPal => { AuthProvider::PayPal => {
let api_url = dotenvy::var("PAYPAL_API_URL")?; let api_url = &ENV.PAYPAL_API_URL;
let client_id = dotenvy::var("PAYPAL_CLIENT_ID")?; let client_id = &ENV.PAYPAL_CLIENT_ID;
let auth_url = if api_url.contains("sandbox") { let auth_url = if api_url.contains("sandbox") {
"sandbox.paypal.com" "sandbox.paypal.com"
@@ -340,8 +340,7 @@ impl AuthProvider {
&self, &self,
query: HashMap<String, String>, query: HashMap<String, String>,
) -> Result<String, AuthenticationError> { ) -> Result<String, AuthenticationError> {
let redirect_uri = let redirect_uri = format!("{}/v2/auth/callback", &ENV.SELF_ADDR);
format!("{}/v2/auth/callback", dotenvy::var("SELF_ADDR")?);
#[derive(Deserialize)] #[derive(Deserialize)]
struct AccessToken { struct AccessToken {
@@ -353,8 +352,8 @@ impl AuthProvider {
let code = query let code = query
.get("code") .get("code")
.ok_or_else(|| AuthenticationError::InvalidCredentials)?; .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
let client_id = dotenvy::var("GITHUB_CLIENT_ID")?; let client_id = ENV.GITHUB_CLIENT_ID.as_str();
let client_secret = dotenvy::var("GITHUB_CLIENT_SECRET")?; let client_secret = ENV.GITHUB_CLIENT_SECRET.as_str();
let url = format!( let url = format!(
"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={client_secret}&code={code}&redirect_uri={redirect_uri}" "https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={client_secret}&code={code}&redirect_uri={redirect_uri}"
@@ -374,12 +373,12 @@ impl AuthProvider {
let code = query let code = query
.get("code") .get("code")
.ok_or_else(|| AuthenticationError::InvalidCredentials)?; .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
let client_id = dotenvy::var("DISCORD_CLIENT_ID")?; let client_id = ENV.DISCORD_CLIENT_ID.as_str();
let client_secret = dotenvy::var("DISCORD_CLIENT_SECRET")?; let client_secret = ENV.DISCORD_CLIENT_SECRET.as_str();
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert("client_id", &*client_id); map.insert("client_id", client_id);
map.insert("client_secret", &*client_secret); map.insert("client_secret", client_secret);
map.insert("code", code); map.insert("code", code);
map.insert("grant_type", "authorization_code"); map.insert("grant_type", "authorization_code");
map.insert("redirect_uri", &redirect_uri); map.insert("redirect_uri", &redirect_uri);
@@ -399,12 +398,12 @@ impl AuthProvider {
let code = query let code = query
.get("code") .get("code")
.ok_or_else(|| AuthenticationError::InvalidCredentials)?; .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
let client_id = dotenvy::var("MICROSOFT_CLIENT_ID")?; let client_id = ENV.MICROSOFT_CLIENT_ID.as_str();
let client_secret = dotenvy::var("MICROSOFT_CLIENT_SECRET")?; let client_secret = ENV.MICROSOFT_CLIENT_SECRET.as_str();
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert("client_id", &*client_id); map.insert("client_id", client_id);
map.insert("client_secret", &*client_secret); map.insert("client_secret", client_secret);
map.insert("code", code); map.insert("code", code);
map.insert("grant_type", "authorization_code"); map.insert("grant_type", "authorization_code");
map.insert("redirect_uri", &redirect_uri); map.insert("redirect_uri", &redirect_uri);
@@ -424,12 +423,12 @@ impl AuthProvider {
let code = query let code = query
.get("code") .get("code")
.ok_or_else(|| AuthenticationError::InvalidCredentials)?; .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
let client_id = dotenvy::var("GITLAB_CLIENT_ID")?; let client_id = ENV.GITLAB_CLIENT_ID.as_str();
let client_secret = dotenvy::var("GITLAB_CLIENT_SECRET")?; let client_secret = ENV.GITLAB_CLIENT_SECRET.as_str();
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert("client_id", &*client_id); map.insert("client_id", client_id);
map.insert("client_secret", &*client_secret); map.insert("client_secret", client_secret);
map.insert("code", code); map.insert("code", code);
map.insert("grant_type", "authorization_code"); map.insert("grant_type", "authorization_code");
map.insert("redirect_uri", &redirect_uri); map.insert("redirect_uri", &redirect_uri);
@@ -449,12 +448,12 @@ impl AuthProvider {
let code = query let code = query
.get("code") .get("code")
.ok_or_else(|| AuthenticationError::InvalidCredentials)?; .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
let client_id = dotenvy::var("GOOGLE_CLIENT_ID")?; let client_id = ENV.GOOGLE_CLIENT_ID.as_str();
let client_secret = dotenvy::var("GOOGLE_CLIENT_SECRET")?; let client_secret = ENV.GOOGLE_CLIENT_SECRET.as_str();
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert("client_id", &*client_id); map.insert("client_id", client_id);
map.insert("client_secret", &*client_secret); map.insert("client_secret", client_secret);
map.insert("code", code); map.insert("code", code);
map.insert("grant_type", "authorization_code"); map.insert("grant_type", "authorization_code");
map.insert("redirect_uri", &redirect_uri); map.insert("redirect_uri", &redirect_uri);
@@ -529,9 +528,9 @@ impl AuthProvider {
let code = query let code = query
.get("code") .get("code")
.ok_or_else(|| AuthenticationError::InvalidCredentials)?; .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
let api_url = dotenvy::var("PAYPAL_API_URL")?; let api_url = ENV.PAYPAL_API_URL.as_str();
let client_id = dotenvy::var("PAYPAL_CLIENT_ID")?; let client_id = ENV.PAYPAL_CLIENT_ID.as_str();
let client_secret = dotenvy::var("PAYPAL_CLIENT_SECRET")?; let client_secret = ENV.PAYPAL_CLIENT_SECRET.as_str();
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert("code", code.as_str()); map.insert("code", code.as_str());
@@ -580,9 +579,7 @@ impl AuthProvider {
.get("x-oauth-client-id") .get("x-oauth-client-id")
.and_then(|x| x.to_str().ok()); .and_then(|x| x.to_str().ok());
if client_id if client_id != Some(ENV.GITHUB_CLIENT_ID.as_str()) {
!= Some(&*dotenvy::var("GITHUB_CLIENT_ID").unwrap())
{
return Err(AuthenticationError::InvalidClientId); return Err(AuthenticationError::InvalidClientId);
} }
} }
@@ -732,7 +729,7 @@ impl AuthProvider {
} }
} }
AuthProvider::Steam => { AuthProvider::Steam => {
let api_key = dotenvy::var("STEAM_API_KEY")?; let api_key = &ENV.STEAM_API_KEY;
#[derive(Deserialize)] #[derive(Deserialize)]
struct SteamResponse { struct SteamResponse {
@@ -797,7 +794,7 @@ impl AuthProvider {
pub country: String, 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() let paypal_user: PayPalUser = reqwest::Client::new()
.get(format!( .get(format!(
@@ -1100,10 +1097,11 @@ pub async fn init(
let url = let url =
url::Url::parse(&info.url).map_err(|_| AuthenticationError::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)?; 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" && domain != "modrinth.com"
{ {
return Err(AuthenticationError::Url); return Err(AuthenticationError::Url);
@@ -1396,9 +1394,9 @@ pub async fn delete_auth_provider(
pub async fn check_sendy_subscription( pub async fn check_sendy_subscription(
email: &str, email: &str,
) -> Result<bool, AuthenticationError> { ) -> Result<bool, AuthenticationError> {
let url = dotenvy::var("SENDY_URL")?; let url = &ENV.SENDY_URL;
let id = dotenvy::var("SENDY_LIST_ID")?; let id = &ENV.SENDY_LIST_ID;
let api_key = dotenvy::var("SENDY_API_KEY")?; let api_key = &ENV.SENDY_API_KEY;
if url.is_empty() || url == "none" { if url.is_empty() || url == "none" {
tracing::info!( tracing::info!(
@@ -1408,9 +1406,9 @@ pub async fn check_sendy_subscription(
} }
let mut form = HashMap::new(); 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("email", email);
form.insert("list_id", &*id); form.insert("list_id", id.as_str());
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let response = client let response = client

View File

@@ -1,5 +1,6 @@
use std::{collections::HashMap, sync::LazyLock}; use std::{collections::HashMap, sync::LazyLock};
use crate::env::ENV;
use actix_web::{get, web}; use actix_web::{get, web};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -28,7 +29,8 @@ static GLOBALS: LazyLock<Globals> = LazyLock::new(|| Globals {
tax_compliance_thresholds: [(2025, 600), (2026, 2000)] tax_compliance_thresholds: [(2025, 600), (2026, 2000)]
.into_iter() .into_iter()
.collect(), .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. /// Gets configured global non-secret variables for this backend instance.

View File

@@ -4,11 +4,11 @@ use crate::database::models::session_item::DBSession;
use crate::database::models::session_item::SessionBuilder; use crate::database::models::session_item::SessionBuilder;
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::database::{PgPool, PgTransaction}; use crate::database::{PgPool, PgTransaction};
use crate::env::ENV;
use crate::models::pats::Scopes; use crate::models::pats::Scopes;
use crate::models::sessions::Session; use crate::models::sessions::Session;
use crate::queue::session::AuthQueue; use crate::queue::session::AuthQueue;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::util::env::parse_var;
use actix_web::http::header::AUTHORIZATION; use actix_web::http::header::AUTHORIZATION;
use actix_web::web::{Data, ServiceConfig, scope}; use actix_web::web::{Data, ServiceConfig, scope};
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web}; use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
@@ -41,7 +41,7 @@ pub async fn get_session_metadata(
req: &HttpRequest, req: &HttpRequest,
) -> Result<SessionMetadata, AuthenticationError> { ) -> Result<SessionMetadata, AuthenticationError> {
let conn_info = req.connection_info().clone(); 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") { if let Some(header) = req.headers().get("CF-Connecting-IP") {
header.to_str().ok() header.to_str().ok()
} else { } else {

View File

@@ -1,8 +1,8 @@
use crate::database::models::DelphiReportIssueDetailsId; use crate::database::models::DelphiReportIssueDetailsId;
use crate::env::ENV;
use crate::file_hosting::FileHostingError; use crate::file_hosting::FileHostingError;
use crate::routes::analytics::{page_view_ingest, playtime_ingest}; use crate::routes::analytics::{page_view_ingest, playtime_ingest};
use crate::util::cors::default_cors; use crate::util::cors::default_cors;
use crate::util::env::parse_strings_from_var;
use actix_cors::Cors; use actix_cors::Cors;
use actix_files::Files; use actix_files::Files;
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
@@ -40,10 +40,7 @@ pub fn root_config(cfg: &mut web::ServiceConfig) {
.wrap( .wrap(
Cors::default() Cors::default()
.allowed_origin_fn(|origin, _req_head| { .allowed_origin_fn(|origin, _req_head| {
let allowed_origins = let allowed_origins = &ENV.ANALYTICS_ALLOWED_ORIGINS;
parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS")
.unwrap_or_default();
allowed_origins.contains(&"*".to_string()) allowed_origins.contains(&"*".to_string())
|| allowed_origins.contains( || allowed_origins.contains(
&origin &origin

View File

@@ -1,6 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::database::PgPool; use crate::database::PgPool;
use crate::env::ENV;
use actix_web::{HttpRequest, HttpResponse, get, web}; use actix_web::{HttpRequest, HttpResponse, get, web};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -94,11 +95,7 @@ pub async fn forge_updates(
} }
let mut response = ForgeUpdates { let mut response = ForgeUpdates {
homepage: format!( homepage: format!("{}/mod/{}", ENV.SITE_URL, id),
"{}/mod/{}",
dotenvy::var("SITE_URL").unwrap_or_default(),
id
),
promos: HashMap::new(), promos: HashMap::new(),
}; };

View File

@@ -4,6 +4,7 @@ use crate::database::PgPool;
use crate::database::models::DBUserId; use crate::database::models::DBUserId;
use crate::database::models::{generate_payout_id, users_compliance}; use crate::database::models::{generate_payout_id, users_compliance};
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::env::ENV;
use crate::models::ids::PayoutId; use crate::models::ids::PayoutId;
use crate::models::pats::Scopes; use crate::models::pats::Scopes;
use crate::models::payouts::{PayoutMethodType, PayoutStatus, Withdrawal}; use crate::models::payouts::{PayoutMethodType, PayoutStatus, Withdrawal};
@@ -212,7 +213,7 @@ pub async fn paypal_webhook(
\"webhook_id\": \"{}\", \"webhook_id\": \"{}\",
\"webhook_event\": {body} \"webhook_event\": {body}
}}", }}",
dotenvy::var("PAYPAL_WEBHOOK_ID")? ENV.PAYPAL_WEBHOOK_ID,
)), )),
None, None,
) )
@@ -322,7 +323,7 @@ pub async fn tremendous_webhook(
})?; })?;
let mut mac: Hmac<Sha256> = Hmac::new_from_slice( let mut mac: Hmac<Sha256> = 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()))?; .map_err(|_| ApiError::Payments("error initializing HMAC".to_string()))?;
mac.update(body.as_bytes()); mac.update(body.as_bytes());
@@ -1114,9 +1115,7 @@ async fn update_compliance_status(
} }
fn tax_compliance_payout_threshold() -> Option<Decimal> { fn tax_compliance_payout_threshold() -> Option<Decimal> {
dotenvy::var("COMPLIANCE_PAYOUT_THRESHOLD") ENV.COMPLIANCE_PAYOUT_THRESHOLD.parse().ok()
.ok()
.and_then(|s| s.parse().ok())
} }
#[derive(Deserialize)] #[derive(Deserialize)]

View File

@@ -12,6 +12,7 @@ use crate::database::models::{
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::database::{self, models as db_models}; use crate::database::{self, models as db_models};
use crate::database::{PgPool, PgTransaction}; use crate::database::{PgPool, PgTransaction};
use crate::env::ENV;
use crate::file_hosting::{FileHost, FileHostPublicity}; use crate::file_hosting::{FileHost, FileHostPublicity};
use crate::models; use crate::models;
use crate::models::ids::{ProjectId, VersionId}; use crate::models::ids::{ProjectId, VersionId};
@@ -427,13 +428,13 @@ pub async fn project_edit(
if status.is_searchable() if status.is_searchable()
&& !project_item.inner.webhook_sent && !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( crate::util::webhook::send_discord_webhook(
project_item.inner.id.into(), project_item.inner.id.into(),
&pool, &pool,
&redis, &redis,
webhook_url, &ENV.PUBLIC_DISCORD_WEBHOOK,
None, None,
) )
.await .await
@@ -451,18 +452,16 @@ pub async fn project_edit(
.await?; .await?;
} }
if user.role.is_mod() if user.role.is_mod() && !ENV.MODERATION_SLACK_WEBHOOK.is_empty() {
&& let Ok(webhook_url) = dotenvy::var("MODERATION_SLACK_WEBHOOK")
{
crate::util::webhook::send_slack_project_webhook( crate::util::webhook::send_slack_project_webhook(
project_item.inner.id.into(), project_item.inner.id.into(),
&pool, &pool,
&redis, &redis,
webhook_url, &ENV.MODERATION_SLACK_WEBHOOK,
Some( Some(
format!( format!(
"*<{}/user/{}|{}>* changed project status from *{}* to *{}*", "*<{}/user/{}|{}>* changed project status from *{}* to *{}*",
dotenvy::var("SITE_URL")?, ENV.SITE_URL,
user.username, user.username,
user.username, user.username,
&project_item.inner.status.as_friendly_str(), &project_item.inner.status.as_friendly_str(),

View File

@@ -7,6 +7,7 @@ use crate::database::models::image_item;
use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::thread_item::ThreadMessageBuilder; use crate::database::models::thread_item::ThreadMessageBuilder;
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::env::ENV;
use crate::file_hosting::{FileHost, FileHostPublicity}; use crate::file_hosting::{FileHost, FileHostPublicity};
use crate::models::ids::{ThreadId, ThreadMessageId}; use crate::models::ids::{ThreadId, ThreadMessageId};
use crate::models::images::{Image, ImageContext}; use crate::models::images::{Image, ImageContext};
@@ -631,9 +632,8 @@ pub async fn message_delete(
let images = let images =
database::DBImage::get_many_contexted(context, &mut transaction) database::DBImage::get_many_contexted(context, &mut transaction)
.await?; .await?;
let cdn_url = dotenvy::var("CDN_URL")?;
for image in images { 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 { if let Some(icon_path) = name {
file_host file_host
.delete_file( .delete_file(

View File

@@ -11,6 +11,7 @@ use crate::database::models::version_item::{
}; };
use crate::database::models::{self, DBOrganization, image_item}; use crate::database::models::{self, DBOrganization, image_item};
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::env::ENV;
use crate::file_hosting::{FileHost, FileHostPublicity}; use crate::file_hosting::{FileHost, FileHostPublicity};
use crate::models::ids::{ImageId, ProjectId, VersionId}; use crate::models::ids::{ImageId, ProjectId, VersionId};
use crate::models::images::{Image, ImageContext}; use crate::models::images::{Image, ImageContext};
@@ -974,7 +975,7 @@ pub async fn upload_file(
version_files.push(VersionFileBuilder { version_files.push(VersionFileBuilder {
filename: file_name.to_string(), filename: file_name.to_string(),
url: format!("{}/{file_path_encode}", dotenvy::var("CDN_URL")?), url: format!("{}/{file_path_encode}", ENV.CDN_URL),
hashes: vec![ hashes: vec![
models::version_item::HashBuilder { models::version_item::HashBuilder {
algorithm: "sha1".to_string(), algorithm: "sha1".to_string(),

View File

@@ -5,6 +5,7 @@ use std::time::Duration;
use crate::database::PgPool; use crate::database::PgPool;
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::env::ENV;
use crate::search::{SearchConfig, UploadSearchProject}; use crate::search::{SearchConfig, UploadSearchProject};
use crate::util::error::Context; use crate::util::error::Context;
use ariadne::ids::base62_impl::to_base62; use ariadne::ids::base62_impl::to_base62;
@@ -43,12 +44,7 @@ pub enum IndexingError {
const MEILISEARCH_CHUNK_SIZE: usize = 50000; // 10_000_000 const MEILISEARCH_CHUNK_SIZE: usize = 50000; // 10_000_000
fn search_operation_timeout() -> std::time::Duration { fn search_operation_timeout() -> std::time::Duration {
let default_ms = 5 * 60 * 1000; // 5 minutes std::time::Duration::from_millis(ENV.SEARCH_OPERATION_TIMEOUT)
let ms = dotenvy::var("SEARCH_OPERATION_TIMEOUT")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(default_ms);
std::time::Duration::from_millis(ms)
} }
pub async fn remove_documents( pub async fn remove_documents(

View File

@@ -1,3 +1,4 @@
use crate::env::ENV;
use crate::models::projects::SearchRequest; use crate::models::projects::SearchRequest;
use crate::{models::error::ApiError, search::indexing::IndexingError}; use crate::{models::error::ApiError, search::indexing::IndexingError};
use actix_web::HttpResponse; use actix_web::HttpResponse;
@@ -134,26 +135,11 @@ impl SearchConfig {
// Panics if the environment variables are not set, // Panics if the environment variables are not set,
// but these are already checked for on startup. // but these are already checked for on startup.
pub fn new(meta_namespace: Option<String>) -> Self { pub fn new(meta_namespace: Option<String>) -> 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::<Vec<String>>();
let key =
dotenvy::var("MEILISEARCH_KEY").expect("MEILISEARCH_KEY not set");
Self { Self {
addresses, addresses: ENV.MEILISEARCH_WRITE_ADDRS.0.clone(),
key, key: ENV.MEILISEARCH_KEY.clone(),
meta_namespace: meta_namespace.unwrap_or_default(), meta_namespace: meta_namespace.unwrap_or_default(),
read_lb_address, read_lb_address: ENV.MEILISEARCH_READ_ADDR.clone(),
} }
} }

View File

@@ -3,6 +3,7 @@ use super::{
environment::LocalService, environment::LocalService,
}; };
use crate::LabrinthConfig; use crate::LabrinthConfig;
use crate::env::ENV;
use actix_web::{App, dev::ServiceResponse, test}; use actix_web::{App, dev::ServiceResponse, test};
use async_trait::async_trait; use async_trait::async_trait;
use std::rc::Rc; use std::rc::Rc;
@@ -46,10 +47,7 @@ impl Api for ApiV2 {
async fn reset_search_index(&self) -> ServiceResponse { async fn reset_search_index(&self) -> ServiceResponse {
let req = actix_web::test::TestRequest::post() let req = actix_web::test::TestRequest::post()
.uri("/v2/admin/_force_reindex") .uri("/v2/admin/_force_reindex")
.append_header(( .append_header(("Modrinth-Admin", ENV.LABRINTH_ADMIN_KEY.clone()))
"Modrinth-Admin",
dotenvy::var("LABRINTH_ADMIN_KEY").unwrap(),
))
.to_request(); .to_request();
self.call(req).await self.call(req).await
} }

View File

@@ -3,6 +3,7 @@ use super::{
environment::LocalService, environment::LocalService,
}; };
use crate::LabrinthConfig; use crate::LabrinthConfig;
use crate::env::ENV;
use actix_web::{App, dev::ServiceResponse, test}; use actix_web::{App, dev::ServiceResponse, test};
use async_trait::async_trait; use async_trait::async_trait;
use std::rc::Rc; use std::rc::Rc;
@@ -51,10 +52,7 @@ impl Api for ApiV3 {
async fn reset_search_index(&self) -> ServiceResponse { async fn reset_search_index(&self) -> ServiceResponse {
let req = actix_web::test::TestRequest::post() let req = actix_web::test::TestRequest::post()
.uri("/_internal/admin/_force_reindex") .uri("/_internal/admin/_force_reindex")
.append_header(( .append_header(("Modrinth-Admin", ENV.LABRINTH_ADMIN_KEY.clone()))
"Modrinth-Admin",
dotenvy::var("LABRINTH_ADMIN_KEY").unwrap(),
))
.to_request(); .to_request();
self.call(req).await self.call(req).await
} }

View File

@@ -1,6 +1,7 @@
use crate::database::PgPool; use crate::database::PgPool;
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::database::{MIGRATOR, ReadOnlyPgPool}; use crate::database::{MIGRATOR, ReadOnlyPgPool};
use crate::env::ENV;
use crate::search; use crate::search;
use sqlx::postgres::PgPoolOptions; use sqlx::postgres::PgPoolOptions;
use std::time::Duration; use std::time::Duration;
@@ -57,15 +58,14 @@ impl TemporaryDatabase {
let temp_database_name = generate_random_name("labrinth_tests_db_"); let temp_database_name = generate_random_name("labrinth_tests_db_");
println!("Creating temporary database: {}", &temp_database_name); println!("Creating temporary database: {}", &temp_database_name);
let database_url = let database_url = &ENV.DATABASE_URL;
dotenvy::var("DATABASE_URL").expect("No database URL");
// Create the temporary (and template database, if needed) // 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 // Pool to the temporary database
let mut temporary_url = 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)); temporary_url.set_path(&format!("/{}", &temp_database_name));
let temp_db_url = temporary_url.to_string(); let temp_db_url = temporary_url.to_string();
@@ -139,10 +139,8 @@ impl TemporaryDatabase {
} }
// Switch to template // Switch to template
let url = let mut template_url = Url::parse(&ENV.DATABASE_URL)
dotenvy::var("DATABASE_URL").expect("No database URL"); .expect("Invalid database URL");
let mut template_url =
Url::parse(&url).expect("Invalid database URL");
template_url.set_path(&format!("/{TEMPLATE_DATABASE_NAME}")); template_url.set_path(&format!("/{TEMPLATE_DATABASE_NAME}"));
let pool = sqlx::PgPool::connect(template_url.as_str()) let pool = sqlx::PgPool::connect(template_url.as_str())
@@ -234,11 +232,10 @@ impl TemporaryDatabase {
// If a temporary db is created, it must be cleaned up with cleanup. // 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. // 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) { pub async fn cleanup(mut self) {
let database_url = let database_url = &ENV.DATABASE_URL;
dotenvy::var("DATABASE_URL").expect("No database URL");
self.pool.close().await; self.pool.close().await;
self.pool = sqlx::PgPool::connect(&database_url) self.pool = sqlx::PgPool::connect(database_url)
.await .await
.map(PgPool::from) .map(PgPool::from)
.expect("Connection to main database failed"); .expect("Connection to main database failed");

View File

@@ -1,8 +1,9 @@
use crate::env::ENV;
use crate::queue::email::EmailQueue; use crate::queue::email::EmailQueue;
use crate::util::anrok; use crate::util::anrok;
use crate::util::gotenberg::GotenbergClient; use crate::util::gotenberg::GotenbergClient;
use crate::{LabrinthConfig, file_hosting}; use crate::{LabrinthConfig, file_hosting};
use crate::{check_env_vars, clickhouse}; use crate::{clickhouse, env};
use std::sync::Arc; use std::sync::Arc;
pub mod api_common; pub mod api_common;
@@ -22,12 +23,8 @@ pub mod search;
// If making a test, you should probably use environment::TestEnvironment::build() (which calls this) // If making a test, you should probably use environment::TestEnvironment::build() (which calls this)
pub async fn setup(db: &database::TemporaryDatabase) -> LabrinthConfig { pub async fn setup(db: &database::TemporaryDatabase) -> LabrinthConfig {
println!("Setting up labrinth config"); println!("Setting up labrinth config");
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
env::init().expect("failed to initialize environment variables");
if check_env_vars() {
println!("Some environment variables are missing!");
}
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
@@ -39,8 +36,7 @@ pub async fn setup(db: &database::TemporaryDatabase) -> LabrinthConfig {
Arc::new(file_hosting::MockHost::new()); Arc::new(file_hosting::MockHost::new());
let mut clickhouse = clickhouse::init_client().await.unwrap(); let mut clickhouse = clickhouse::init_client().await.unwrap();
let stripe_client = let stripe_client = stripe::Client::new(ENV.STRIPE_API_KEY.clone());
stripe::Client::new(dotenvy::var("STRIPE_API_KEY").unwrap());
let anrok_client = anrok::Client::from_env().unwrap(); let anrok_client = anrok::Client::from_env().unwrap();
let email_queue = let email_queue =

View File

@@ -7,6 +7,9 @@ use serde_with::{DisplayFromStr, serde_as};
use thiserror::Error; use thiserror::Error;
use tracing::trace; use tracing::trace;
use crate::env::ENV;
use crate::routes::ApiError;
pub fn transaction_id_stripe_pi(pi: &stripe::PaymentIntentId) -> String { pub fn transaction_id_stripe_pi(pi: &stripe::PaymentIntentId) -> String {
format!("stripe:charge:{pi}") format!("stripe:charge:{pi}")
} }
@@ -154,19 +157,14 @@ pub struct Client {
} }
impl Client { impl Client {
pub fn from_env() -> Result<Self, dotenvy::Error> { pub fn from_env() -> Result<Self, ApiError> {
let api_key = dotenvy::var("ANROK_API_KEY")?;
let api_url = dotenvy::var("ANROK_API_URL")?
.trim_start_matches('/')
.to_owned();
Ok(Self { Ok(Self {
client: reqwest::Client::builder() client: reqwest::Client::builder()
.user_agent("Modrinth") .user_agent("Modrinth")
.build() .build()
.expect("AnrokClient to build"), .expect("AnrokClient to build"),
api_key, api_key: ENV.ANROK_API_KEY.clone(),
api_url, api_url: ENV.ANROK_API_URL.trim_start_matches('/').to_owned(),
}) })
} }

View File

@@ -2,6 +2,7 @@ use reqwest::header::HeaderName;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::env::ENV;
use crate::routes::ApiError; use crate::routes::ApiError;
const X_MASTER_KEY: HeaderName = HeaderName::from_static("x-master-key"); const X_MASTER_KEY: HeaderName = HeaderName::from_static("x-master-key");
@@ -42,13 +43,12 @@ impl ArchonClient {
pub fn from_env() -> Result<Self, ApiError> { pub fn from_env() -> Result<Self, ApiError> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let base_url = let base_url = ENV.ARCHON_URL.trim_end_matches('/').to_owned();
dotenvy::var("ARCHON_URL")?.trim_end_matches('/').to_owned();
Ok(Self { Ok(Self {
client, client,
base_url, base_url,
pyro_api_key: dotenvy::var("PYRO_API_KEY")?, pyro_api_key: ENV.PYRO_API_KEY.clone(),
}) })
} }

View File

@@ -1,4 +1,5 @@
use crate::database::models::{DBUserId, users_compliance::FormType}; use crate::database::models::{DBUserId, users_compliance::FormType};
use crate::env::ENV;
use crate::routes::ApiError; use crate::routes::ApiError;
use ariadne::ids::base62_impl::to_base62; use ariadne::ids::base62_impl::to_base62;
use chrono::Datelike; use chrono::Datelike;
@@ -131,10 +132,10 @@ fn team_request(
method: reqwest::Method, method: reqwest::Method,
route: &str, route: &str,
) -> Result<(reqwest::RequestBuilder, String), ApiError> { ) -> Result<(reqwest::RequestBuilder, String), ApiError> {
let key = dotenvy::var("AVALARA_1099_API_KEY")?; let key = &ENV.AVALARA_1099_API_KEY;
let url = dotenvy::var("AVALARA_1099_API_URL")?; let url = &ENV.AVALARA_1099_API_URL;
let team = dotenvy::var("AVALARA_1099_API_TEAM_ID")?; let team = &ENV.AVALARA_1099_API_TEAM_ID;
let company = dotenvy::var("AVALARA_1099_COMPANY_ID")?; let company = &ENV.AVALARA_1099_COMPANY_ID;
let url = url.trim_end_matches('/'); let url = url.trim_end_matches('/');
@@ -144,8 +145,8 @@ fn team_request(
client client
.request(method, format!("{url}/v1/{team}{route}")) .request(method, format!("{url}/v1/{team}{route}"))
.header(reqwest::header::USER_AGENT, "Modrinth") .header(reqwest::header::USER_AGENT, "Modrinth")
.bearer_auth(&key), .bearer_auth(key),
company, company.to_string(),
)) ))
} }

View File

@@ -1,5 +1,5 @@
use crate::env::ENV;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::util::env::parse_var;
use actix_web::HttpRequest; use actix_web::HttpRequest;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
@@ -8,7 +8,7 @@ pub async fn check_hcaptcha(
req: &HttpRequest, req: &HttpRequest,
challenge: &str, challenge: &str,
) -> Result<bool, ApiError> { ) -> Result<bool, ApiError> {
let secret = dotenvy::var("HCAPTCHA_SECRET")?; let secret = &ENV.HCAPTCHA_SECRET;
if secret.is_empty() || secret == "none" { if secret.is_empty() || secret == "none" {
tracing::info!("hCaptcha secret not set, skipping check"); tracing::info!("hCaptcha secret not set, skipping check");
@@ -16,7 +16,7 @@ pub async fn check_hcaptcha(
} }
let conn_info = req.connection_info().clone(); let 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") { if let Some(header) = req.headers().get("CF-Connecting-IP") {
header.to_str().ok() header.to_str().ok()
} else { } else {
@@ -38,7 +38,7 @@ pub async fn check_hcaptcha(
let mut form = HashMap::new(); let mut form = HashMap::new();
form.insert("response", challenge); form.insert("response", challenge);
form.insert("secret", &*secret); form.insert("secret", secret);
form.insert("remoteip", ip_addr); form.insert("remoteip", ip_addr);
let val: Response = client let val: Response = client

View File

@@ -1,17 +0,0 @@
use std::str::FromStr;
use eyre::{Context, eyre};
pub fn env_var(key: &str) -> eyre::Result<String> {
dotenvy::var(key)
.wrap_err_with(|| eyre!("missing environment variable `{key}`"))
}
pub fn parse_var<T: FromStr>(var: &str) -> Option<T> {
dotenvy::var(var).ok().and_then(|i| i.parse().ok())
}
pub fn parse_strings_from_var(var: &'static str) -> Option<Vec<String>> {
dotenvy::var(var)
.ok()
.and_then(|s| serde_json::from_str::<Vec<String>>(&s).ok())
}

View File

@@ -1,8 +1,8 @@
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::env::ENV;
use crate::models::ids::PayoutId; use crate::models::ids::PayoutId;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::routes::internal::gotenberg::{GotenbergDocument, GotenbergError}; use crate::routes::internal::gotenberg::{GotenbergDocument, GotenbergError};
use crate::util::env::env_var;
use crate::util::error::Context; use crate::util::error::Context;
use actix_web::http::header::HeaderName; use actix_web::http::header::HeaderName;
use chrono::{DateTime, Datelike, Utc}; use chrono::{DateTime, Datelike, Utc};
@@ -71,15 +71,14 @@ impl GotenbergClient {
.build() .build()
.wrap_err("failed to build reqwest client")?; .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 { Ok(Self {
client, client,
gotenberg_url: gotenberg_url.trim_end_matches('/').to_owned(), gotenberg_url: ENV.GOTENBERG_URL.trim_end_matches('/').to_owned(),
site_url: site_url.trim_end_matches('/').to_owned(), site_url: ENV.SITE_URL.trim_end_matches('/').to_owned(),
callback_base: callback_base.trim_end_matches('/').to_owned(), callback_base: ENV
.GOTENBERG_CALLBACK_BASE
.trim_end_matches('/')
.to_owned(),
redis, redis,
}) })
} }
@@ -189,12 +188,7 @@ impl GotenbergClient {
self.generate_payment_statement(statement).await?; self.generate_payment_statement(statement).await?;
let timeout_ms = env_var("GOTENBERG_TIMEOUT") let timeout_ms = ENV.GOTENBERG_TIMEOUT;
.map_err(ApiError::Internal)?
.parse::<u64>()
.wrap_internal_err(
"`GOTENBERG_TIMEOUT` is not a valid number of milliseconds",
)?;
let [_key, document] = tokio::time::timeout( let [_key, document] = tokio::time::timeout(
Duration::from_millis(timeout_ms), Duration::from_millis(timeout_ms),

View File

@@ -1,47 +1,33 @@
use actix_web::guard::GuardContext; use actix_web::guard::GuardContext;
use actix_web::http::header::X_FORWARDED_FOR; use actix_web::http::header::X_FORWARDED_FOR;
use crate::env::ENV;
pub const ADMIN_KEY_HEADER: &str = "Modrinth-Admin"; pub const ADMIN_KEY_HEADER: &str = "Modrinth-Admin";
pub const MEDAL_KEY_HEADER: &str = "X-Medal-Access-Key"; pub const MEDAL_KEY_HEADER: &str = "X-Medal-Access-Key";
pub const EXTERNAL_NOTIFICATION_KEY_HEADER: &str = "External-Notification-Key"; pub const EXTERNAL_NOTIFICATION_KEY_HEADER: &str = "External-Notification-Key";
pub fn admin_key_guard(ctx: &GuardContext) -> bool { 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() ctx.head()
.headers() .headers()
.get(ADMIN_KEY_HEADER) .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 { pub fn medal_key_guard(ctx: &GuardContext) -> bool {
let maybe_medal_key = dotenvy::var("LABRINTH_MEDAL_KEY").ok(); ctx.head()
.headers()
match maybe_medal_key { .get(MEDAL_KEY_HEADER)
None => false, .is_some_and(|it| it.as_bytes() == ENV.LABRINTH_MEDAL_KEY.as_bytes())
Some(medal_key) => ctx
.head()
.headers()
.get(MEDAL_KEY_HEADER)
.is_some_and(|it| it.as_bytes() == medal_key.as_bytes()),
}
} }
pub fn external_notification_key_guard(ctx: &GuardContext) -> bool { pub fn external_notification_key_guard(ctx: &GuardContext) -> bool {
let maybe_external_notification_key = ctx.head()
dotenvy::var("LABRINTH_EXTERNAL_NOTIFICATION_KEY").ok(); .headers()
.get(EXTERNAL_NOTIFICATION_KEY_HEADER)
match maybe_external_notification_key { .is_some_and(|it| {
None => false, it.as_bytes() == ENV.LABRINTH_EXTERNAL_NOTIFICATION_KEY.as_bytes()
Some(external_notification_key) => ctx })
.head()
.headers()
.get(EXTERNAL_NOTIFICATION_KEY_HEADER)
.is_some_and(|it| {
it.as_bytes() == external_notification_key.as_bytes()
}),
}
} }
pub fn internal_network_guard(ctx: &GuardContext) -> bool { pub fn internal_network_guard(ctx: &GuardContext) -> bool {

View File

@@ -1,6 +1,7 @@
use crate::database::models::image_item; use crate::database::models::image_item;
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::database::{self, PgTransaction}; use crate::database::{self, PgTransaction};
use crate::env::ENV;
use crate::file_hosting::{FileHost, FileHostPublicity}; use crate::file_hosting::{FileHost, FileHostPublicity};
use crate::models::images::ImageContext; use crate::models::images::ImageContext;
use crate::routes::ApiError; 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::<String>(); let hash = sha1::Sha1::digest(&bytes).encode_hex::<String>();
let (processed_image, processed_image_ext) = process_image( let (processed_image, processed_image_ext) = process_image(
@@ -175,7 +176,7 @@ pub async fn delete_old_images(
publicity: FileHostPublicity, publicity: FileHostPublicity,
file_host: &dyn FileHost, file_host: &dyn FileHost,
) -> Result<(), ApiError> { ) -> Result<(), ApiError> {
let cdn_url = dotenvy::var("CDN_URL")?; let cdn_url = &ENV.CDN_URL;
let cdn_url_start = format!("{cdn_url}/"); let cdn_url_start = format!("{cdn_url}/");
if let Some(image_url) = image_url { if let Some(image_url) = image_url {
let name = image_url.split(&cdn_url_start).nth(1); let name = image_url.split(&cdn_url_start).nth(1);

View File

@@ -6,7 +6,6 @@ pub mod bitflag;
pub mod captcha; pub mod captcha;
pub mod cors; pub mod cors;
pub mod date; pub mod date;
pub mod env;
pub mod error; pub mod error;
pub mod ext; pub mod ext;
pub mod gotenberg; pub mod gotenberg;

View File

@@ -1,6 +1,5 @@
use crate::database::redis::RedisPool;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::util::env::parse_var; use crate::{database::redis::RedisPool, env::ENV};
use actix_web::{ use actix_web::{
Error, ResponseError, Error, ResponseError,
body::{EitherBody, MessageBody}, body::{EitherBody, MessageBody},
@@ -134,14 +133,13 @@ pub async fn rate_limit_middleware(
.clone(); .clone();
if let Some(key) = req.headers().get("x-ratelimit-key") if let Some(key) = req.headers().get("x-ratelimit-key")
&& key.to_str().ok() && key.to_str().ok() == Some(&ENV.RATE_LIMIT_IGNORE_KEY)
== dotenvy::var("RATE_LIMIT_IGNORE_KEY").ok().as_deref()
{ {
return Ok(next.call(req).await?.map_into_left_body()); return Ok(next.call(req).await?.map_into_left_body());
} }
let conn_info = req.connection_info().clone(); 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") { if let Some(header) = req.headers().get("CF-Connecting-IP") {
header.to_str().ok() header.to_str().ok()
} else { } else {

View File

@@ -1,8 +1,8 @@
use crate::database::PgPool;
use crate::database::models::legacy_loader_fields::MinecraftGameVersion; use crate::database::models::legacy_loader_fields::MinecraftGameVersion;
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::models::ids::ProjectId; use crate::models::ids::ProjectId;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::{database::PgPool, env::ENV};
use ariadne::ids::base62_impl::to_base62; use ariadne::ids::base62_impl::to_base62;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::Serialize; use serde::Serialize;
@@ -69,7 +69,7 @@ async fn get_webhook_metadata(
name: organization.name, name: organization.name,
url: format!( url: format!(
"{}/organization/{}", "{}/organization/{}",
dotenvy::var("SITE_URL").unwrap_or_default(), ENV.SITE_URL,
to_base62(organization.id.0 as u64) to_base62(organization.id.0 as u64)
), ),
icon_url: organization.icon_url, icon_url: organization.icon_url,
@@ -95,7 +95,7 @@ async fn get_webhook_metadata(
owner = Some(WebhookAuthor { owner = Some(WebhookAuthor {
url: format!( url: format!(
"{}/user/{}", "{}/user/{}",
dotenvy::var("SITE_URL").unwrap_or_default(), ENV.SITE_URL,
to_base62(user.id.0 as u64) to_base62(user.id.0 as u64)
), ),
name: user.username, name: user.username,
@@ -142,7 +142,7 @@ async fn get_webhook_metadata(
Ok(Some(WebhookMetadata { Ok(Some(WebhookMetadata {
project_url: format!( project_url: format!(
"{}/{}/{}", "{}/{}/{}",
dotenvy::var("SITE_URL").unwrap_or_default(), ENV.SITE_URL,
project_type, project_type,
to_base62(project.inner.id.0 as u64) to_base62(project.inner.id.0 as u64)
), ),
@@ -251,7 +251,7 @@ pub async fn send_slack_project_webhook(
project_id: ProjectId, project_id: ProjectId,
pool: &PgPool, pool: &PgPool,
redis: &RedisPool, redis: &RedisPool,
webhook_url: String, webhook_url: &str,
message: Option<String>, message: Option<String>,
) -> Result<(), ApiError> { ) -> Result<(), ApiError> {
let metadata = get_webhook_metadata(project_id, pool, redis).await?; 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(); let client = reqwest::Client::new();
client client
.post(&webhook_url) .post(webhook_url)
.json(&serde_json::json!({ .json(&serde_json::json!({
"blocks": blocks, "blocks": blocks,
})) }))
@@ -422,7 +422,7 @@ pub async fn send_discord_webhook(
project_id: ProjectId, project_id: ProjectId,
pool: &PgPool, pool: &PgPool,
redis: &RedisPool, redis: &RedisPool,
webhook_url: String, webhook_url: &str,
message: Option<String>, message: Option<String>,
) -> Result<(), ApiError> { ) -> Result<(), ApiError> {
let metadata = get_webhook_metadata(project_id, pool, redis).await?; 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(); let client = reqwest::Client::new();
client client
.post(&webhook_url) .post(webhook_url)
.json(&DiscordWebhook { .json(&DiscordWebhook {
avatar_url: Some( avatar_url: Some(
"https://cdn.modrinth.com/Modrinth_Dark_Logo.png" "https://cdn.modrinth.com/Modrinth_Dark_Logo.png"