274 lines
8.3 KiB
Rust
274 lines
8.3 KiB
Rust
use std::{env, net::SocketAddr};
|
|
|
|
// 集中管理 api-server 的启动配置,避免入口层直接散落环境变量解析逻辑。
|
|
#[derive(Clone, Debug)]
|
|
pub struct AppConfig {
|
|
pub bind_host: String,
|
|
pub bind_port: u16,
|
|
pub log_filter: String,
|
|
pub jwt_issuer: String,
|
|
pub jwt_secret: String,
|
|
pub jwt_access_token_ttl_seconds: u64,
|
|
pub refresh_cookie_name: String,
|
|
pub refresh_cookie_path: String,
|
|
pub refresh_cookie_secure: bool,
|
|
pub refresh_cookie_same_site: String,
|
|
pub refresh_session_ttl_days: u32,
|
|
pub sms_auth_enabled: bool,
|
|
pub wechat_auth_enabled: bool,
|
|
pub oss_bucket: Option<String>,
|
|
pub oss_endpoint: Option<String>,
|
|
pub oss_access_key_id: Option<String>,
|
|
pub oss_access_key_secret: Option<String>,
|
|
pub oss_public_base_url: Option<String>,
|
|
pub oss_post_expire_seconds: u64,
|
|
pub oss_post_max_size_bytes: u64,
|
|
pub oss_success_action_status: u16,
|
|
}
|
|
|
|
impl Default for AppConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
bind_host: "127.0.0.1".to_string(),
|
|
bind_port: 3000,
|
|
log_filter: "info,tower_http=info".to_string(),
|
|
jwt_issuer: "https://auth.genarrative.local".to_string(),
|
|
jwt_secret: "genarrative-dev-secret".to_string(),
|
|
jwt_access_token_ttl_seconds: 2 * 60 * 60,
|
|
refresh_cookie_name: "genarrative_refresh_session".to_string(),
|
|
refresh_cookie_path: "/api/auth".to_string(),
|
|
refresh_cookie_secure: false,
|
|
refresh_cookie_same_site: "Lax".to_string(),
|
|
refresh_session_ttl_days: 30,
|
|
sms_auth_enabled: false,
|
|
wechat_auth_enabled: false,
|
|
oss_bucket: None,
|
|
oss_endpoint: None,
|
|
oss_access_key_id: None,
|
|
oss_access_key_secret: None,
|
|
oss_public_base_url: None,
|
|
oss_post_expire_seconds: 10 * 60,
|
|
oss_post_max_size_bytes: 20 * 1024 * 1024,
|
|
oss_success_action_status: 200,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AppConfig {
|
|
pub fn from_env() -> Self {
|
|
let mut config = Self::default();
|
|
|
|
if let Ok(bind_host) = env::var("GENARRATIVE_API_HOST")
|
|
&& !bind_host.trim().is_empty()
|
|
{
|
|
config.bind_host = bind_host;
|
|
}
|
|
|
|
if let Ok(bind_port) = env::var("GENARRATIVE_API_PORT")
|
|
&& let Ok(parsed_port) = bind_port.parse::<u16>()
|
|
{
|
|
config.bind_port = parsed_port;
|
|
}
|
|
|
|
if let Ok(log_filter) = env::var("GENARRATIVE_API_LOG")
|
|
&& !log_filter.trim().is_empty()
|
|
{
|
|
config.log_filter = log_filter;
|
|
}
|
|
|
|
if let Some(jwt_issuer) =
|
|
read_first_non_empty_env(&["GENARRATIVE_JWT_ISSUER", "JWT_ISSUER"])
|
|
{
|
|
config.jwt_issuer = jwt_issuer;
|
|
}
|
|
|
|
if let Some(jwt_secret) =
|
|
read_first_non_empty_env(&["GENARRATIVE_JWT_SECRET", "JWT_SECRET"])
|
|
{
|
|
config.jwt_secret = jwt_secret;
|
|
}
|
|
|
|
if let Some(ttl_seconds) = read_first_duration_seconds_env(&[
|
|
"GENARRATIVE_JWT_ACCESS_TOKEN_TTL_SECONDS",
|
|
"JWT_EXPIRES_IN",
|
|
]) {
|
|
config.jwt_access_token_ttl_seconds = ttl_seconds;
|
|
}
|
|
|
|
if let Some(refresh_cookie_name) = read_first_non_empty_env(&["AUTH_REFRESH_COOKIE_NAME"]) {
|
|
config.refresh_cookie_name = refresh_cookie_name;
|
|
}
|
|
|
|
if let Some(refresh_cookie_path) = read_first_non_empty_env(&["AUTH_REFRESH_COOKIE_PATH"]) {
|
|
config.refresh_cookie_path = refresh_cookie_path;
|
|
}
|
|
|
|
if let Some(refresh_cookie_same_site) =
|
|
read_first_non_empty_env(&["AUTH_REFRESH_COOKIE_SAME_SITE"])
|
|
{
|
|
config.refresh_cookie_same_site = refresh_cookie_same_site;
|
|
}
|
|
|
|
if let Some(refresh_cookie_secure) = read_first_bool_env(&["AUTH_REFRESH_COOKIE_SECURE"]) {
|
|
config.refresh_cookie_secure = refresh_cookie_secure;
|
|
}
|
|
|
|
if let Some(refresh_session_ttl_days) =
|
|
read_first_positive_u32_env(&["AUTH_REFRESH_SESSION_TTL_DAYS"])
|
|
{
|
|
config.refresh_session_ttl_days = refresh_session_ttl_days;
|
|
}
|
|
|
|
if let Some(sms_auth_enabled) = read_first_bool_env(&["SMS_AUTH_ENABLED"]) {
|
|
config.sms_auth_enabled = sms_auth_enabled;
|
|
}
|
|
|
|
if let Some(wechat_auth_enabled) = read_first_bool_env(&["WECHAT_AUTH_ENABLED"]) {
|
|
config.wechat_auth_enabled = wechat_auth_enabled;
|
|
}
|
|
|
|
config.oss_bucket = read_first_non_empty_env(&["ALIYUN_OSS_BUCKET"]);
|
|
config.oss_endpoint = read_first_non_empty_env(&["ALIYUN_OSS_ENDPOINT"]);
|
|
config.oss_access_key_id = read_first_non_empty_env(&["ALIYUN_OSS_ACCESS_KEY_ID"]);
|
|
config.oss_access_key_secret = read_first_non_empty_env(&["ALIYUN_OSS_ACCESS_KEY_SECRET"]);
|
|
config.oss_public_base_url = read_first_non_empty_env(&["ALIYUN_OSS_PUBLIC_BASE_URL"]);
|
|
|
|
if let Some(oss_post_expire_seconds) =
|
|
read_first_duration_seconds_env(&["ALIYUN_OSS_POST_EXPIRE_SECONDS"])
|
|
{
|
|
config.oss_post_expire_seconds = oss_post_expire_seconds;
|
|
}
|
|
|
|
if let Some(oss_post_max_size_bytes) =
|
|
read_first_positive_u64_env(&["ALIYUN_OSS_POST_MAX_SIZE_BYTES"])
|
|
{
|
|
config.oss_post_max_size_bytes = oss_post_max_size_bytes;
|
|
}
|
|
|
|
if let Some(oss_success_action_status) =
|
|
read_first_positive_u16_env(&["ALIYUN_OSS_SUCCESS_ACTION_STATUS"])
|
|
{
|
|
config.oss_success_action_status = oss_success_action_status;
|
|
}
|
|
|
|
config
|
|
}
|
|
|
|
pub fn bind_socket_addr(&self) -> SocketAddr {
|
|
let address = format!("{}:{}", self.bind_host, self.bind_port);
|
|
address
|
|
.parse()
|
|
.unwrap_or_else(|_| SocketAddr::from(([127, 0, 0, 1], 3000)))
|
|
}
|
|
}
|
|
|
|
fn read_first_non_empty_env(keys: &[&str]) -> Option<String> {
|
|
keys.iter().find_map(|key| {
|
|
env::var(key).ok().and_then(|value| {
|
|
let value = value.trim().to_string();
|
|
if value.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
Some(value)
|
|
})
|
|
})
|
|
}
|
|
|
|
fn read_first_duration_seconds_env(keys: &[&str]) -> Option<u64> {
|
|
keys.iter().find_map(|key| {
|
|
env::var(key)
|
|
.ok()
|
|
.and_then(|value| parse_duration_seconds(&value))
|
|
})
|
|
}
|
|
|
|
fn read_first_bool_env(keys: &[&str]) -> Option<bool> {
|
|
keys.iter()
|
|
.find_map(|key| env::var(key).ok().and_then(|value| parse_bool(&value)))
|
|
}
|
|
|
|
fn read_first_positive_u32_env(keys: &[&str]) -> Option<u32> {
|
|
keys.iter().find_map(|key| {
|
|
env::var(key)
|
|
.ok()
|
|
.and_then(|value| parse_positive_u32(&value))
|
|
})
|
|
}
|
|
|
|
fn read_first_positive_u64_env(keys: &[&str]) -> Option<u64> {
|
|
keys.iter().find_map(|key| {
|
|
env::var(key)
|
|
.ok()
|
|
.and_then(|value| parse_positive_u64(&value))
|
|
})
|
|
}
|
|
|
|
fn read_first_positive_u16_env(keys: &[&str]) -> Option<u16> {
|
|
keys.iter().find_map(|key| {
|
|
env::var(key)
|
|
.ok()
|
|
.and_then(|value| parse_positive_u16(&value))
|
|
})
|
|
}
|
|
|
|
fn parse_duration_seconds(raw: &str) -> Option<u64> {
|
|
let raw = raw.trim();
|
|
if raw.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
if let Ok(seconds) = raw.parse::<u64>() {
|
|
return Some(seconds);
|
|
}
|
|
|
|
let (number, unit) = raw.split_at(raw.len().checked_sub(1)?);
|
|
let unit = unit.to_ascii_lowercase();
|
|
let number = number.trim().parse::<u64>().ok()?;
|
|
|
|
let multiplier = match unit.as_str() {
|
|
"s" => 1,
|
|
"m" => 60,
|
|
"h" => 60 * 60,
|
|
"d" => 24 * 60 * 60,
|
|
_ => return None,
|
|
};
|
|
|
|
number.checked_mul(multiplier)
|
|
}
|
|
|
|
fn parse_bool(raw: &str) -> Option<bool> {
|
|
match raw.trim().to_ascii_lowercase().as_str() {
|
|
"1" | "true" | "yes" | "on" => Some(true),
|
|
"0" | "false" | "no" | "off" => Some(false),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn parse_positive_u32(raw: &str) -> Option<u32> {
|
|
let value = raw.trim().parse::<u32>().ok()?;
|
|
if value == 0 {
|
|
return None;
|
|
}
|
|
|
|
Some(value)
|
|
}
|
|
|
|
fn parse_positive_u64(raw: &str) -> Option<u64> {
|
|
let value = raw.trim().parse::<u64>().ok()?;
|
|
if value == 0 {
|
|
return None;
|
|
}
|
|
|
|
Some(value)
|
|
}
|
|
|
|
fn parse_positive_u16(raw: &str) -> Option<u16> {
|
|
let value = raw.trim().parse::<u16>().ok()?;
|
|
if value == 0 {
|
|
return None;
|
|
}
|
|
|
|
Some(value)
|
|
}
|