yggdrasil/api/settings/
security.rs1#![allow(clippy::unused_unit, deprecated, clippy::too_many_arguments)]
4
5use crate::api::error::ApiError;
6use leptos::prelude::*;
7
8#[cfg(feature = "server")]
9use crate::api::auth::get_current_admin_user;
10#[cfg(feature = "server")]
11use crate::api::error::AppError;
12#[cfg(feature = "server")]
13use crate::db::pool::get_conn;
14use crate::models::settings::SecuritySettings;
15#[cfg(feature = "server")]
17use crate::cache::invalidate_security_settings;
18
19#[cfg(feature = "server")]
30pub(crate) async fn seed_security_settings_from_env(
31 client: &tokio_postgres::Client,
32) -> Result<(), AppError> {
33 let mut seeds: Vec<(&'static str, String)> = Vec::new();
34
35 if let Ok(v) = std::env::var("APP_BASE_URL") {
36 match crate::models::settings::SecuritySettings::parse_app_base_url(&v) {
37 Some(url) if !url.is_empty() => seeds.push(("security_app_base_url", url)),
38 Some(_) => {}
39 None => tracing::warn!(
40 "APP_BASE_URL={v:?} 非法(期望 http(s)://主机[:端口],如 https://example.com),跳过"
41 ),
42 }
43 }
44 if let Ok(v) = std::env::var("COOKIE_SECURE") {
45 match v.trim().parse::<bool>() {
46 Ok(b) => seeds.push(("security_cookie_secure", b.to_string())),
47 Err(_) => tracing::warn!("COOKIE_SECURE={v:?} 非法(期望 true/false),跳过"),
48 }
49 }
50 if let Ok(v) = std::env::var("TRUSTED_PROXY_COUNT") {
51 match v.trim().parse::<u32>() {
52 Ok(n) => seeds.push((
53 "security_trusted_proxy_count",
54 crate::models::settings::SecuritySettings::clamp_trusted_proxy_count(n).to_string(),
55 )),
56 Err(_) => tracing::warn!("TRUSTED_PROXY_COUNT={v:?} 非法(期望非负整数),跳过"),
57 }
58 }
59 if let Ok(v) = std::env::var("MAX_SESSIONS_PER_USER") {
60 match v.trim().parse::<u32>() {
61 Ok(n) => seeds.push((
62 "security_max_sessions_per_user",
63 crate::models::settings::SecuritySettings::clamp_max_sessions(n).to_string(),
64 )),
65 Err(_) => tracing::warn!("MAX_SESSIONS_PER_USER={v:?} 非法(期望正整数),跳过"),
66 }
67 }
68
69 super::insert_env_seeds(client, seeds).await
70}
71
72#[cfg(feature = "server")]
77pub(crate) async fn load_security_settings(
78 client: &tokio_postgres::Client,
79) -> Result<crate::models::settings::SecuritySettings, AppError> {
80 let values = super::load_setting_values(
81 client,
82 &[
83 "security_app_base_url",
84 "security_cookie_secure",
85 "security_trusted_proxy_count",
86 "security_max_sessions_per_user",
87 ],
88 )
89 .await?;
90
91 let app_base_url = values
94 .get("security_app_base_url")
95 .map(|v| {
96 crate::models::settings::SecuritySettings::parse_app_base_url(v).unwrap_or_else(|| {
97 tracing::warn!("站点地址 {v:?} 不合法(缺少 http:// 或 https://),按未配置处理");
98 String::new()
99 })
100 })
101 .unwrap_or_default();
102 let cookie_secure = values
103 .get("security_cookie_secure")
104 .and_then(|v| v.parse().ok())
105 .unwrap_or(crate::models::settings::DEFAULT_COOKIE_SECURE);
106 let trusted_proxy_count = values
107 .get("security_trusted_proxy_count")
108 .and_then(|v| v.parse().ok())
109 .map(crate::models::settings::SecuritySettings::clamp_trusted_proxy_count)
110 .unwrap_or(crate::models::settings::DEFAULT_TRUSTED_PROXY_COUNT);
111 let max_sessions_per_user = values
112 .get("security_max_sessions_per_user")
113 .and_then(|v| v.parse().ok())
114 .map(crate::models::settings::SecuritySettings::clamp_max_sessions)
115 .unwrap_or(crate::models::settings::DEFAULT_MAX_SESSIONS_PER_USER);
116
117 Ok(crate::models::settings::SecuritySettings {
118 app_base_url,
119 cookie_secure,
120 trusted_proxy_count,
121 max_sessions_per_user,
122 })
123}
124
125#[cfg(feature = "server")]
130pub(crate) async fn runtime_security_settings() -> crate::models::settings::SecuritySettings {
131 if let Some(s) = crate::cache::get_security_settings().await {
132 return s;
133 }
134 let fallback = crate::models::settings::SecuritySettings::default();
135 if std::env::var("DATABASE_URL").is_err() {
138 return fallback;
139 }
140 match get_conn().await {
141 Ok(client) => match load_security_settings(&client).await {
142 Ok(s) => {
143 crate::cache::set_security_settings(s.clone()).await;
144 s
145 }
146 Err(e) => {
147 tracing::warn!("读取安全配置失败,回退默认值:{e:?}");
148 fallback
149 }
150 },
151 Err(e) => {
152 tracing::warn!("获取连接读取安全配置失败,回退默认值:{e:?}");
153 fallback
154 }
155 }
156}
157
158#[server(GetSecuritySettings, endpoint = "get_security_settings", input = leptos::server_fn::codec::Json)]
160pub async fn get_security_settings() -> Result<SecuritySettings, ApiError> {
161 let _user = get_current_admin_user().await?;
162
163 #[cfg(feature = "server")]
164 {
165 let client = get_conn().await.map_err(AppError::db_conn)?;
166 let s = load_security_settings(&client)
167 .await
168 .map_err(<ApiError>::from)?;
169 crate::cache::set_security_settings(s.clone()).await;
170 Ok(s)
171 }
172
173 #[cfg(not(feature = "server"))]
174 {
175 Ok(SecuritySettings::default())
176 }
177}
178
179#[server(UpdateSecuritySettings, endpoint = "update_security_settings", input = leptos::server_fn::codec::Json)]
184pub async fn update_security_settings(
185 app_base_url: String,
186 cookie_secure: bool,
187 trusted_proxy_count: u32,
188 max_sessions_per_user: u32,
189) -> Result<SecuritySettings, ApiError> {
190 let _user = get_current_admin_user().await?;
191
192 let app_base_url = SecuritySettings::parse_app_base_url(&app_base_url).ok_or_else(|| {
193 AppError::BadRequest(
194 "站点地址需以 http:// 或 https:// 开头,例如 https://example.com;留空则按请求主机校验"
195 .to_string(),
196 )
197 })?;
198 let trusted_proxy_count = SecuritySettings::clamp_trusted_proxy_count(trusted_proxy_count);
199 let max_sessions_per_user = SecuritySettings::clamp_max_sessions(max_sessions_per_user);
200
201 #[cfg(feature = "server")]
202 {
203 let client = get_conn().await.map_err(AppError::db_conn)?;
204
205 super::upsert_settings(
206 &**client,
207 &[
208 ("security_app_base_url", app_base_url.clone()),
209 ("security_cookie_secure", cookie_secure.to_string()),
210 (
211 "security_trusted_proxy_count",
212 trusted_proxy_count.to_string(),
213 ),
214 (
215 "security_max_sessions_per_user",
216 max_sessions_per_user.to_string(),
217 ),
218 ],
219 )
220 .await?;
221
222 invalidate_security_settings();
223 crate::ssr_cache::invalidate_ssr_all_public();
225 tracing::info!(
226 "Security settings updated: app_base_url={:?}, cookie_secure={}, proxy_count={}, max_sessions={}",
227 app_base_url,
228 cookie_secure,
229 trusted_proxy_count,
230 max_sessions_per_user
231 );
232 }
233
234 Ok(SecuritySettings {
235 app_base_url,
236 cookie_secure,
237 trusted_proxy_count,
238 max_sessions_per_user,
239 })
240}