1use super::Object;
3use crate::client::{
4 ClientError, WekanClient, datetime::validate_response_date, transport::decode_success,
5};
6use reqwest::{Method, header::ACCEPT};
7use secrecy::{ExposeSecret, SecretString};
8use serde::{Deserialize, Serialize, de::DeserializeOwned};
9
10const MAX_RESPONSE_BYTES: usize = 64 * 1024 * 1024;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
13#[serde(rename_all = "snake_case")]
14pub enum FeatureField {
15 SharedTemplates,
16 PropagateMembersToBoards,
17 SyncMembersFromAuth,
18}
19
20impl FeatureField {
21 fn org_field(self) -> &'static str {
22 match self {
23 Self::SharedTemplates => "orgSharedTemplates",
24 Self::PropagateMembersToBoards => "orgPropagateMembersToBoards",
25 Self::SyncMembersFromAuth => "orgSyncMembersFromAuth",
26 }
27 }
28 fn team_field(self) -> &'static str {
29 match self {
30 Self::SharedTemplates => "teamSharedTemplates",
31 Self::PropagateMembersToBoards => "teamPropagateMembersToBoards",
32 Self::SyncMembersFromAuth => "teamSyncMembersFromAuth",
33 }
34 }
35}
36
37#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
38#[serde(deny_unknown_fields)]
39pub struct DomainCount {
40 pub domain: String,
41 pub count: u64,
42}
43
44#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
45#[serde(deny_unknown_fields)]
46pub struct OrganizationSummary {
47 #[serde(rename(deserialize = "_id"))]
48 pub organization_id: String,
49 #[serde(rename(deserialize = "orgDisplayName"))]
50 pub display_name: Option<String>,
51 #[serde(rename(deserialize = "orgShortName"))]
52 pub short_name: Option<String>,
53 #[serde(rename(deserialize = "orgSharedTemplates"))]
54 pub shared_templates: bool,
55 #[serde(rename(deserialize = "orgPropagateMembersToBoards"))]
56 pub propagate_members_to_boards: bool,
57 #[serde(rename(deserialize = "orgSyncMembersFromAuth"))]
58 pub sync_members_from_auth: bool,
59}
60
61#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
62#[serde(deny_unknown_fields)]
63pub struct TeamSummary {
64 #[serde(rename(deserialize = "_id"))]
65 pub team_id: String,
66 #[serde(rename(deserialize = "teamDisplayName"))]
67 pub display_name: Option<String>,
68 #[serde(rename(deserialize = "teamShortName"))]
69 pub short_name: Option<String>,
70 #[serde(rename(deserialize = "teamSharedTemplates"))]
71 pub shared_templates: bool,
72 #[serde(rename(deserialize = "teamPropagateMembersToBoards"))]
73 pub propagate_members_to_boards: bool,
74 #[serde(rename(deserialize = "teamSyncMembersFromAuth"))]
75 pub sync_members_from_auth: bool,
76}
77
78#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
79#[serde(deny_unknown_fields)]
80pub struct OrganizationDocument {
81 #[serde(rename(deserialize = "_id"))]
82 pub organization_id: String,
83 #[serde(rename(deserialize = "orgDisplayName"))]
84 pub display_name: Option<String>,
85 #[serde(rename(deserialize = "orgDesc"))]
86 pub desc: Option<String>,
87 #[serde(rename(deserialize = "orgShortName"))]
88 pub short_name: Option<String>,
89 #[serde(rename(deserialize = "orgAutoAddUsersWithDomainName"))]
90 pub auto_add_users_with_domain_name: Option<String>,
91 #[serde(rename(deserialize = "orgWebsite"))]
92 pub website: Option<String>,
93 #[serde(rename(deserialize = "orgIsActive"))]
94 pub is_active: Option<bool>,
95 #[serde(rename(deserialize = "orgSharedTemplates"))]
96 pub shared_templates: Option<bool>,
97 #[serde(rename(deserialize = "orgPropagateMembersToBoards"))]
98 pub propagate_members_to_boards: Option<bool>,
99 #[serde(rename(deserialize = "orgSyncMembersFromAuth"))]
100 pub sync_members_from_auth: Option<bool>,
101 #[serde(rename(deserialize = "orgDomains"))]
102 pub domains: Option<String>,
103 #[serde(rename(deserialize = "orgProductName"))]
104 pub product_name: Option<String>,
105 #[serde(rename(deserialize = "orgThemeColor"))]
106 pub theme_color: Option<String>,
107 #[serde(rename(deserialize = "orgThemeCustomColors"))]
108 pub theme_custom_colors: Option<Vec<Option<String>>>,
109 #[serde(rename(deserialize = "orgCustomLoginLogoImageUrl"))]
110 pub custom_login_logo_image_url: Option<String>,
111 #[serde(rename(deserialize = "orgCustomLoginLogoLinkUrl"))]
112 pub custom_login_logo_link_url: Option<String>,
113 #[serde(rename(deserialize = "orgTextBelowCustomLoginLogo"))]
114 pub text_below_custom_login_logo: Option<String>,
115 #[serde(rename(deserialize = "orgCustomTopLeftCornerLogoImageUrl"))]
116 pub custom_top_left_corner_logo_image_url: Option<String>,
117 #[serde(rename(deserialize = "orgCustomTopLeftCornerLogoLinkUrl"))]
118 pub custom_top_left_corner_logo_link_url: Option<String>,
119 #[serde(rename(deserialize = "orgCustomHelpLinkUrl"))]
120 pub custom_help_link_url: Option<String>,
121 #[serde(rename(deserialize = "orgLegalNotice"))]
122 pub legal_notice: Option<String>,
123 #[serde(rename(deserialize = "createdAt"), deserialize_with = "date")]
124 pub created_at: String,
125 #[serde(rename(deserialize = "modifiedAt"), deserialize_with = "date")]
126 pub modified_at: String,
127}
128
129#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
130#[serde(deny_unknown_fields)]
131pub struct TeamDocument {
132 #[serde(rename(deserialize = "_id"))]
133 pub team_id: String,
134 #[serde(rename(deserialize = "teamDisplayName"))]
135 pub display_name: Option<String>,
136 #[serde(rename(deserialize = "teamDesc"))]
137 pub desc: Option<String>,
138 #[serde(rename(deserialize = "teamShortName"))]
139 pub short_name: Option<String>,
140 #[serde(rename(deserialize = "teamWebsite"))]
141 pub website: Option<String>,
142 #[serde(rename(deserialize = "teamIsActive"))]
143 pub is_active: Option<bool>,
144 #[serde(rename(deserialize = "teamSharedTemplates"))]
145 pub shared_templates: Option<bool>,
146 #[serde(rename(deserialize = "teamPropagateMembersToBoards"))]
147 pub propagate_members_to_boards: Option<bool>,
148 #[serde(rename(deserialize = "teamSyncMembersFromAuth"))]
149 pub sync_members_from_auth: Option<bool>,
150 #[serde(rename(deserialize = "createdAt"), deserialize_with = "date")]
151 pub created_at: String,
152 #[serde(rename(deserialize = "modifiedAt"), deserialize_with = "date")]
153 pub modified_at: String,
154}
155
156fn date<'de, D: serde::Deserializer<'de>>(d: D) -> Result<String, D::Error> {
157 let value = String::deserialize(d)?;
158 validate_response_date(&value)
159 .map_err(|()| serde::de::Error::custom("invalid administrator date-time"))?;
160 Ok(value)
161}
162
163#[derive(Clone, Debug, Default, Serialize)]
164pub struct UpdateOrganizationFeatures {
165 #[serde(rename = "orgSharedTemplates", skip_serializing_if = "Option::is_none")]
166 pub shared_templates: Option<bool>,
167 #[serde(
168 rename = "orgPropagateMembersToBoards",
169 skip_serializing_if = "Option::is_none"
170 )]
171 pub propagate_members_to_boards: Option<bool>,
172 #[serde(
173 rename = "orgSyncMembersFromAuth",
174 skip_serializing_if = "Option::is_none"
175 )]
176 pub sync_members_from_auth: Option<bool>,
177}
178
179#[derive(Clone, Debug, Default, Serialize)]
180pub struct UpdateTeamFeatures {
181 #[serde(
182 rename = "teamSharedTemplates",
183 skip_serializing_if = "Option::is_none"
184 )]
185 pub shared_templates: Option<bool>,
186 #[serde(
187 rename = "teamPropagateMembersToBoards",
188 skip_serializing_if = "Option::is_none"
189 )]
190 pub propagate_members_to_boards: Option<bool>,
191 #[serde(
192 rename = "teamSyncMembersFromAuth",
193 skip_serializing_if = "Option::is_none"
194 )]
195 pub sync_members_from_auth: Option<bool>,
196}
197
198#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
199#[serde(deny_unknown_fields)]
200pub struct FeatureUpdateResult {
201 pub updated: u64,
202}
203
204#[derive(Serialize)]
205struct UpdateAllFeatures {
206 field: &'static str,
207 value: bool,
208}
209
210impl WekanClient {
211 pub async fn admin_domains(
212 &self,
213 token: &SecretString,
214 ) -> Result<Vec<DomainCount>, ClientError> {
215 self.admin_people_list("domains", token, "admin domain list")
216 .await
217 }
218 pub async fn admin_organizations(
219 &self,
220 token: &SecretString,
221 ) -> Result<Vec<OrganizationSummary>, ClientError> {
222 self.admin_people_list("orgs", token, "admin org list")
223 .await
224 }
225 pub async fn admin_teams(&self, token: &SecretString) -> Result<Vec<TeamSummary>, ClientError> {
226 self.admin_people_list("teams", token, "admin team list")
227 .await
228 }
229 pub async fn update_organization_features(
230 &self,
231 id: &str,
232 request: &UpdateOrganizationFeatures,
233 token: &SecretString,
234 ) -> Result<OrganizationDocument, ClientError> {
235 let document: OrganizationDocument = self
236 .admin_people_update(
237 "orgs",
238 Some(id),
239 request,
240 token,
241 "admin org features update",
242 )
243 .await?;
244 check_id(&document.organization_id, id)?;
245 Ok(document)
246 }
247 pub async fn update_team_features(
248 &self,
249 id: &str,
250 request: &UpdateTeamFeatures,
251 token: &SecretString,
252 ) -> Result<TeamDocument, ClientError> {
253 let document: TeamDocument = self
254 .admin_people_update(
255 "teams",
256 Some(id),
257 request,
258 token,
259 "admin team features update",
260 )
261 .await?;
262 check_id(&document.team_id, id)?;
263 Ok(document)
264 }
265 pub async fn update_all_organization_features(
266 &self,
267 field: FeatureField,
268 value: bool,
269 token: &SecretString,
270 ) -> Result<FeatureUpdateResult, ClientError> {
271 self.admin_people_update(
272 "orgs",
273 None,
274 &UpdateAllFeatures {
275 field: field.org_field(),
276 value,
277 },
278 token,
279 "admin org features update-all",
280 )
281 .await
282 }
283 pub async fn update_all_team_features(
284 &self,
285 field: FeatureField,
286 value: bool,
287 token: &SecretString,
288 ) -> Result<FeatureUpdateResult, ClientError> {
289 self.admin_people_update(
290 "teams",
291 None,
292 &UpdateAllFeatures {
293 field: field.team_field(),
294 value,
295 },
296 token,
297 "admin team features update-all",
298 )
299 .await
300 }
301
302 async fn admin_people_list<T: DeserializeOwned>(
303 &self,
304 resource: &str,
305 token: &SecretString,
306 operation: &str,
307 ) -> Result<Vec<T>, ClientError> {
308 let body = self
309 .execute_success_request_with_limit(
310 self.http
311 .request(
312 Method::GET,
313 self.admin_people_endpoint(resource, None, false)?,
314 )
315 .header(ACCEPT, "application/json")
316 .bearer_auth(token.expose_secret()),
317 MAX_RESPONSE_BYTES,
318 )
319 .await?;
320 let entries: Vec<Object<T>> = decode_success(&body, operation, true)?;
321 Ok(entries.into_iter().map(|entry| entry.0).collect())
322 }
323 async fn admin_people_update<T: DeserializeOwned>(
324 &self,
325 resource: &str,
326 id: Option<&str>,
327 request: &impl Serialize,
328 token: &SecretString,
329 operation: &str,
330 ) -> Result<T, ClientError> {
331 let body = self
332 .execute_success_request_with_limit(
333 self.http
334 .request(Method::PUT, self.admin_people_endpoint(resource, id, true)?)
335 .header(ACCEPT, "application/json")
336 .bearer_auth(token.expose_secret())
337 .json(request),
338 MAX_RESPONSE_BYTES,
339 )
340 .await?;
341 decode_success::<Object<T>>(&body, operation, true).map(|entry| entry.0)
342 }
343 fn admin_people_endpoint(
344 &self,
345 resource: &str,
346 id: Option<&str>,
347 features: bool,
348 ) -> Result<reqwest::Url, ClientError> {
349 if id.is_some_and(|id| id.trim().is_empty() || matches!(id, "." | "..")) {
350 return Err(protocol(
351 "administrator IDs must be nonempty and not dot-only; no request was sent",
352 false,
353 ));
354 }
355 let mut endpoint = self.server().join("api/admin").map_err(|_| {
356 protocol(
357 "could not construct administrator endpoint; no request was sent",
358 false,
359 )
360 })?;
361 let mut segments = endpoint.path_segments_mut().map_err(|()| {
362 protocol(
363 "could not construct administrator path; no request was sent",
364 false,
365 )
366 })?;
367 segments.push(resource);
368 if let Some(id) = id {
369 segments.push(id);
370 }
371 if features {
372 segments.push("features");
373 }
374 drop(segments);
375 Ok(endpoint)
376 }
377}
378
379fn check_id(actual: &str, expected: &str) -> Result<(), ClientError> {
380 if actual != expected {
381 return Err(protocol(
382 "Wekan returned a different administrator resource ID than requested",
383 true,
384 ));
385 }
386 Ok(())
387}
388fn protocol(message: &str, success_status_received: bool) -> ClientError {
389 ClientError::Protocol {
390 message: message.into(),
391 diagnostic: None,
392 success_status_received,
393 }
394}