Skip to main content

yggdrasil/api/settings/
rate_limit.rs

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