Skip to main content

yggdrasil/api/settings/
site.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::SiteSettings;
15
16// ============================================================================
17// 站点公开配置
18// ============================================================================
19
20/// 读取站点公开配置。
21///
22/// **公开接口**(无需登录):供前台页脚与首页 SSR 读取 GitHub 链接和首页标语。
23/// 先查 moka 缓存,未命中查 settings 表,缺失键回退空串(不展示图标 / 使用默认标语)。
24#[server(GetSiteSettings, endpoint = "get_site_settings", input = leptos::server_fn::codec::Json)]
25pub async fn get_site_settings() -> Result<SiteSettings, ApiError> {
26    #[cfg(feature = "server")]
27    {
28        if let Some(cached) = crate::cache::get_site_settings().await {
29            return Ok(cached);
30        }
31
32        let client = get_conn().await.map_err(AppError::db_conn)?;
33
34        let rows = client
35            .query(
36                "SELECT key, value FROM settings WHERE key = ANY($1)",
37                &[&SITE_KEYS.as_slice()],
38            )
39            .await
40            .map_err(AppError::query)?;
41        let mut settings = SiteSettings::default();
42        for row in rows {
43            let value: String = row.get("value");
44            match row.get::<_, &str>("key") {
45                "site_github_url" => settings.github_url = value,
46                "site_home_title" => settings.home_title = value,
47                "site_home_subtitle" => settings.home_subtitle = value,
48                "site_home_intro" => settings.home_intro = value,
49                "site_description" => settings.site_description = value,
50                _ => {}
51            }
52        }
53        crate::cache::set_site_settings(settings.clone()).await;
54        Ok(settings)
55    }
56
57    #[cfg(not(feature = "server"))]
58    {
59        Ok(SiteSettings::default())
60    }
61}
62
63/// 友链页「交换友链」展示的本站信息。
64#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
65pub struct SiteCard {
66    pub name: String,
67    /// 站点地址(APP_BASE_URL);未配置时为空,前台不显示地址与订阅。
68    pub url: String,
69    pub description: String,
70}
71
72impl SiteCard {
73    /// RSS 订阅地址。
74    pub fn feed_url(&self) -> String {
75        format!("{}/feed.xml", self.url)
76    }
77}
78
79/// 读取友链交换信息:站名、站点地址与简介。**公开接口**。
80///
81/// 地址只取「站点配置 → 安全」里的 APP_BASE_URL,不信任请求 Host。
82#[server(GetSiteCard, endpoint = "get_site_card", input = leptos::server_fn::codec::Json)]
83pub async fn get_site_card() -> Result<SiteCard, ApiError> {
84    let settings = get_site_settings().await?;
85    #[cfg(feature = "server")]
86    let url = crate::api::settings::runtime_security_settings()
87        .await
88        .app_base_url
89        .trim()
90        .trim_end_matches('/')
91        .to_string();
92    #[cfg(not(feature = "server"))]
93    let url = String::new();
94    Ok(SiteCard {
95        name: "Yggdrasil".to_string(),
96        url,
97        description: settings.description().to_string(),
98    })
99}
100
101/// 站点公开配置在 settings 表中的键,顺序与 [`site_values`] 一致。
102#[cfg(feature = "server")]
103const SITE_KEYS: [&str; 5] = [
104    "site_github_url",
105    "site_home_title",
106    "site_home_subtitle",
107    "site_home_intro",
108    "site_description",
109];
110
111#[cfg(feature = "server")]
112fn site_values(settings: &SiteSettings) -> [&str; 5] {
113    [
114        &settings.github_url,
115        &settings.home_title,
116        &settings.home_subtitle,
117        &settings.home_intro,
118        &settings.site_description,
119    ]
120}
121
122/// 更新站点公开配置。
123///
124/// 仅 admin。写入前规范化(GitHub 链接 trim / 截断 / 补 https://,标语合并空白并截断);
125/// 写后失效 moka 站点配置缓存与全部公开页 SSR 缓存(页脚出现在每个前台页面,无法定向失效)。
126#[server(UpdateSiteSettings, endpoint = "update_site_settings", input = leptos::server_fn::codec::Json)]
127pub async fn update_site_settings(settings: SiteSettings) -> Result<SiteSettings, ApiError> {
128    let _user = get_current_admin_user().await?;
129
130    let settings = settings.normalized();
131
132    #[cfg(feature = "server")]
133    {
134        let mut client = get_conn().await.map_err(AppError::db_conn)?;
135        let tx = client.transaction().await.map_err(AppError::tx)?;
136        for (key, value) in SITE_KEYS.iter().zip(site_values(&settings)) {
137            super::upsert_setting(&*tx, key, value).await?;
138        }
139        tx.commit().await.map_err(AppError::tx)?;
140
141        crate::cache::invalidate_site_settings();
142        crate::ssr_cache::invalidate_ssr_all_public();
143        tracing::info!("Site settings updated: github_url={}", settings.github_url);
144    }
145
146    Ok(settings)
147}