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    super::insert_env_seeds(client, seeds).await
137}
138
139/// 从 settings 表读取限流配置(缺键回退默认值)。
140/// 启动时由 startup.rs 调用,将结果写入 `config::RATE_LIMIT_CFG`,供 rate_limit.rs
141/// 的 LazyLock 在首次请求时读取。
142#[cfg(feature = "server")]
143pub(crate) async fn load_rate_limit_settings(
144    client: &tokio_postgres::Client,
145) -> Result<RateLimitSettings, AppError> {
146    use crate::models::settings as m;
147    use RateLimitSettings as R;
148
149    let values = super::load_setting_values(
150        client,
151        &[
152            "ratelimit_strict_per_sec",
153            "ratelimit_strict_burst",
154            "ratelimit_upload_per_sec",
155            "ratelimit_upload_burst",
156            "ratelimit_image_per_sec",
157            "ratelimit_image_burst",
158            "ratelimit_comment_per_sec",
159            "ratelimit_comment_burst",
160            "ratelimit_comment_upload_per_sec",
161            "ratelimit_comment_upload_burst",
162            "ratelimit_comment_upload_daily",
163            "ratelimit_code_exec_per_sec",
164            "ratelimit_code_exec_burst",
165            "ratelimit_code_exec_daily",
166            "ratelimit_unknown_per_sec",
167            "ratelimit_unknown_burst",
168            "ratelimit_gc_interval_secs",
169        ],
170    )
171    .await?;
172    let read_clamped = |key: &str, default: u32, clamp: fn(u32) -> u32| {
173        values
174            .get(key)
175            .and_then(|v| v.parse().ok())
176            .map(clamp)
177            .unwrap_or(default)
178    };
179
180    Ok(RateLimitSettings {
181        strict_per_sec: read_clamped(
182            "ratelimit_strict_per_sec",
183            m::DEFAULT_RATE_LIMIT_STRICT_PER_SEC,
184            R::clamp_per_sec,
185        ),
186        strict_burst: read_clamped(
187            "ratelimit_strict_burst",
188            m::DEFAULT_RATE_LIMIT_STRICT_BURST,
189            R::clamp_burst,
190        ),
191        upload_per_sec: read_clamped(
192            "ratelimit_upload_per_sec",
193            m::DEFAULT_RATE_LIMIT_UPLOAD_PER_SEC,
194            R::clamp_per_sec,
195        ),
196        upload_burst: read_clamped(
197            "ratelimit_upload_burst",
198            m::DEFAULT_RATE_LIMIT_UPLOAD_BURST,
199            R::clamp_burst,
200        ),
201        image_per_sec: read_clamped(
202            "ratelimit_image_per_sec",
203            m::DEFAULT_RATE_LIMIT_IMAGE_PER_SEC,
204            R::clamp_per_sec,
205        ),
206        image_burst: read_clamped(
207            "ratelimit_image_burst",
208            m::DEFAULT_RATE_LIMIT_IMAGE_BURST,
209            R::clamp_burst,
210        ),
211        comment_per_sec: read_clamped(
212            "ratelimit_comment_per_sec",
213            m::DEFAULT_RATE_LIMIT_COMMENT_PER_SEC,
214            R::clamp_per_sec,
215        ),
216        comment_burst: read_clamped(
217            "ratelimit_comment_burst",
218            m::DEFAULT_RATE_LIMIT_COMMENT_BURST,
219            R::clamp_burst,
220        ),
221        comment_upload_per_sec: read_clamped(
222            "ratelimit_comment_upload_per_sec",
223            m::DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_PER_SEC,
224            R::clamp_per_sec,
225        ),
226        comment_upload_burst: read_clamped(
227            "ratelimit_comment_upload_burst",
228            m::DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_BURST,
229            R::clamp_burst,
230        ),
231        comment_upload_daily: read_clamped(
232            "ratelimit_comment_upload_daily",
233            m::DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_DAILY,
234            R::clamp_daily,
235        ),
236        code_exec_per_sec: read_clamped(
237            "ratelimit_code_exec_per_sec",
238            m::DEFAULT_RATE_LIMIT_CODE_EXEC_PER_SEC,
239            R::clamp_per_sec,
240        ),
241        code_exec_burst: read_clamped(
242            "ratelimit_code_exec_burst",
243            m::DEFAULT_RATE_LIMIT_CODE_EXEC_BURST,
244            R::clamp_burst,
245        ),
246        code_exec_daily: read_clamped(
247            "ratelimit_code_exec_daily",
248            m::DEFAULT_RATE_LIMIT_CODE_EXEC_DAILY,
249            R::clamp_daily,
250        ),
251        unknown_per_sec: read_clamped(
252            "ratelimit_unknown_per_sec",
253            m::DEFAULT_RATE_LIMIT_UNKNOWN_PER_SEC,
254            R::clamp_per_sec,
255        ),
256        unknown_burst: read_clamped(
257            "ratelimit_unknown_burst",
258            m::DEFAULT_RATE_LIMIT_UNKNOWN_BURST,
259            R::clamp_burst,
260        ),
261        gc_interval_secs: read_clamped(
262            "ratelimit_gc_interval_secs",
263            m::DEFAULT_RATE_LIMIT_GC_INTERVAL_SECS,
264            R::clamp_gc_interval,
265        ),
266    })
267}
268
269/// 读取限流配置(面板用)。
270#[server(GetRateLimitSettings, "/api")]
271pub async fn get_rate_limit_settings() -> Result<RateLimitSettings, ServerFnError> {
272    let _user = get_current_admin_user().await?;
273
274    #[cfg(feature = "server")]
275    {
276        let client = get_conn().await.map_err(AppError::db_conn)?;
277        let s = load_rate_limit_settings(&client)
278            .await
279            .map_err(ServerFnError::from)?;
280        Ok(s)
281    }
282
283    #[cfg(not(feature = "server"))]
284    {
285        Ok(RateLimitSettings::default())
286    }
287}
288
289/// 更新限流配置。
290///
291/// 所有字段会被 clamp 后写入 DB。由于限流器是 LazyLock 静态量(首次请求构造即
292/// 固化),修改后需**重启进程**生效——不做运行时缓存失效。
293#[server(UpdateRateLimitSettings, "/api")]
294pub async fn update_rate_limit_settings(
295    strict_per_sec: u32,
296    strict_burst: u32,
297    upload_per_sec: u32,
298    upload_burst: u32,
299    image_per_sec: u32,
300    image_burst: u32,
301    comment_per_sec: u32,
302    comment_burst: u32,
303    comment_upload_per_sec: u32,
304    comment_upload_burst: u32,
305    comment_upload_daily: u32,
306    code_exec_per_sec: u32,
307    code_exec_burst: u32,
308    code_exec_daily: u32,
309    unknown_per_sec: u32,
310    unknown_burst: u32,
311    gc_interval_secs: u32,
312) -> Result<RateLimitSettings, ServerFnError> {
313    let _user = get_current_admin_user().await?;
314
315    let strict_per_sec = RateLimitSettings::clamp_per_sec(strict_per_sec);
316    let strict_burst = RateLimitSettings::clamp_burst(strict_burst);
317    let upload_per_sec = RateLimitSettings::clamp_per_sec(upload_per_sec);
318    let upload_burst = RateLimitSettings::clamp_burst(upload_burst);
319    let image_per_sec = RateLimitSettings::clamp_per_sec(image_per_sec);
320    let image_burst = RateLimitSettings::clamp_burst(image_burst);
321    let comment_per_sec = RateLimitSettings::clamp_per_sec(comment_per_sec);
322    let comment_burst = RateLimitSettings::clamp_burst(comment_burst);
323    let comment_upload_per_sec = RateLimitSettings::clamp_per_sec(comment_upload_per_sec);
324    let comment_upload_burst = RateLimitSettings::clamp_burst(comment_upload_burst);
325    let comment_upload_daily = RateLimitSettings::clamp_daily(comment_upload_daily);
326    let code_exec_per_sec = RateLimitSettings::clamp_per_sec(code_exec_per_sec);
327    let code_exec_burst = RateLimitSettings::clamp_burst(code_exec_burst);
328    let code_exec_daily = RateLimitSettings::clamp_daily(code_exec_daily);
329    let unknown_per_sec = RateLimitSettings::clamp_per_sec(unknown_per_sec);
330    let unknown_burst = RateLimitSettings::clamp_burst(unknown_burst);
331    let gc_interval_secs = RateLimitSettings::clamp_gc_interval(gc_interval_secs);
332
333    #[cfg(feature = "server")]
334    {
335        let client = get_conn().await.map_err(AppError::db_conn)?;
336
337        for (key, value) in [
338            ("ratelimit_strict_per_sec", strict_per_sec.to_string()),
339            ("ratelimit_strict_burst", strict_burst.to_string()),
340            ("ratelimit_upload_per_sec", upload_per_sec.to_string()),
341            ("ratelimit_upload_burst", upload_burst.to_string()),
342            ("ratelimit_image_per_sec", image_per_sec.to_string()),
343            ("ratelimit_image_burst", image_burst.to_string()),
344            ("ratelimit_comment_per_sec", comment_per_sec.to_string()),
345            ("ratelimit_comment_burst", comment_burst.to_string()),
346            (
347                "ratelimit_comment_upload_per_sec",
348                comment_upload_per_sec.to_string(),
349            ),
350            (
351                "ratelimit_comment_upload_burst",
352                comment_upload_burst.to_string(),
353            ),
354            (
355                "ratelimit_comment_upload_daily",
356                comment_upload_daily.to_string(),
357            ),
358            ("ratelimit_code_exec_per_sec", code_exec_per_sec.to_string()),
359            ("ratelimit_code_exec_burst", code_exec_burst.to_string()),
360            ("ratelimit_code_exec_daily", code_exec_daily.to_string()),
361            ("ratelimit_unknown_per_sec", unknown_per_sec.to_string()),
362            ("ratelimit_unknown_burst", unknown_burst.to_string()),
363            ("ratelimit_gc_interval_secs", gc_interval_secs.to_string()),
364        ] {
365            client
366                .execute(
367                    "INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW())
368                     ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()",
369                    &[&key, &value],
370                )
371                .await
372                .map_err(AppError::query)?;
373        }
374
375        tracing::info!(
376            "Rate limit settings updated (需重启生效): strict={}/{}, upload={}/{}, \
377             image={}/{}, comment={}/{}, comment_upload={}/{}/{}, code_exec={}/{}/{}, unknown={}/{}, gc={}s",
378            strict_per_sec,
379            strict_burst,
380            upload_per_sec,
381            upload_burst,
382            image_per_sec,
383            image_burst,
384            comment_per_sec,
385            comment_burst,
386            comment_upload_per_sec,
387            comment_upload_burst,
388            comment_upload_daily,
389            code_exec_per_sec,
390            code_exec_burst,
391            code_exec_daily,
392            unknown_per_sec,
393            unknown_burst,
394            gc_interval_secs
395        );
396    }
397
398    Ok(RateLimitSettings {
399        strict_per_sec,
400        strict_burst,
401        upload_per_sec,
402        upload_burst,
403        image_per_sec,
404        image_burst,
405        comment_per_sec,
406        comment_burst,
407        comment_upload_per_sec,
408        comment_upload_burst,
409        comment_upload_daily,
410        code_exec_per_sec,
411        code_exec_burst,
412        code_exec_daily,
413        unknown_per_sec,
414        unknown_burst,
415        gc_interval_secs,
416    })
417}