6c405e142d
crate::lock replaces three poison-tolerant lock helpers; the rate limiter reuses auth::bearer_token; the landing page reuses invites::display_code. Removed: AppError::not_implemented (D-004 placeholder, only its own test used it) and the unused Mutation::mutation_id/entity_type. tasks::start never awaited or failed: now a plain fn returning AppState. Milestone-ownership comments (M2a/M3/M4) rewritten to say what the code does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
137 lines
5.1 KiB
Rust
137 lines
5.1 KiB
Rust
//! Server configuration from the environment (SPEC §10.4). The variable list is exhaustive.
|
|
|
|
use std::net::SocketAddr;
|
|
use std::path::PathBuf;
|
|
|
|
use crate::error::AppError;
|
|
|
|
/// Default of `KVITT_DATA_DIR`.
|
|
pub const DEFAULT_DATA_DIR: &str = "/data";
|
|
/// Default of `KVITT_BIND`.
|
|
pub const DEFAULT_BIND: &str = "0.0.0.0:8080";
|
|
/// Default of `KVITT_PUBLIC_URL`.
|
|
pub const DEFAULT_PUBLIC_URL: &str = "https://icehack.asuscomm.com:8443";
|
|
/// Default of `KVITT_FCM_CREDENTIALS`.
|
|
pub const DEFAULT_FCM_CREDENTIALS: &str = "/secrets/fcm-service-account.json";
|
|
/// Default of `KVITT_FCM_PROJECT_ID`.
|
|
pub const DEFAULT_FCM_PROJECT_ID: &str = "kvitt-1337";
|
|
/// Default of `KVITT_TRUSTED_PROXY`.
|
|
pub const DEFAULT_TRUSTED_PROXY: &str = "caddy";
|
|
/// Default of `RUST_LOG`.
|
|
pub const DEFAULT_LOG_FILTER: &str = "info";
|
|
|
|
/// File name of the database inside the data directory.
|
|
pub const DATABASE_FILE: &str = "kvitt.sqlite";
|
|
|
|
/// The server's configuration. `RUST_LOG` is read by the tracing set-up in `main`, not stored here.
|
|
///
|
|
/// Nothing here touches the network or the file system: a trusted proxy host that does not resolve and a missing
|
|
/// FCM key file are both normal (no proxy: the peer address is the client; no key: push disabled).
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Config {
|
|
/// `KVITT_DATA_DIR`: holds `kvitt.sqlite`, `pictures/`, `apk/`.
|
|
pub data_dir: PathBuf,
|
|
/// `KVITT_BIND`: listen address.
|
|
pub bind: SocketAddr,
|
|
/// `KVITT_PUBLIC_URL`: used in invite links and the landing page. Stored without a trailing slash.
|
|
pub public_url: String,
|
|
/// `KVITT_FCM_CREDENTIALS`: service-account key; push is disabled if the file is missing.
|
|
pub fcm_credentials: PathBuf,
|
|
/// `KVITT_FCM_PROJECT_ID`.
|
|
pub fcm_project_id: String,
|
|
/// `KVITT_TRUSTED_PROXY`: host name whose resolved address may set `X-Forwarded-For`. Resolved lazily by the
|
|
/// rate limiter; an unresolvable name means "no proxy".
|
|
pub trusted_proxy: String,
|
|
}
|
|
|
|
impl Config {
|
|
/// Reads the process environment.
|
|
pub fn from_env() -> Result<Self, AppError> {
|
|
Self::from_lookup(|key| std::env::var(key).ok())
|
|
}
|
|
|
|
/// Reads the variables through `lookup` (testable without touching the process environment). Empty values count
|
|
/// as unset.
|
|
pub fn from_lookup(lookup: impl Fn(&str) -> Option<String>) -> Result<Self, AppError> {
|
|
let get = |key: &str, default: &str| {
|
|
lookup(key)
|
|
.filter(|v| !v.trim().is_empty())
|
|
.unwrap_or_else(|| default.to_owned())
|
|
};
|
|
let bind_text = get("KVITT_BIND", DEFAULT_BIND);
|
|
let bind: SocketAddr = bind_text.parse().map_err(|e| {
|
|
AppError::new(
|
|
crate::error::ErrorCode::Internal,
|
|
format!("KVITT_BIND {bind_text:?} is not ip:port: {e}"),
|
|
)
|
|
})?;
|
|
Ok(Self {
|
|
data_dir: PathBuf::from(get("KVITT_DATA_DIR", DEFAULT_DATA_DIR)),
|
|
bind,
|
|
public_url: get("KVITT_PUBLIC_URL", DEFAULT_PUBLIC_URL)
|
|
.trim_end_matches('/')
|
|
.to_owned(),
|
|
fcm_credentials: PathBuf::from(get("KVITT_FCM_CREDENTIALS", DEFAULT_FCM_CREDENTIALS)),
|
|
fcm_project_id: get("KVITT_FCM_PROJECT_ID", DEFAULT_FCM_PROJECT_ID),
|
|
trusted_proxy: get("KVITT_TRUSTED_PROXY", DEFAULT_TRUSTED_PROXY),
|
|
})
|
|
}
|
|
|
|
/// `<data_dir>/kvitt.sqlite`.
|
|
pub fn database_path(&self) -> PathBuf {
|
|
self.data_dir.join(DATABASE_FILE)
|
|
}
|
|
|
|
/// `<data_dir>/pictures`.
|
|
pub fn pictures_dir(&self) -> PathBuf {
|
|
self.data_dir.join("pictures")
|
|
}
|
|
|
|
/// `<data_dir>/apk`.
|
|
pub fn apk_dir(&self) -> PathBuf {
|
|
self.data_dir.join("apk")
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn defaults_when_unset_or_empty() {
|
|
let config = Config::from_lookup(|k| (k == "KVITT_BIND").then(String::new)).unwrap();
|
|
assert_eq!(config.data_dir, PathBuf::from("/data"));
|
|
assert_eq!(config.bind, "0.0.0.0:8080".parse().unwrap());
|
|
assert_eq!(config.public_url, "https://icehack.asuscomm.com:8443");
|
|
assert_eq!(
|
|
config.fcm_credentials,
|
|
PathBuf::from("/secrets/fcm-service-account.json")
|
|
);
|
|
assert_eq!(config.fcm_project_id, "kvitt-1337");
|
|
assert_eq!(config.trusted_proxy, "caddy");
|
|
assert_eq!(config.database_path(), PathBuf::from("/data/kvitt.sqlite"));
|
|
}
|
|
|
|
#[test]
|
|
fn overrides() {
|
|
let config = Config::from_lookup(|k| match k {
|
|
"KVITT_DATA_DIR" => Some("/tmp/kvitt".into()),
|
|
"KVITT_BIND" => Some("127.0.0.1:9000".into()),
|
|
"KVITT_PUBLIC_URL" => Some("http://192.168.1.10:8080/".into()),
|
|
_ => None,
|
|
})
|
|
.unwrap();
|
|
assert_eq!(
|
|
config.database_path(),
|
|
PathBuf::from("/tmp/kvitt/kvitt.sqlite")
|
|
);
|
|
assert_eq!(config.bind, "127.0.0.1:9000".parse().unwrap());
|
|
assert_eq!(config.public_url, "http://192.168.1.10:8080");
|
|
}
|
|
|
|
#[test]
|
|
fn bad_bind_is_an_error() {
|
|
assert!(Config::from_lookup(|k| (k == "KVITT_BIND").then(|| "nope".into())).is_err());
|
|
}
|
|
}
|