1pub mod attachment_settings;
3pub mod people;
4pub mod settings;
5
6use super::{ClientError, WekanClient, transport::decode_success};
7use reqwest::{
8 Method,
9 header::{ACCEPT, CONTENT_TYPE},
10};
11use secrecy::{ExposeSecret, SecretString};
12use serde::{
13 Deserialize, Deserializer, Serialize, Serializer,
14 de::{self, DeserializeOwned, MapAccess, Visitor},
15};
16use serde_json::{Number, Value};
17
18pub const MAX_SETTINGS_BYTES: usize = 50 * 1024 * 1024;
19const MAX_RESPONSE_BYTES: usize = 64 * 1024 * 1024;
20
21pub struct PreparedSettingsUpdate<T> {
23 body: Vec<u8>,
24 request: std::marker::PhantomData<fn() -> T>,
25}
26
27#[derive(Debug, thiserror::Error)]
28#[error("serialized settings cannot be encoded within the 50 MiB limit; no request was sent")]
29pub struct SettingsEncodeError;
30
31impl<T: Serialize> PreparedSettingsUpdate<T> {
32 pub fn new(request: &T) -> Result<Self, SettingsEncodeError> {
33 Ok(Self {
34 body: encode_bounded(request, MAX_SETTINGS_BYTES)?,
35 request: std::marker::PhantomData,
36 })
37 }
38}
39
40pub(super) fn present<'de, D: Deserializer<'de>, T: Deserialize<'de>>(
41 d: D,
42) -> Result<Option<T>, D::Error> {
43 T::deserialize(d).map(Some)
44}
45
46pub(crate) struct Object<T>(pub T);
48impl<'de, T: Deserialize<'de>> Deserialize<'de> for Object<T> {
49 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
50 struct ObjectVisitor<T>(std::marker::PhantomData<T>);
51 impl<'de, T: Deserialize<'de>> Visitor<'de> for ObjectVisitor<T> {
52 type Value = Object<T>;
53 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
54 f.write_str("a JSON object")
55 }
56 fn visit_map<M: MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
57 T::deserialize(de::value::MapAccessDeserializer::new(map)).map(Object)
58 }
59 }
60 d.deserialize_map(ObjectVisitor(std::marker::PhantomData))
61 }
62}
63pub(super) fn present_object<'de, D: Deserializer<'de>, T: Deserialize<'de>>(
64 d: D,
65) -> Result<Option<T>, D::Error> {
66 Object::<T>::deserialize(d).map(|v| Some(v.0))
67}
68pub(super) fn nullable_object<'de, D: Deserializer<'de>, T: Deserialize<'de>>(
69 d: D,
70) -> Result<Option<T>, D::Error> {
71 Option::<Object<T>>::deserialize(d).map(|v| v.map(|v| v.0))
72}
73pub(super) fn date<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
74 let value = String::deserialize(d)?;
75 super::datetime::validate_response_date(&value)
76 .map_err(|()| de::Error::custom("invalid settings date"))?;
77 Ok(value)
78}
79
80macro_rules! numeric_setting {
81 ($name:ident, $min:expr, $max:expr) => {
82 #[derive(Clone, Debug, Eq, PartialEq, Serialize)]
83 #[serde(transparent)]
84 pub struct $name(Number);
85 impl<'de> Deserialize<'de> for $name {
86 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
87 let number = Number::deserialize(d)?;
88 let value = number
89 .as_f64()
90 .filter(|v| v.is_finite())
91 .ok_or_else(|| de::Error::custom("expected a finite settings number"))?;
92 let min: Option<f64> = $min;
93 let max: Option<f64> = $max;
94 if min.is_some_and(|m| value < m) || max.is_some_and(|m| value > m) {
95 return Err(de::Error::custom(
96 "settings number is outside the supported range",
97 ));
98 }
99 Ok(Self(number))
100 }
101 }
102 impl std::str::FromStr for $name {
103 type Err = &'static str;
104 fn from_str(value: &str) -> Result<Self, Self::Err> {
105 serde_json::from_str(value)
106 .map_err(|_| "expected a finite JSON number within the setting's range")
107 }
108 }
109 };
110}
111numeric_setting!(SettingNumber, None, None);
112numeric_setting!(NonNegativeNumber, Some(0.0), None);
113numeric_setting!(BatchSize, Some(1.0), Some(100.0));
114numeric_setting!(DelayMs, Some(100.0), Some(10000.0));
115numeric_setting!(CpuThreshold, Some(10.0), Some(90.0));
116
117#[derive(Clone, Debug, Eq, PartialEq)]
119pub struct MaskedSecret;
120impl<'de> Deserialize<'de> for MaskedSecret {
121 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
122 if String::deserialize(d)?.is_empty() {
123 Ok(Self)
124 } else {
125 Err(de::Error::custom("unmasked storage credential"))
126 }
127 }
128}
129
130#[derive(Clone, Debug)]
131pub struct StorageSecret(pub(crate) SecretString);
132impl<'de> Deserialize<'de> for StorageSecret {
133 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
134 String::deserialize(d).map(|v| Self(SecretString::from(v)))
135 }
136}
137impl Serialize for StorageSecret {
138 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
139 s.serialize_str(self.0.expose_secret())
140 }
141}
142
143#[derive(Clone, Debug)]
145pub struct GcsCredentials {
146 value: SecretString,
147 object: bool,
148}
149impl GcsCredentials {
150 pub(crate) fn text(value: SecretString) -> Self {
151 Self {
152 value,
153 object: false,
154 }
155 }
156 pub(crate) fn redaction_secrets(&self) -> Vec<SecretString> {
157 fn collect(value: &Value, secrets: &mut Vec<SecretString>) {
158 match value {
159 Value::String(s) => secrets.push(SecretString::from(s.clone())),
160 Value::Object(m) => m.values().for_each(|v| collect(v, secrets)),
161 Value::Array(a) => a.iter().for_each(|v| collect(v, secrets)),
162 _ => {}
163 }
164 }
165 let mut secrets = vec![self.value.clone()];
166 if let Ok(value) = serde_json::from_str::<Value>(self.value.expose_secret()) {
167 collect(&value, &mut secrets);
168 }
169 secrets
170 }
171}
172impl<'de> Deserialize<'de> for GcsCredentials {
173 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
174 match Value::deserialize(d)? {
175 Value::String(s) => Ok(Self::text(SecretString::from(s))),
176 Value::Object(map) => Ok(Self {
177 value: SecretString::from(Value::Object(map).to_string()),
178 object: true,
179 }),
180 _ => Err(de::Error::custom(
181 "expected credential text or a credential object",
182 )),
183 }
184 }
185}
186impl Serialize for GcsCredentials {
187 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
188 if self.object {
189 serde_json::from_str::<Value>(self.value.expose_secret())
191 .map_err(serde::ser::Error::custom)?
192 .serialize(s)
193 } else {
194 s.serialize_str(self.value.expose_secret())
195 }
196 }
197}
198
199impl WekanClient {
200 async fn admin_settings<T: DeserializeOwned>(
201 &self,
202 method: Method,
203 route: &str,
204 body: Option<Vec<u8>>,
205 token: &SecretString,
206 operation: &str,
207 ) -> Result<T, ClientError> {
208 let endpoint = self
209 .server()
210 .join(route)
211 .map_err(|_| ClientError::Protocol {
212 message: "could not construct settings endpoint; no request was sent".into(),
213 diagnostic: None,
214 success_status_received: false,
215 })?;
216 let mut request = self
217 .http
218 .request(method, endpoint)
219 .header(ACCEPT, "application/json")
220 .bearer_auth(token.expose_secret());
221 if let Some(body) = body {
222 request = request.header(CONTENT_TYPE, "application/json").body(body);
223 }
224 let body = self
225 .execute_success_request_with_limit(request, MAX_RESPONSE_BYTES)
226 .await?;
227 decode_success::<Object<T>>(&body, operation, true).map(|v| v.0)
228 }
229 pub async fn global_settings(
230 &self,
231 token: &SecretString,
232 ) -> Result<settings::GlobalSettingsDocument, ClientError> {
233 self.admin_settings(
234 Method::GET,
235 "api/settings",
236 None,
237 token,
238 "admin settings get",
239 )
240 .await
241 }
242 pub async fn update_global_settings(
243 &self,
244 request: PreparedSettingsUpdate<settings::UpdateGlobalSettings>,
245 token: &SecretString,
246 ) -> Result<settings::GlobalSettingsDocument, ClientError> {
247 self.admin_settings(
248 Method::PUT,
249 "api/settings",
250 Some(request.body),
251 token,
252 "admin settings update",
253 )
254 .await
255 }
256 pub async fn attachment_settings(
257 &self,
258 token: &SecretString,
259 ) -> Result<attachment_settings::AttachmentSettingsDocument, ClientError> {
260 self.admin_settings(
261 Method::GET,
262 "api/admin/attachment-settings",
263 None,
264 token,
265 "admin attachment-settings get",
266 )
267 .await
268 }
269 pub async fn update_attachment_settings(
270 &self,
271 request: PreparedSettingsUpdate<attachment_settings::UpdateAttachmentSettings>,
272 token: &SecretString,
273 ) -> Result<attachment_settings::AttachmentSettingsDocument, ClientError> {
274 self.admin_settings(
275 Method::PUT,
276 "api/admin/attachment-settings",
277 Some(request.body),
278 token,
279 "admin attachment-settings update",
280 )
281 .await
282 }
283}
284
285fn encode_bounded<T: Serialize>(request: &T, limit: usize) -> Result<Vec<u8>, SettingsEncodeError> {
286 struct BoundedBody {
287 bytes: Vec<u8>,
288 limit: usize,
289 }
290 impl std::io::Write for BoundedBody {
291 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
292 if bytes.len() > self.limit.saturating_sub(self.bytes.len()) {
293 return Err(std::io::Error::other("settings size limit"));
294 }
295 self.bytes.extend_from_slice(bytes);
296 Ok(bytes.len())
297 }
298 fn flush(&mut self) -> std::io::Result<()> {
299 Ok(())
300 }
301 }
302 let mut body = BoundedBody {
303 bytes: Vec::new(),
304 limit,
305 };
306 serde_json::to_writer(&mut body, request).map_err(|_| SettingsEncodeError)?;
307 Ok(body.bytes)
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use std::cell::Cell;
314
315 #[test]
316 fn preparation_serializes_once_and_bounds_encoded_bytes_including_escapes() {
317 struct Counted(Cell<usize>);
318 impl Serialize for Counted {
319 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
320 self.0.set(self.0.get() + 1);
321 serializer.serialize_str("line\n")
322 }
323 }
324 let request = Counted(Cell::new(0));
325 let prepared = PreparedSettingsUpdate::new(&request).unwrap();
326 assert_eq!(request.0.get(), 1);
327 let expected = b"\"line\\n\"";
328 assert_eq!(prepared.body, expected);
329 assert_eq!(encode_bounded(&"line\n", expected.len()).unwrap(), expected);
330 assert!(encode_bounded(&"line\n", expected.len() - 1).is_err());
331 }
332}