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 super::insert_env_seeds(client, seeds).await
66}
67
68#[cfg(feature = "server")]
73pub(crate) async fn load_security_settings(
74 client: &tokio_postgres::Client,
75) -> Result<crate::models::settings::SecuritySettings, AppError> {
76 let values = super::load_setting_values(
77 client,
78 &[
79 "security_app_base_url",
80 "security_cookie_secure",
81 "security_trusted_proxy_count",
82 "security_max_sessions_per_user",
83 ],
84 )
85 .await?;
86
87 let app_base_url = values
88 .get("security_app_base_url")
89 .map(|v| crate::models::settings::SecuritySettings::normalize_app_base_url(v))
90 .unwrap_or_default();
91 let cookie_secure = values
92 .get("security_cookie_secure")
93 .and_then(|v| v.parse().ok())
94 .unwrap_or(crate::models::settings::DEFAULT_COOKIE_SECURE);
95 let trusted_proxy_count = values
96 .get("security_trusted_proxy_count")
97 .and_then(|v| v.parse().ok())
98 .map(crate::models::settings::SecuritySettings::clamp_trusted_proxy_count)
99 .unwrap_or(crate::models::settings::DEFAULT_TRUSTED_PROXY_COUNT);
100 let max_sessions_per_user = values
101 .get("security_max_sessions_per_user")
102 .and_then(|v| v.parse().ok())
103 .map(crate::models::settings::SecuritySettings::clamp_max_sessions)
104 .unwrap_or(crate::models::settings::DEFAULT_MAX_SESSIONS_PER_USER);
105
106 Ok(crate::models::settings::SecuritySettings {
107 app_base_url,
108 cookie_secure,
109 trusted_proxy_count,
110 max_sessions_per_user,
111 })
112}
113
114#[cfg(feature = "server")]
119pub(crate) async fn runtime_security_settings() -> crate::models::settings::SecuritySettings {
120 if let Some(s) = crate::cache::get_security_settings().await {
121 return s;
122 }
123 let fallback = crate::models::settings::SecuritySettings::default();
124 if std::env::var("DATABASE_URL").is_err() {
127 return fallback;
128 }
129 match get_conn().await {
130 Ok(client) => match load_security_settings(&client).await {
131 Ok(s) => {
132 crate::cache::set_security_settings(s.clone()).await;
133 s
134 }
135 Err(e) => {
136 tracing::warn!("读取安全配置失败,回退默认值:{e:?}");
137 fallback
138 }
139 },
140 Err(e) => {
141 tracing::warn!("获取连接读取安全配置失败,回退默认值:{e:?}");
142 fallback
143 }
144 }
145}
146
147#[server(GetSecuritySettings, "/api")]
149pub async fn get_security_settings() -> Result<SecuritySettings, ServerFnError> {
150 let _user = get_current_admin_user().await?;
151
152 #[cfg(feature = "server")]
153 {
154 let client = get_conn().await.map_err(AppError::db_conn)?;
155 let s = load_security_settings(&client)
156 .await
157 .map_err(ServerFnError::from)?;
158 crate::cache::set_security_settings(s.clone()).await;
159 Ok(s)
160 }
161
162 #[cfg(not(feature = "server"))]
163 {
164 Ok(SecuritySettings::default())
165 }
166}
167
168#[server(UpdateSecuritySettings, "/api")]
172pub async fn update_security_settings(
173 app_base_url: String,
174 cookie_secure: bool,
175 trusted_proxy_count: u32,
176 max_sessions_per_user: u32,
177) -> Result<SecuritySettings, ServerFnError> {
178 let _user = get_current_admin_user().await?;
179
180 let app_base_url = SecuritySettings::normalize_app_base_url(&app_base_url);
181 let trusted_proxy_count = SecuritySettings::clamp_trusted_proxy_count(trusted_proxy_count);
182 let max_sessions_per_user = SecuritySettings::clamp_max_sessions(max_sessions_per_user);
183
184 #[cfg(feature = "server")]
185 {
186 let client = get_conn().await.map_err(AppError::db_conn)?;
187
188 for (key, value) in [
189 ("security_app_base_url", app_base_url.clone()),
190 ("security_cookie_secure", cookie_secure.to_string()),
191 (
192 "security_trusted_proxy_count",
193 trusted_proxy_count.to_string(),
194 ),
195 (
196 "security_max_sessions_per_user",
197 max_sessions_per_user.to_string(),
198 ),
199 ] {
200 client
201 .execute(
202 "INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW())
203 ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()",
204 &[&key, &value],
205 )
206 .await
207 .map_err(AppError::query)?;
208 }
209
210 invalidate_security_settings();
211 tracing::info!(
212 "Security settings updated: app_base_url={:?}, cookie_secure={}, proxy_count={}, max_sessions={}",
213 app_base_url,
214 cookie_secure,
215 trusted_proxy_count,
216 max_sessions_per_user
217 );
218 }
219
220 Ok(SecuritySettings {
221 app_base_url,
222 cookie_secure,
223 trusted_proxy_count,
224 max_sessions_per_user,
225 })
226}