yggdrasil/api/settings/
security.rs1#![allow(clippy::unused_unit, deprecated, clippy::too_many_arguments)]
4
5use dioxus::prelude::*;
6
7#[cfg(feature = "server")]
8use crate::api::auth::get_current_admin_user;
9#[cfg(feature = "server")]
10use crate::api::error::AppError;
11#[cfg(feature = "server")]
12use crate::db::pool::get_conn;
13use crate::models::settings::SecuritySettings;
14#[cfg(feature = "server")]
16use crate::cache::invalidate_security_settings;
17
18#[cfg(feature = "server")]
29pub(crate) async fn seed_security_settings_from_env(
30 client: &tokio_postgres::Client,
31) -> Result<(), AppError> {
32 let mut seeds: Vec<(&'static str, String)> = Vec::new();
33
34 if let Ok(v) = std::env::var("APP_BASE_URL") {
35 let url = crate::models::settings::SecuritySettings::normalize_app_base_url(&v);
36 if !url.is_empty() {
37 seeds.push(("security_app_base_url", url));
38 }
39 }
40 if let Ok(v) = std::env::var("COOKIE_SECURE") {
41 match v.trim().parse::<bool>() {
42 Ok(b) => seeds.push(("security_cookie_secure", b.to_string())),
43 Err(_) => tracing::warn!("COOKIE_SECURE={v:?} 非法(期望 true/false),跳过"),
44 }
45 }
46 if let Ok(v) = std::env::var("TRUSTED_PROXY_COUNT") {
47 match v.trim().parse::<u32>() {
48 Ok(n) => seeds.push((
49 "security_trusted_proxy_count",
50 crate::models::settings::SecuritySettings::clamp_trusted_proxy_count(n).to_string(),
51 )),
52 Err(_) => tracing::warn!("TRUSTED_PROXY_COUNT={v:?} 非法(期望非负整数),跳过"),
53 }
54 }
55 if let Ok(v) = std::env::var("MAX_SESSIONS_PER_USER") {
56 match v.trim().parse::<u32>() {
57 Ok(n) => seeds.push((
58 "security_max_sessions_per_user",
59 crate::models::settings::SecuritySettings::clamp_max_sessions(n).to_string(),
60 )),
61 Err(_) => tracing::warn!("MAX_SESSIONS_PER_USER={v:?} 非法(期望正整数),跳过"),
62 }
63 }
64
65 for (key, value) in seeds {
66 client
67 .execute(
68 "INSERT INTO settings (key, value) VALUES ($1, $2) ON CONFLICT (key) DO NOTHING",
69 &[&key, &value],
70 )
71 .await
72 .map_err(AppError::query)?;
73 tracing::info!("安全配置已从环境变量播种: {key}={value}(仅键缺失时生效)");
74 }
75 Ok(())
76}
77
78#[cfg(feature = "server")]
83pub(crate) async fn load_security_settings(
84 client: &tokio_postgres::Client,
85) -> Result<crate::models::settings::SecuritySettings, AppError> {
86 async fn read_key(
87 client: &tokio_postgres::Client,
88 key: &str,
89 ) -> Result<Option<String>, AppError> {
90 let row = client
91 .query_opt("SELECT value FROM settings WHERE key = $1", &[&key])
92 .await
93 .map_err(AppError::query)?;
94 Ok(row.map(|r| r.get::<_, String>("value")))
95 }
96
97 let app_base_url = read_key(client, "security_app_base_url")
98 .await?
99 .map(|v| crate::models::settings::SecuritySettings::normalize_app_base_url(&v))
100 .unwrap_or_default();
101 let cookie_secure = read_key(client, "security_cookie_secure")
102 .await?
103 .and_then(|v| v.parse().ok())
104 .unwrap_or(crate::models::settings::DEFAULT_COOKIE_SECURE);
105 let trusted_proxy_count = read_key(client, "security_trusted_proxy_count")
106 .await?
107 .and_then(|v| v.parse().ok())
108 .map(crate::models::settings::SecuritySettings::clamp_trusted_proxy_count)
109 .unwrap_or(crate::models::settings::DEFAULT_TRUSTED_PROXY_COUNT);
110 let max_sessions_per_user = read_key(client, "security_max_sessions_per_user")
111 .await?
112 .and_then(|v| v.parse().ok())
113 .map(crate::models::settings::SecuritySettings::clamp_max_sessions)
114 .unwrap_or(crate::models::settings::DEFAULT_MAX_SESSIONS_PER_USER);
115
116 Ok(crate::models::settings::SecuritySettings {
117 app_base_url,
118 cookie_secure,
119 trusted_proxy_count,
120 max_sessions_per_user,
121 })
122}
123
124#[cfg(feature = "server")]
129pub(crate) async fn runtime_security_settings() -> crate::models::settings::SecuritySettings {
130 if let Some(s) = crate::cache::get_security_settings().await {
131 return s;
132 }
133 let fallback = crate::models::settings::SecuritySettings::default();
134 if std::env::var("DATABASE_URL").is_err() {
137 return fallback;
138 }
139 match get_conn().await {
140 Ok(client) => match load_security_settings(&client).await {
141 Ok(s) => {
142 crate::cache::set_security_settings(s.clone()).await;
143 s
144 }
145 Err(e) => {
146 tracing::warn!("读取安全配置失败,回退默认值:{e:?}");
147 fallback
148 }
149 },
150 Err(e) => {
151 tracing::warn!("获取连接读取安全配置失败,回退默认值:{e:?}");
152 fallback
153 }
154 }
155}
156
157#[server(GetSecuritySettings, "/api")]
159pub async fn get_security_settings() -> Result<SecuritySettings, ServerFnError> {
160 let _user = get_current_admin_user().await?;
161
162 #[cfg(feature = "server")]
163 {
164 let client = get_conn().await.map_err(AppError::db_conn)?;
165 let s = load_security_settings(&client)
166 .await
167 .map_err(ServerFnError::from)?;
168 crate::cache::set_security_settings(s.clone()).await;
169 Ok(s)
170 }
171
172 #[cfg(not(feature = "server"))]
173 {
174 Ok(SecuritySettings::default())
175 }
176}
177
178#[server(UpdateSecuritySettings, "/api")]
182pub async fn update_security_settings(
183 app_base_url: String,
184 cookie_secure: bool,
185 trusted_proxy_count: u32,
186 max_sessions_per_user: u32,
187) -> Result<SecuritySettings, ServerFnError> {
188 let _user = get_current_admin_user().await?;
189
190 let app_base_url = SecuritySettings::normalize_app_base_url(&app_base_url);
191 let trusted_proxy_count = SecuritySettings::clamp_trusted_proxy_count(trusted_proxy_count);
192 let max_sessions_per_user = SecuritySettings::clamp_max_sessions(max_sessions_per_user);
193
194 #[cfg(feature = "server")]
195 {
196 let client = get_conn().await.map_err(AppError::db_conn)?;
197
198 for (key, value) in [
199 ("security_app_base_url", app_base_url.clone()),
200 ("security_cookie_secure", cookie_secure.to_string()),
201 (
202 "security_trusted_proxy_count",
203 trusted_proxy_count.to_string(),
204 ),
205 (
206 "security_max_sessions_per_user",
207 max_sessions_per_user.to_string(),
208 ),
209 ] {
210 client
211 .execute(
212 "INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW())
213 ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()",
214 &[&key, &value],
215 )
216 .await
217 .map_err(AppError::query)?;
218 }
219
220 invalidate_security_settings();
221 tracing::info!(
222 "Security settings updated: app_base_url={:?}, cookie_secure={}, proxy_count={}, max_sessions={}",
223 app_base_url,
224 cookie_secure,
225 trusted_proxy_count,
226 max_sessions_per_user
227 );
228 }
229
230 Ok(SecuritySettings {
231 app_base_url,
232 cookie_secure,
233 trusted_proxy_count,
234 max_sessions_per_user,
235 })
236}