Skip to main content

wekan_cli/client/admin/
people.rs

1//! Site-admin domain reporting and organization/team feature operations, v11.06.
2use 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}