yggdrasil/api/settings/
trash.rs1#![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::TrashSettings;
15
16#[cfg(feature = "server")]
18pub(crate) async fn load_trash_settings(
19 client: &tokio_postgres::Client,
20) -> Result<TrashSettings, AppError> {
21 let enabled: bool = client
22 .query_opt(
23 "SELECT value FROM settings WHERE key = 'trash_auto_purge_enabled'",
24 &[],
25 )
26 .await
27 .map_err(AppError::query)?
28 .and_then(|r| r.get::<_, String>("value").parse().ok())
29 .unwrap_or(crate::models::settings::DEFAULT_AUTO_PURGE_ENABLED);
30
31 let days: i32 = client
32 .query_opt(
33 "SELECT value FROM settings WHERE key = 'trash_retention_days'",
34 &[],
35 )
36 .await
37 .map_err(AppError::query)?
38 .and_then(|r| r.get::<_, String>("value").parse().ok())
39 .unwrap_or(crate::models::settings::DEFAULT_RETENTION_DAYS);
40
41 Ok(TrashSettings {
42 auto_purge_enabled: enabled,
43 retention_days: TrashSettings::clamp_retention(days),
44 })
45}
46
47#[cfg(feature = "server")]
49pub(crate) async fn save_trash_settings(
50 client: &tokio_postgres::Client,
51 auto_purge_enabled: bool,
52 retention_days: i32,
53) -> Result<TrashSettings, AppError> {
54 let retention_days = TrashSettings::clamp_retention(retention_days);
55
56 super::upsert_settings(
57 client,
58 &[
59 ("trash_auto_purge_enabled", auto_purge_enabled.to_string()),
60 ("trash_retention_days", retention_days.to_string()),
61 ],
62 )
63 .await?;
64
65 Ok(TrashSettings {
66 auto_purge_enabled,
67 retention_days,
68 })
69}
70
71#[server(GetTrashSettings, endpoint = "get_trash_settings", input = leptos::server_fn::codec::Json)]
75pub async fn get_trash_settings() -> Result<TrashSettings, ApiError> {
76 let _user = get_current_admin_user().await?;
77
78 #[cfg(feature = "server")]
79 {
80 let client = get_conn().await.map_err(AppError::db_conn)?;
81 Ok(load_trash_settings(&client).await?)
82 }
83
84 #[cfg(not(feature = "server"))]
85 {
86 Ok(TrashSettings::default())
87 }
88}
89
90#[server(UpdateTrashSettings, endpoint = "update_trash_settings", input = leptos::server_fn::codec::Json)]
94pub async fn update_trash_settings(
95 auto_purge_enabled: bool,
96 retention_days: i32,
97) -> Result<TrashSettings, ApiError> {
98 let _user = get_current_admin_user().await?;
99
100 #[cfg(feature = "server")]
101 {
102 let client = get_conn().await.map_err(AppError::db_conn)?;
103 let settings = save_trash_settings(&client, auto_purge_enabled, retention_days).await?;
104
105 tracing::info!(
106 "Trash settings updated: auto_purge={}, retention_days={}",
107 settings.auto_purge_enabled,
108 settings.retention_days
109 );
110
111 Ok(settings)
112 }
113
114 #[cfg(not(feature = "server"))]
115 {
116 Ok(TrashSettings {
117 auto_purge_enabled,
118 retention_days,
119 })
120 }
121}
122
123#[cfg(all(test, feature = "server"))]
124mod tests {
125 use super::*;
126
127 #[tokio::test]
128 #[ignore = "requires DATABASE_URL; uses only a connection-local temporary table"]
129 async fn trash_settings_database_roundtrip() {
130 let url =
131 std::env::var("DATABASE_URL").expect("set DATABASE_URL to run this database test");
132 let (client, connection) = tokio_postgres::connect(&url, tokio_postgres::NoTls)
133 .await
134 .unwrap();
135 let connection = tokio::spawn(connection);
136 client
137 .batch_execute(
138 "CREATE TEMP TABLE settings (
139 key TEXT PRIMARY KEY,
140 value TEXT NOT NULL,
141 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
142 )",
143 )
144 .await
145 .unwrap();
146
147 assert_eq!(
148 load_trash_settings(&client).await.unwrap(),
149 TrashSettings::default()
150 );
151 client
152 .batch_execute(
153 "INSERT INTO settings (key, value) VALUES
154 ('trash_auto_purge_enabled', 'invalid'),
155 ('trash_retention_days', 'invalid')",
156 )
157 .await
158 .unwrap();
159 assert_eq!(
160 load_trash_settings(&client).await.unwrap(),
161 TrashSettings::default()
162 );
163
164 for (enabled, input_days, expected_days) in [(true, -5, 1), (false, 366, 365), (true, 7, 7)]
165 {
166 let expected = TrashSettings {
167 auto_purge_enabled: enabled,
168 retention_days: expected_days,
169 };
170 assert_eq!(
171 save_trash_settings(&client, enabled, input_days)
172 .await
173 .unwrap(),
174 expected
175 );
176 assert_eq!(load_trash_settings(&client).await.unwrap(), expected);
177 let stored: String = client
178 .query_one(
179 "SELECT value FROM settings WHERE key = 'trash_retention_days'",
180 &[],
181 )
182 .await
183 .unwrap()
184 .get(0);
185 assert_eq!(stored, expected_days.to_string());
186 }
187
188 for (stored, expected_days) in [("-5", 1), ("366", 365)] {
189 client
190 .execute(
191 "UPDATE settings SET value = $1 WHERE key = 'trash_retention_days'",
192 &[&stored],
193 )
194 .await
195 .unwrap();
196 assert_eq!(
197 load_trash_settings(&client).await.unwrap().retention_days,
198 expected_days
199 );
200 }
201
202 client
203 .batch_execute("ALTER TABLE pg_temp.settings RENAME COLUMN value TO invalid_value")
204 .await
205 .unwrap();
206 assert!(matches!(
207 load_trash_settings(&client).await,
208 Err(AppError::Query(_))
209 ));
210 assert!(matches!(
211 save_trash_settings(&client, true, 30).await,
212 Err(AppError::Query(_))
213 ));
214 drop(client);
215 connection.await.unwrap().unwrap();
216 }
217}