yggdrasil/pages/admin/settings/
ratelimit_section.rs1use leptos::prelude::*;
6
7use super::kit::{use_section, SettingRow, SettingsCard};
8use super::SettingsSection;
9use crate::api::error::ApiError;
10use crate::api::settings::{get_rate_limit_settings, update_rate_limit_settings};
11use crate::components::forms::NumberInput;
12use crate::models::settings::RateLimitSettings;
13
14async fn save(d: RateLimitSettings) -> Result<RateLimitSettings, ApiError> {
15 update_rate_limit_settings(
16 d.strict_per_sec,
17 d.strict_burst,
18 d.upload_per_sec,
19 d.upload_burst,
20 d.image_per_sec,
21 d.image_burst,
22 d.comment_per_sec,
23 d.comment_burst,
24 d.comment_upload_per_sec,
25 d.comment_upload_burst,
26 d.comment_upload_daily,
27 d.code_exec_per_sec,
28 d.code_exec_burst,
29 d.code_exec_daily,
30 d.unknown_per_sec,
31 d.unknown_burst,
32 d.mcp_per_sec,
33 d.mcp_burst,
34 d.mcp_upload_per_sec,
35 d.mcp_upload_burst,
36 d.gc_interval_secs,
37 )
38 .await
39}
40
41type Bucket = (
43 &'static str,
44 &'static str,
45 fn(&RateLimitSettings) -> (u32, u32),
46 fn(&mut RateLimitSettings, u32, u32),
47);
48
49const BUCKETS: [Bucket; 9] = [
50 (
51 "登录与注册",
52 "strict",
53 |s| (s.strict_per_sec, s.strict_burst),
54 |s, a, b| {
55 s.strict_per_sec = a;
56 s.strict_burst = b;
57 },
58 ),
59 (
60 "图片上传",
61 "upload",
62 |s| (s.upload_per_sec, s.upload_burst),
63 |s, a, b| {
64 s.upload_per_sec = a;
65 s.upload_burst = b;
66 },
67 ),
68 (
69 "图片访问",
70 "image",
71 |s| (s.image_per_sec, s.image_burst),
72 |s, a, b| {
73 s.image_per_sec = a;
74 s.image_burst = b;
75 },
76 ),
77 (
78 "发表评论",
79 "comment",
80 |s| (s.comment_per_sec, s.comment_burst),
81 |s, a, b| {
82 s.comment_per_sec = a;
83 s.comment_burst = b;
84 },
85 ),
86 (
87 "评论传图",
88 "comment-upload",
89 |s| (s.comment_upload_per_sec, s.comment_upload_burst),
90 |s, a, b| {
91 s.comment_upload_per_sec = a;
92 s.comment_upload_burst = b;
93 },
94 ),
95 (
96 "运行代码",
97 "code-exec",
98 |s| (s.code_exec_per_sec, s.code_exec_burst),
99 |s, a, b| {
100 s.code_exec_per_sec = a;
101 s.code_exec_burst = b;
102 },
103 ),
104 (
105 "无法识别 IP",
106 "unknown",
107 |s| (s.unknown_per_sec, s.unknown_burst),
108 |s, a, b| {
109 s.unknown_per_sec = a;
110 s.unknown_burst = b;
111 },
112 ),
113 (
114 "MCP 请求",
115 "mcp",
116 |s| (s.mcp_per_sec, s.mcp_burst),
117 |s, a, b| {
118 s.mcp_per_sec = a;
119 s.mcp_burst = b;
120 },
121 ),
122 (
123 "MCP 传图",
124 "mcp-upload",
125 |s| (s.mcp_upload_per_sec, s.mcp_upload_burst),
126 |s, a, b| {
127 s.mcp_upload_per_sec = a;
128 s.mcp_upload_burst = b;
129 },
130 ),
131];
132
133#[component]
135pub fn RateLimitSection() -> impl IntoView {
136 let st = use_section(SettingsSection::RateLimit, get_rate_limit_settings, save);
137 view! {
138 <SettingsCard
139 section=SettingsSection::RateLimit
140 description="按 IP 计的令牌桶(MCP 两项按令牌计):每秒补充的请求数与可以一次用掉的突发上限。"
141 ready=st.ready()
142 load_failed=st.load_failed()
143 failed=st.failed()
144 >
145 {BUCKETS
146 .into_iter()
147 .map(|(label, key, get, set)| {
148 view! {
149 <SettingRow label=label>
150 <div class="settings-pair">
151 <NumberInput
152 id=format!("rl-{key}-rate")
153 value=st.field(move |d| i64::from(get(d).0))
154 min=1
155 max=10_000
156 unit="次/秒"
157 onchange=move |v: i64| {
158 st.edit(|s| {
159 let (_, b) = get(s);
160 set(s, v as u32, b);
161 })
162 }
163 />
164 <NumberInput
165 id=format!("rl-{key}-burst")
166 value=st.field(move |d| i64::from(get(d).1))
167 min=1
168 max=10_000
169 unit="突发"
170 onchange=move |v: i64| {
171 st.edit(|s| {
172 let (a, _) = get(s);
173 set(s, a, v as u32);
174 })
175 }
176 />
177 </div>
178 </SettingRow>
179 }
180 })
181 .collect_view()}
182 <SettingRow label="评论传图日限额" html_for="rl-comment-upload-daily">
183 <NumberInput
184 id="rl-comment-upload-daily"
185 value=st.field(|d| i64::from(d.comment_upload_daily))
186 min=1
187 max=100_000
188 unit="次/天"
189 hint="评论区允许匿名传图,日限额是防止磁盘被占满的关键防线。"
190 onchange=move |v: i64| st.edit(|d| d.comment_upload_daily = v as u32)
191 />
192 </SettingRow>
193 <SettingRow label="运行代码日限额" html_for="rl-code-exec-daily">
194 <NumberInput
195 id="rl-code-exec-daily"
196 value=st.field(|d| i64::from(d.code_exec_daily))
197 min=1
198 max=100_000
199 unit="次/天"
200 onchange=move |v: i64| st.edit(|d| d.code_exec_daily = v as u32)
201 />
202 </SettingRow>
203 <SettingRow label="回收间隔" html_for="rl-gc">
204 <NumberInput
205 id="rl-gc"
206 value=st.field(|d| i64::from(d.gc_interval_secs))
207 min=10
208 max=86_400
209 step=60
210 unit="秒"
211 hint="定期清掉不活跃 IP 的限流桶,释放内存。"
212 onchange=move |v: i64| st.edit(|d| d.gc_interval_secs = v as u32)
213 />
214 </SettingRow>
215 </SettingsCard>
216 }
217}