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:
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user