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
CDN_URL=file:///tmp/modrinth
LABRINTH_ADMIN_KEY=feedbeef
LABRINTH_MEDAL_KEY=
LABRINTH_EXTERNAL_NOTIFICATION_KEY=beeffeed
RATE_LIMIT_IGNORE_KEY=feedbeef
DATABASE_URL=postgresql://labrinth:labrinth@labrinth-postgres/labrinth
@@ -152,6 +154,6 @@ ARCHON_URL=none
MURALPAY_API_URL=https://api.muralpay.com
MURALPAY_API_KEY=none
MURALPAY_TRANSFER_API_KEY=none
MURALPAY_SOURCE_ACCOUNT_ID=none
MURALPAY_SOURCE_ACCOUNT_ID=00000000-0000-0000-0000-000000000000
DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1

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
CDN_URL=file:///tmp/modrinth
LABRINTH_ADMIN_KEY=feedbeef
LABRINTH_MEDAL_KEY=
LABRINTH_EXTERNAL_NOTIFICATION_KEY=beeffeed
RATE_LIMIT_IGNORE_KEY=feedbeef
@@ -163,6 +164,6 @@ ARCHON_URL=none
MURALPAY_API_URL=https://api-staging.muralpay.com
MURALPAY_API_KEY=none
MURALPAY_TRANSFER_API_KEY=none
MURALPAY_SOURCE_ACCOUNT_ID=none
MURALPAY_SOURCE_ACCOUNT_ID=00000000-0000-0000-0000-000000000000
DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -1,3 +1,5 @@
use std::str::FromStr;
use async_trait::async_trait;
use thiserror::Error;
@@ -63,3 +65,25 @@ pub trait FileHost {
file_publicity: FileHostPublicity,
) -> Result<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::database::{PgPool, ReadOnlyPgPool};
use crate::env::ENV;
use crate::queue::billing::{index_billing, index_subscriptions};
use crate::queue::moderation::AutomatedModerationQueue;
use crate::util::anrok;
use crate::util::archon::ArchonClient;
use crate::util::env::{parse_strings_from_var, parse_var};
use crate::util::ratelimit::{AsyncRateLimiter, GCRAParameters};
use sync::friends::handle_pubsub;
@@ -28,6 +28,7 @@ pub mod auth;
pub mod background_task;
pub mod clickhouse;
pub mod database;
pub mod env;
pub mod file_hosting;
pub mod models;
pub mod queue;
@@ -83,10 +84,7 @@ pub fn app_setup(
gotenberg_client: GotenbergClient,
enable_background_tasks: bool,
) -> LabrinthConfig {
info!(
"Starting labrinth on {}",
dotenvy::var("BIND_ADDR").unwrap()
);
info!("Starting labrinth on {}", &ENV.BIND_ADDR);
let automated_moderation_queue =
web::Data::new(AutomatedModerationQueue::default());
@@ -112,9 +110,8 @@ pub fn app_setup(
if enable_background_tasks {
// The interval in seconds at which the local database is indexed
// for searching. Defaults to 1 hour if unset.
let local_index_interval = Duration::from_secs(
parse_var("LOCAL_INDEX_INTERVAL").unwrap_or(3600),
);
let local_index_interval =
Duration::from_secs(ENV.LOCAL_INDEX_INTERVAL);
let pool_ref = pool.clone();
let search_config_ref = search_config.clone();
let redis_pool_ref = redis_pool.clone();
@@ -142,9 +139,8 @@ pub fn app_setup(
}
});
let version_index_interval = Duration::from_secs(
parse_var("VERSION_INDEX_INTERVAL").unwrap_or(1800),
);
let version_index_interval =
Duration::from_secs(ENV.VERSION_INDEX_INTERVAL);
let pool_ref = pool.clone();
let redis_pool_ref = redis_pool.clone();
scheduler.run(version_index_interval, move || {
@@ -349,188 +345,3 @@ pub fn utoipa_app_config(
.configure(routes::v3::utoipa_config)
.configure(routes::internal::utoipa_config);
}
// This is so that env vars not used immediately don't panic at runtime
pub fn check_env_vars() -> bool {
let mut failed = false;
fn check_var<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 clap::Parser;
use labrinth::app_config;
use labrinth::background_task::BackgroundTask;
use labrinth::database::redis::RedisPool;
use labrinth::file_hosting::{S3BucketConfig, S3Host};
use labrinth::env::ENV;
use labrinth::file_hosting::{FileHostKind, S3BucketConfig, S3Host};
use labrinth::queue::email::EmailQueue;
use labrinth::search;
use labrinth::util::anrok;
use labrinth::util::env::parse_var;
use labrinth::util::gotenberg::GotenbergClient;
use labrinth::util::ratelimit::rate_limit_middleware;
use labrinth::utoipa_app_config;
use labrinth::{check_env_vars, clickhouse, database, file_hosting};
use labrinth::{app_config, env};
use labrinth::{clickhouse, database, file_hosting};
use std::ffi::CStr;
use std::sync::Arc;
use tracing::{Instrument, error, info, info_span};
use tracing::{Instrument, info, info_span};
use tracing_actix_web::TracingLogger;
use utoipa::OpenApi;
use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme};
@@ -58,11 +58,7 @@ fn main() -> std::io::Result<()> {
color_eyre::install().expect("failed to install `color-eyre`");
dotenvy::dotenv().ok();
modrinth_util::log::init().expect("failed to initialize logging");
if check_env_vars() {
error!("Some environment variables are missing!");
std::process::exit(1);
}
env::init().expect("failed to initialize environment variables");
// Sentry must be set up before the async runtime is started
// <https://docs.sentry.io/platforms/rust/guides/actix-web/>
@@ -70,11 +66,8 @@ fn main() -> std::io::Result<()> {
// Has no effect if not set.
let sentry = sentry::init(sentry::ClientOptions {
release: sentry::release_name!(),
traces_sample_rate: dotenvy::var("SENTRY_TRACES_SAMPLE_RATE")
.unwrap()
.parse()
.expect("failed to parse `SENTRY_TRACES_SAMPLE_RATE` as number"),
environment: Some(dotenvy::var("SENTRY_ENVIRONMENT").unwrap().into()),
traces_sample_rate: ENV.SENTRY_TRACES_SAMPLE_RATE,
environment: Some((&ENV.SENTRY_ENVIRONMENT).into()),
..Default::default()
});
if sentry.is_enabled() {
@@ -99,10 +92,7 @@ async fn app() -> std::io::Result<()> {
.unwrap();
if args.run_background_task.is_none() {
info!(
"Starting labrinth on {}",
dotenvy::var("BIND_ADDR").unwrap()
);
info!("Starting labrinth on {}", &ENV.BIND_ADDR);
if !args.no_migrations {
database::check_for_migrations()
@@ -119,40 +109,44 @@ async fn app() -> std::io::Result<()> {
// Redis connector
let redis_pool = RedisPool::new("");
let storage_backend =
dotenvy::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string());
let storage_backend = ENV.STORAGE_BACKEND;
let file_host: Arc<dyn file_hosting::FileHost + Send + Sync> =
match storage_backend.as_str() {
"s3" => {
let config_from_env = |bucket_type| S3BucketConfig {
name: parse_var(&format!("S3_{bucket_type}_BUCKET_NAME"))
.unwrap(),
uses_path_style: parse_var(&format!(
"S3_{bucket_type}_USES_PATH_STYLE_BUCKET"
))
.unwrap(),
region: parse_var(&format!("S3_{bucket_type}_REGION"))
.unwrap(),
url: parse_var(&format!("S3_{bucket_type}_URL")).unwrap(),
access_token: parse_var(&format!(
"S3_{bucket_type}_ACCESS_TOKEN"
))
.unwrap(),
secret: parse_var(&format!("S3_{bucket_type}_SECRET"))
.unwrap(),
match storage_backend {
FileHostKind::S3 => {
let not_empty = |v: &str| -> String {
assert!(!v.is_empty(), "S3 env var is empty");
v.to_string()
};
Arc::new(
S3Host::new(
config_from_env("PUBLIC"),
config_from_env("PRIVATE"),
S3BucketConfig {
name: not_empty(&ENV.S3_PUBLIC_BUCKET_NAME),
uses_path_style: ENV
.S3_PUBLIC_USES_PATH_STYLE_BUCKET,
region: not_empty(&ENV.S3_PUBLIC_REGION),
url: not_empty(&ENV.S3_PUBLIC_URL),
access_token: not_empty(
&ENV.S3_PUBLIC_ACCESS_TOKEN,
),
secret: not_empty(&ENV.S3_PUBLIC_SECRET),
},
S3BucketConfig {
name: not_empty(&ENV.S3_PRIVATE_BUCKET_NAME),
uses_path_style: ENV
.S3_PRIVATE_USES_PATH_STYLE_BUCKET,
region: not_empty(&ENV.S3_PRIVATE_REGION),
url: not_empty(&ENV.S3_PRIVATE_URL),
access_token: not_empty(
&ENV.S3_PRIVATE_ACCESS_TOKEN,
),
secret: not_empty(&ENV.S3_PRIVATE_SECRET),
},
)
.unwrap(),
)
}
"local" => Arc::new(file_hosting::MockHost::new()),
_ => panic!("Invalid storage backend specified. Aborting startup!"),
FileHostKind::Local => Arc::new(file_hosting::MockHost::new()),
};
info!("Initializing clickhouse connection");
@@ -160,8 +154,7 @@ async fn app() -> std::io::Result<()> {
let search_config = search::SearchConfig::new(None);
let stripe_client =
stripe::Client::new(dotenvy::var("STRIPE_API_KEY").unwrap());
let stripe_client = stripe::Client::new(ENV.STRIPE_API_KEY.clone());
let anrok_client = anrok::Client::from_env().unwrap();
let email_queue =
@@ -272,7 +265,7 @@ async fn app() -> std::io::Result<()> {
.into_app()
.configure(|cfg| app_config(cfg, labrinth_config.clone()))
})
.bind(dotenvy::var("BIND_ADDR").unwrap())?
.bind(&ENV.BIND_ADDR)?
.run()
.await
}

View File

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

View File

@@ -12,6 +12,7 @@ use crate::database::models::{
};
use crate::database::redis::RedisPool;
use crate::database::{PgPool, PgTransaction};
use crate::env::ENV;
use crate::models::billing::{
ChargeStatus, ChargeType, PaymentPlatform, Price, PriceDuration,
ProductMetadata, SubscriptionMetadata, SubscriptionStatus,
@@ -913,10 +914,10 @@ async fn unprovision_subscriptions(
let res = reqwest::Client::new()
.post(format!(
"{}/modrinth/v0/servers/{}/suspend",
dotenvy::var("ARCHON_URL")?,
ENV.ARCHON_URL,
server_id
))
.header("X-Master-Key", dotenvy::var("PYRO_API_KEY")?)
.header("X-Master-Key", &ENV.PYRO_API_KEY)
.json(&serde_json::json!({
"reason": if charge.status == ChargeStatus::Cancelled || charge.status == ChargeStatus::Expiring {
"cancelled"

View File

@@ -5,6 +5,7 @@ use crate::database::models::notifications_template_item::NotificationTemplate;
use crate::database::models::user_item::DBUser;
use crate::database::redis::RedisPool;
use crate::database::{PgPool, PgTransaction};
use crate::env::ENV;
use crate::models::notifications::{NotificationBody, NotificationType};
use crate::models::v3::notifications::{
NotificationChannel, NotificationDeliveryStatus,
@@ -36,16 +37,16 @@ impl Mailer {
) -> Result<Arc<AsyncSmtpTransport<Tokio1Executor>>, MailError> {
let maybe_transport = match self {
Mailer::Uninitialized => {
let username = dotenvy::var("SMTP_USERNAME")?;
let password = dotenvy::var("SMTP_PASSWORD")?;
let host = dotenvy::var("SMTP_HOST")?;
let port =
dotenvy::var("SMTP_PORT")?.parse::<u16>().unwrap_or(465);
let username = &ENV.SMTP_USERNAME;
let password = &ENV.SMTP_PASSWORD;
let host = &ENV.SMTP_HOST;
let port = ENV.SMTP_PORT;
let creds = (!username.is_empty())
.then(|| Credentials::new(username, password));
let creds = (!username.is_empty()).then(|| {
Credentials::new(username.clone(), password.clone())
});
let tls_setting = match dotenvy::var("SMTP_TLS")?.as_str() {
let tls_setting = match ENV.SMTP_TLS.as_str() {
"none" => Tls::None,
"opportunistic_start_tls" => Tls::Opportunistic(
TlsParameters::new(host.to_string())?,
@@ -65,7 +66,7 @@ impl Mailer {
};
let mut mailer =
AsyncSmtpTransport::<Tokio1Executor>::relay(&host)?
AsyncSmtpTransport::<Tokio1Executor>::relay(host)?
.port(port)
.tls(tls_setting);

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ use serde_json::json;
use crate::{
database::models::payout_item::DBPayout,
env::ENV,
models::payouts::{
PayoutMethod, PayoutMethodFee, PayoutMethodType, PayoutStatus,
TremendousCurrency, TremendousDetails, TremendousForexResponse,
@@ -210,7 +211,7 @@ pub(super) async fn execute(
"products": [
method_id,
],
"campaign_id": dotenvy::var("TREMENDOUS_CAMPAIGN_ID")?,
"campaign_id": ENV.TREMENDOUS_CAMPAIGN_ID.as_str(),
}]
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -1,47 +1,33 @@
use actix_web::guard::GuardContext;
use actix_web::http::header::X_FORWARDED_FOR;
use crate::env::ENV;
pub const ADMIN_KEY_HEADER: &str = "Modrinth-Admin";
pub const MEDAL_KEY_HEADER: &str = "X-Medal-Access-Key";
pub const EXTERNAL_NOTIFICATION_KEY_HEADER: &str = "External-Notification-Key";
pub fn admin_key_guard(ctx: &GuardContext) -> bool {
let admin_key = std::env::var("LABRINTH_ADMIN_KEY").expect(
"No admin key provided, this should have been caught by check_env_vars",
);
ctx.head()
.headers()
.get(ADMIN_KEY_HEADER)
.is_some_and(|it| it.as_bytes() == admin_key.as_bytes())
.is_some_and(|it| it.as_bytes() == ENV.LABRINTH_ADMIN_KEY.as_bytes())
}
pub fn medal_key_guard(ctx: &GuardContext) -> bool {
let maybe_medal_key = dotenvy::var("LABRINTH_MEDAL_KEY").ok();
match maybe_medal_key {
None => false,
Some(medal_key) => ctx
.head()
.headers()
.get(MEDAL_KEY_HEADER)
.is_some_and(|it| it.as_bytes() == medal_key.as_bytes()),
}
ctx.head()
.headers()
.get(MEDAL_KEY_HEADER)
.is_some_and(|it| it.as_bytes() == ENV.LABRINTH_MEDAL_KEY.as_bytes())
}
pub fn external_notification_key_guard(ctx: &GuardContext) -> bool {
let maybe_external_notification_key =
dotenvy::var("LABRINTH_EXTERNAL_NOTIFICATION_KEY").ok();
match maybe_external_notification_key {
None => false,
Some(external_notification_key) => ctx
.head()
.headers()
.get(EXTERNAL_NOTIFICATION_KEY_HEADER)
.is_some_and(|it| {
it.as_bytes() == external_notification_key.as_bytes()
}),
}
ctx.head()
.headers()
.get(EXTERNAL_NOTIFICATION_KEY_HEADER)
.is_some_and(|it| {
it.as_bytes() == ENV.LABRINTH_EXTERNAL_NOTIFICATION_KEY.as_bytes()
})
}
pub fn internal_network_guard(ctx: &GuardContext) -> bool {

View File

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

View File

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

View File

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

View File

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