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