Skip to main content

yggdrasil/api/settings/
rate_limit.rs

1// 与 posts 模块一致:Dioxus `#[server]` 宏触发 deprecated/unit/too_many_arguments
2// 提示,按项目惯例放行(限流/运行器等配置项天然参数多)。
3#![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::RateLimitSettings;
14
15// ============================================================================
16// 限流配置(重启生效)
17// ============================================================================
18//
19// 原本经环境变量在首次请求时经 LazyLock 读取(src/api/rate_limit.rs)。迁移到
20// settings 表后语义为 Tier B:env 首启播种 DB,之后面板值优先。由于限流器是
21// LazyLock 静态量,修改 DB 值需**重启进程**生效,不做运行时缓存失效。
22
23/// 启动时用 `RATE_LIMIT_*` 环境变量播种限流配置。
24///
25/// 语义与 [`seed_image_cache_settings_from_env`][crate::api::settings::seed_image_cache_settings_from_env] 一致:仅当对应 settings 键
26/// **不存在**时插入(首次部署),之后以面板写入的 DB 值为准,重启不被 env 覆盖。
27/// 单个变量非法只告警跳过,不影响其他变量与启动。
28#[cfg(feature = "server")]
29pub(crate) async fn seed_rate_limit_from_env(
30    client: &tokio_postgres::Client,
31) -> Result<(), AppError> {
32    // (env 变量名, DB 键, clamp 函数)
33    type EnvField = (&'static str, &'static str, fn(u32) -> u32);
34
35    use crate::models::settings::RateLimitSettings as R;
36
37    let mut seeds: Vec<(&'static str, String)> = Vec::new();
38
39    let fields: &[EnvField] = &[
40        (
41            "RATE_LIMIT_STRICT_PER_SEC",
42            "ratelimit_strict_per_sec",
43            R::clamp_per_sec,
44        ),
45        (
46            "RATE_LIMIT_STRICT_BURST",
47            "ratelimit_strict_burst",
48            R::clamp_burst,
49        ),
50        (
51            "RATE_LIMIT_UPLOAD_PER_SEC",
52            "ratelimit_upload_per_sec",
53            R::clamp_per_sec,
54        ),
55        (
56            "RATE_LIMIT_UPLOAD_BURST",
57            "ratelimit_upload_burst",
58            R::clamp_burst,
59        ),
60        (
61            "RATE_LIMIT_IMAGE_PER_SEC",
62            "ratelimit_image_per_sec",
63            R::clamp_per_sec,
64        ),
65        (
66            "RATE_LIMIT_IMAGE_BURST",
67            "ratelimit_image_burst",
68            R::clamp_burst,
69        ),
70        (
71            "RATE_LIMIT_COMMENT_PER_SEC",
72            "ratelimit_comment_per_sec",
73            R::clamp_per_sec,
74        ),
75        (
76            "RATE_LIMIT_COMMENT_BURST",
77            "ratelimit_comment_burst",
78            R::clamp_burst,
79        ),
80        (
81            "RATE_LIMIT_COMMENT_UPLOAD_PER_SEC",
82            "ratelimit_comment_upload_per_sec",
83            R::clamp_per_sec,
84        ),
85        (
86            "RATE_LIMIT_COMMENT_UPLOAD_BURST",
87            "ratelimit_comment_upload_burst",
88            R::clamp_burst,
89        ),
90        (
91            "RATE_LIMIT_COMMENT_UPLOAD_DAILY",
92            "ratelimit_comment_upload_daily",
93            R::clamp_daily,
94        ),
95        (
96            "RATE_LIMIT_CODE_EXEC_PER_SEC",
97            "ratelimit_code_exec_per_sec",
98            R::clamp_per_sec,
99        ),
100        (
101            "RATE_LIMIT_CODE_EXEC_BURST",
102            "ratelimit_code_exec_burst",
103            R::clamp_burst,
104        ),
105        (
106            "RATE_LIMIT_CODE_EXEC_DAILY",
107            "ratelimit_code_exec_daily",
108            R::clamp_daily,
109        ),
110        (
111            "RATE_LIMIT_UNKNOWN_PER_SEC",
112            "ratelimit_unknown_per_sec",
113            R::clamp_per_sec,
114        ),
115        (
116            "RATE_LIMIT_UNKNOWN_BURST",
117            "ratelimit_unknown_burst",
118            R::clamp_burst,
119        ),
120        (
121            "RATE_LIMIT_GC_INTERVAL_SECS",
122            "ratelimit_gc_interval_secs",
123            R::clamp_gc_interval,
124        ),
125    ];
126
127    for &(env_key, db_key, clamp) in fields {
128        if let Ok(v) = std::env::var(env_key) {
129            match v.trim().parse::<u32>() {
130                Ok(n) => seeds.push((db_key, clamp(n).to_string())),
131                Err(_) => tracing::warn!("{env_key}={v:?} 非法(期望正整数),跳过"),
132            }
133        }
134    }
135
136    for (key, value) in seeds {
137        client
138            .execute(
139                "INSERT INTO settings (key, value) VALUES ($1, $2) ON CONFLICT (key) DO NOTHING",
140                &[&key, &value],
141            )
142            .await
143            .map_err(AppError::query)?;
144        tracing::info!("限流配置已从环境变量播种: {key}={value}(仅键缺失时生效)");
145    }
146    Ok(())
147}
148
149/// 从 settings 表读取限流配置(缺键回退默认值)。
150///
151/// 启动时由 main.rs 调用,将结果写入 `config::RATE_LIMIT_CFG`,供 rate_limit.rs
152/// 的 LazyLock 在首次请求时读取。
153#[cfg(feature = "server")]
154pub(crate) async fn load_rate_limit_settings(
155    client: &tokio_postgres::Client,
156) -> Result<RateLimitSettings, AppError> {
157    use crate::models::settings as m;
158    use RateLimitSettings as R;
159
160    async fn read_clamped(
161        client: &tokio_postgres::Client,
162        key: &str,
163        default: u32,
164        clamp: fn(u32) -> u32,
165    ) -> Result<u32, AppError> {
166        let row = client
167            .query_opt("SELECT value FROM settings WHERE key = $1", &[&key])
168            .await
169            .map_err(AppError::query)?;
170        Ok(row
171            .map(|r| r.get::<_, String>("value"))
172            .and_then(|v| v.parse().ok())
173            .map(clamp)
174            .unwrap_or(default))
175    }
176
177    Ok(RateLimitSettings {
178        strict_per_sec: read_clamped(
179            client,
180            "ratelimit_strict_per_sec",
181            m::DEFAULT_RATE_LIMIT_STRICT_PER_SEC,
182            R::clamp_per_sec,
183        )
184        .await?,
185        strict_burst: read_clamped(
186            client,
187            "ratelimit_strict_burst",
188            m::DEFAULT_RATE_LIMIT_STRICT_BURST,
189            R::clamp_burst,
190        )
191        .await?,
192        upload_per_sec: read_clamped(
193            client,
194            "ratelimit_upload_per_sec",
195            m::DEFAULT_RATE_LIMIT_UPLOAD_PER_SEC,
196            R::clamp_per_sec,
197        )
198        .await?,
199        upload_burst: read_clamped(
200            client,
201            "ratelimit_upload_burst",
202            m::DEFAULT_RATE_LIMIT_UPLOAD_BURST,
203            R::clamp_burst,
204        )
205        .await?,
206        image_per_sec: read_clamped(
207            client,
208            "ratelimit_image_per_sec",
209            m::DEFAULT_RATE_LIMIT_IMAGE_PER_SEC,
210            R::clamp_per_sec,
211        )
212        .await?,
213        image_burst: read_clamped(
214            client,
215            "ratelimit_image_burst",
216            m::DEFAULT_RATE_LIMIT_IMAGE_BURST,
217            R::clamp_burst,
218        )
219        .await?,
220        comment_per_sec: read_clamped(
221            client,
222            "ratelimit_comment_per_sec",
223            m::DEFAULT_RATE_LIMIT_COMMENT_PER_SEC,
224            R::clamp_per_sec,
225        )
226        .await?,
227        comment_burst: read_clamped(
228            client,
229            "ratelimit_comment_burst",
230            m::DEFAULT_RATE_LIMIT_COMMENT_BURST,
231            R::clamp_burst,
232        )
233        .await?,
234        comment_upload_per_sec: read_clamped(
235            client,
236            "ratelimit_comment_upload_per_sec",
237            m::DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_PER_SEC,
238            R::clamp_per_sec,
239        )
240        .await?,
241        comment_upload_burst: read_clamped(
242            client,
243            "ratelimit_comment_upload_burst",
244            m::DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_BURST,
245            R::clamp_burst,
246        )
247        .await?,
248        comment_upload_daily: read_clamped(
249            client,
250            "ratelimit_comment_upload_daily",
251            m::DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_DAILY,
252            R::clamp_daily,
253        )
254        .await?,
255        code_exec_per_sec: read_clamped(
256            client,
257            "ratelimit_code_exec_per_sec",
258            m::DEFAULT_RATE_LIMIT_CODE_EXEC_PER_SEC,
259            R::clamp_per_sec,
260        )
261        .await?,
262        code_exec_burst: read_clamped(
263            client,
264            "ratelimit_code_exec_burst",
265            m::DEFAULT_RATE_LIMIT_CODE_EXEC_BURST,
266            R::clamp_burst,
267        )
268        .await?,
269        code_exec_daily: read_clamped(
270            client,
271            "ratelimit_code_exec_daily",
272            m::DEFAULT_RATE_LIMIT_CODE_EXEC_DAILY,
273            R::clamp_daily,
274        )
275        .await?,
276        unknown_per_sec: read_clamped(
277            client,
278            "ratelimit_unknown_per_sec",
279            m::DEFAULT_RATE_LIMIT_UNKNOWN_PER_SEC,
280            R::clamp_per_sec,
281        )
282        .await?,
283        unknown_burst: read_clamped(
284            client,
285            "ratelimit_unknown_burst",
286            m::DEFAULT_RATE_LIMIT_UNKNOWN_BURST,
287            R::clamp_burst,
288        )
289        .await?,
290        gc_interval_secs: read_clamped(
291            client,
292            "ratelimit_gc_interval_secs",
293            m::DEFAULT_RATE_LIMIT_GC_INTERVAL_SECS,
294            R::clamp_gc_interval,
295        )
296        .await?,
297    })
298}
299
300/// 读取限流配置(面板用)。
301#[server(GetRateLimitSettings, "/api")]
302pub async fn get_rate_limit_settings() -> Result<RateLimitSettings, ServerFnError> {
303    let _user = get_current_admin_user().await?;
304
305    #[cfg(feature = "server")]
306    {
307        let client = get_conn().await.map_err(AppError::db_conn)?;
308        let s = load_rate_limit_settings(&client)
309            .await
310            .map_err(ServerFnError::from)?;
311        Ok(s)
312    }
313
314    #[cfg(not(feature = "server"))]
315    {
316        Ok(RateLimitSettings::default())
317    }
318}
319
320/// 更新限流配置。
321///
322/// 所有字段会被 clamp 后写入 DB。由于限流器是 LazyLock 静态量(首次请求构造即
323/// 固化),修改后需**重启进程**生效——不做运行时缓存失效。
324#[server(UpdateRateLimitSettings, "/api")]
325pub async fn update_rate_limit_settings(
326    strict_per_sec: u32,
327    strict_burst: u32,
328    upload_per_sec: u32,
329    upload_burst: u32,
330    image_per_sec: u32,
331    image_burst: u32,
332    comment_per_sec: u32,
333    comment_burst: u32,
334    comment_upload_per_sec: u32,
335    comment_upload_burst: u32,
336    comment_upload_daily: u32,
337    code_exec_per_sec: u32,
338    code_exec_burst: u32,
339    code_exec_daily: u32,
340    unknown_per_sec: u32,
341    unknown_burst: u32,
342    gc_interval_secs: u32,
343) -> Result<RateLimitSettings, ServerFnError> {
344    let _user = get_current_admin_user().await?;
345
346    let strict_per_sec = RateLimitSettings::clamp_per_sec(strict_per_sec);
347    let strict_burst = RateLimitSettings::clamp_burst(strict_burst);
348    let upload_per_sec = RateLimitSettings::clamp_per_sec(upload_per_sec);
349    let upload_burst = RateLimitSettings::clamp_burst(upload_burst);
350    let image_per_sec = RateLimitSettings::clamp_per_sec(image_per_sec);
351    let image_burst = RateLimitSettings::clamp_burst(image_burst);
352    let comment_per_sec = RateLimitSettings::clamp_per_sec(comment_per_sec);
353    let comment_burst = RateLimitSettings::clamp_burst(comment_burst);
354    let comment_upload_per_sec = RateLimitSettings::clamp_per_sec(comment_upload_per_sec);
355    let comment_upload_burst = RateLimitSettings::clamp_burst(comment_upload_burst);
356    let comment_upload_daily = RateLimitSettings::clamp_daily(comment_upload_daily);
357    let code_exec_per_sec = RateLimitSettings::clamp_per_sec(code_exec_per_sec);
358    let code_exec_burst = RateLimitSettings::clamp_burst(code_exec_burst);
359    let code_exec_daily = RateLimitSettings::clamp_daily(code_exec_daily);
360    let unknown_per_sec = RateLimitSettings::clamp_per_sec(unknown_per_sec);
361    let unknown_burst = RateLimitSettings::clamp_burst(unknown_burst);
362    let gc_interval_secs = RateLimitSettings::clamp_gc_interval(gc_interval_secs);
363
364    #[cfg(feature = "server")]
365    {
366        let client = get_conn().await.map_err(AppError::db_conn)?;
367
368        for (key, value) in [
369            ("ratelimit_strict_per_sec", strict_per_sec.to_string()),
370            ("ratelimit_strict_burst", strict_burst.to_string()),
371            ("ratelimit_upload_per_sec", upload_per_sec.to_string()),
372            ("ratelimit_upload_burst", upload_burst.to_string()),
373            ("ratelimit_image_per_sec", image_per_sec.to_string()),
374            ("ratelimit_image_burst", image_burst.to_string()),
375            ("ratelimit_comment_per_sec", comment_per_sec.to_string()),
376            ("ratelimit_comment_burst", comment_burst.to_string()),
377            (
378                "ratelimit_comment_upload_per_sec",
379                comment_upload_per_sec.to_string(),
380            ),
381            (
382                "ratelimit_comment_upload_burst",
383                comment_upload_burst.to_string(),
384            ),
385            (
386                "ratelimit_comment_upload_daily",
387                comment_upload_daily.to_string(),
388            ),
389            ("ratelimit_code_exec_per_sec", code_exec_per_sec.to_string()),
390            ("ratelimit_code_exec_burst", code_exec_burst.to_string()),
391            ("ratelimit_code_exec_daily", code_exec_daily.to_string()),
392            ("ratelimit_unknown_per_sec", unknown_per_sec.to_string()),
393            ("ratelimit_unknown_burst", unknown_burst.to_string()),
394            ("ratelimit_gc_interval_secs", gc_interval_secs.to_string()),
395        ] {
396            client
397                .execute(
398                    "INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW())
399                     ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()",
400                    &[&key, &value],
401                )
402                .await
403                .map_err(AppError::query)?;
404        }
405
406        tracing::info!(
407            "Rate limit settings updated (需重启生效): strict={}/{}, upload={}/{}, \
408             image={}/{}, comment={}/{}, comment_upload={}/{}/{}, code_exec={}/{}/{}, unknown={}/{}, gc={}s",
409            strict_per_sec,
410            strict_burst,
411            upload_per_sec,
412            upload_burst,
413            image_per_sec,
414            image_burst,
415            comment_per_sec,
416            comment_burst,
417            comment_upload_per_sec,
418            comment_upload_burst,
419            comment_upload_daily,
420            code_exec_per_sec,
421            code_exec_burst,
422            code_exec_daily,
423            unknown_per_sec,
424            unknown_burst,
425            gc_interval_secs
426        );
427    }
428
429    Ok(RateLimitSettings {
430        strict_per_sec,
431        strict_burst,
432        upload_per_sec,
433        upload_burst,
434        image_per_sec,
435        image_burst,
436        comment_per_sec,
437        comment_burst,
438        comment_upload_per_sec,
439        comment_upload_burst,
440        comment_upload_daily,
441        code_exec_per_sec,
442        code_exec_burst,
443        code_exec_daily,
444        unknown_per_sec,
445        unknown_burst,
446        gc_interval_secs,
447    })
448}