wekan_cli/client/boards/
members.rs1use 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#[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}