Skip to main content

wekan_cli/client/boards/
members.rs

1use reqwest::header::ACCEPT;
2use secrecy::{ExposeSecret, SecretString};
3use serde::Serialize;
4
5use super::{BoardSummary, ClientError, WekanClient, validate_board_summaries};
6use crate::client::transport::decode_success;
7
8/// Named roles take precedence over individual flags in Wekan v11.06.
9#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
10#[serde(rename_all = "camelCase")]
11pub struct BoardMemberPermissions {
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub role: Option<String>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub is_admin: Option<bool>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub is_no_comments: Option<bool>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub is_comment_only: Option<bool>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub is_worker: Option<bool>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub is_normal_assigned_only: Option<bool>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub is_comment_assigned_only: Option<bool>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub is_read_only: Option<bool>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub is_read_assigned_only: Option<bool>,
30}
31
32#[derive(Serialize)]
33struct AddRequest<'a> {
34    action: &'static str,
35    #[serde(flatten)]
36    permissions: &'a BoardMemberPermissions,
37}
38
39#[derive(Serialize)]
40struct RemoveRequest {
41    action: &'static str,
42}
43
44impl WekanClient {
45    pub async fn add_board_member(
46        &self,
47        board: &str,
48        user: &str,
49        permissions: &BoardMemberPermissions,
50        token: &SecretString,
51    ) -> Result<Vec<BoardSummary>, ClientError> {
52        let body = self
53            .execute_success_request(
54                self.http
55                    .post(self.board_member_endpoint(board, user, Some("add"))?)
56                    .header(ACCEPT, "application/json")
57                    .bearer_auth(token.expose_secret())
58                    .json(&AddRequest {
59                        action: "add",
60                        permissions,
61                    }),
62            )
63            .await?;
64        validate_board_summaries(
65            decode_success(&body, "board member add", true)?,
66            "board member add",
67        )
68    }
69
70    pub async fn remove_board_member(
71        &self,
72        board: &str,
73        user: &str,
74        token: &SecretString,
75    ) -> Result<Vec<BoardSummary>, ClientError> {
76        let body = self
77            .execute_success_request(
78                self.http
79                    .post(self.board_member_endpoint(board, user, Some("remove"))?)
80                    .header(ACCEPT, "application/json")
81                    .bearer_auth(token.expose_secret())
82                    .json(&RemoveRequest { action: "remove" }),
83            )
84            .await?;
85        validate_board_summaries(
86            decode_success(&body, "board member remove", true)?,
87            "board member remove",
88        )
89    }
90
91    pub async fn update_board_member(
92        &self,
93        board: &str,
94        user: &str,
95        permissions: &BoardMemberPermissions,
96        token: &SecretString,
97    ) -> Result<u64, ClientError> {
98        let body = self
99            .execute_success_request(
100                self.http
101                    .post(self.board_member_endpoint(board, user, None)?)
102                    .header(ACCEPT, "application/json")
103                    .bearer_auth(token.expose_secret())
104                    .json(permissions),
105            )
106            .await?;
107        decode_success(&body, "board member update", true)
108    }
109
110    fn board_member_endpoint(
111        &self,
112        board: &str,
113        user: &str,
114        suffix: Option<&str>,
115    ) -> Result<reqwest::Url, ClientError> {
116        for id in [board, user] {
117            validate_id(id)?;
118        }
119        let mut endpoint = self.board_endpoint(board, Some("members"))?;
120        let mut segments = endpoint
121            .path_segments_mut()
122            .expect("the server URL supports path segments");
123        segments.push(user);
124        if let Some(suffix) = suffix {
125            segments.push(suffix);
126        }
127        drop(segments);
128        Ok(endpoint)
129    }
130}
131
132pub(crate) fn validate_id(id: &str) -> Result<(), ClientError> {
133    if crate::client::trim_javascript_whitespace(id).is_empty() || matches!(id, "." | "..") {
134        return Err(ClientError::Protocol {
135            diagnostic: None,
136            message: "membership identifiers must be nonblank and cannot be dot-only; no request was sent".into(),
137            success_status_received: false,
138        });
139    }
140    Ok(())
141}