yggdrasil/pages/admin/settings/
security_section.rs1use leptos::prelude::*;
7
8use super::kit::{use_section, SettingRow, SettingsCard, SwitchRow};
9use super::SettingsSection;
10use crate::api::error::ApiError;
11use crate::api::settings::{get_security_settings, update_security_settings};
12use crate::components::forms::{FormInput, FormSelect, FORM_SELECT_COMPACT_CLASS};
13use crate::components::ui::{SegmentedControl, SegmentedItem, SegmentedKind, SegmentedSize};
14use crate::models::settings::SecuritySettings;
15
16async fn save(d: SecuritySettings) -> Result<SecuritySettings, ApiError> {
17 update_security_settings(
18 d.app_base_url,
19 d.cookie_secure,
20 d.trusted_proxy_count,
21 d.max_sessions_per_user,
22 )
23 .await
24}
25
26#[component]
28pub fn SecuritySection() -> impl IntoView {
29 let st = use_section(SettingsSection::Security, get_security_settings, save);
30 let proxy_items = Signal::stored(
31 (0..=3u32)
32 .map(|n| SegmentedItem::new(n, n.to_string()))
33 .collect::<Vec<_>>(),
34 );
35 let session_options = Signal::stored(vec![
36 (1u32, "1(仅单设备)"),
37 (3, "3"),
38 (5, "5(默认)"),
39 (10, "10"),
40 (20, "20"),
41 (50, "50"),
42 ]);
43 view! {
44 <SettingsCard
45 section=SettingsSection::Security
46 description="CSRF 可信来源、Cookie 与真实 IP 提取。保存后即时生效。"
47 ready=st.ready()
48 load_failed=st.load_failed()
49 failed=st.failed()
50 >
51 <SettingRow
52 label="可信来源"
53 html_for="sec-base-url"
54 hint="即 APP_BASE_URL,须带 http:// 或 https://,写请求的 CSRF 校验以它为准。留空时只要求来源主机与请求 Host 一致,生产环境建议显式设置。"
55 >
56 <FormInput
57 id="sec-base-url"
58 class="field settings-mono"
59 r#type="url"
60 placeholder="https://你的站点"
61 value=st.field(|d| d.app_base_url.clone())
62 oninput=move |v: String| st.edit(|d| d.app_base_url = v)
63 />
64 </SettingRow>
65 <SwitchRow
66 label="Cookie Secure"
67 hint="只在 HTTPS 下发送会话 Cookie。HTTPS 生产环境务必开启。"
68 checked=st.field(|d| d.cookie_secure)
69 ontoggle=move |()| st.edit(|d| d.cookie_secure = !d.cookie_secure)
70 />
71 <SettingRow
72 label="可信代理段数"
73 hint="即 TRUSTED_PROXY_COUNT:X-Forwarded-For 中真实客户端 IP 右侧的段数,不等于代理层数。直连填 0;单层反代覆盖 XFF 并设置 X-Real-IP 时填 1;CDN + 源站反代填 1 而非 2。设大会允许伪造 IP 绕过限流。"
74 >
75 <SegmentedControl
76 items=proxy_items
77 value=st.field(|d| d.trusted_proxy_count)
78 on_change=move |n: u32| st.edit(|d| d.trusted_proxy_count = n)
79 aria_label="可信代理段数"
80 kind=SegmentedKind::Radio
81 size=SegmentedSize::Sm
82 class="settings-segmented-square"
83 />
84 </SettingRow>
85 <SettingRow
86 label="并发会话上限"
87 html_for="sec-sessions"
88 hint="单个账号同时保持登录的设备数,超出时最早的会话自动失效。"
89 >
90 <FormSelect
91 id="sec-sessions"
92 value=st.field(|d| d.max_sessions_per_user)
93 options=session_options
94 trigger_class=FORM_SELECT_COMPACT_CLASS
95 onchange=move |v: u32| st.edit(|d| d.max_sessions_per_user = v)
96 />
97 </SettingRow>
98 </SettingsCard>
99 }
100}