Skip to main content

wekan_cli/client/users/
tokens.rs

1use reqwest::{StatusCode, header::ACCEPT};
2use secrecy::{ExposeSecret, SecretString};
3use serde::{Deserialize, Serialize, Serializer};
4
5use crate::client::{
6    ClientError, WekanClient,
7    decoding::{decode_value, parse_json},
8    transport::{decode_success, embedded_error},
9};
10
11#[derive(Debug, Deserialize)]
12#[serde(deny_unknown_fields)]
13pub struct CreatedUserToken {
14    #[serde(rename = "_id")]
15    pub user_id: String,
16    #[serde(rename = "authToken", deserialize_with = "deserialize_secret")]
17    pub auth_token: SecretString,
18}
19
20#[derive(Debug, Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct DeleteUserTokenRequest {
23    pub user_id: String,
24    #[serde(
25        skip_serializing_if = "Option::is_none",
26        serialize_with = "serialize_token"
27    )]
28    pub token: Option<SecretString>,
29}
30
31fn serialize_token<S: Serializer>(value: &Option<SecretString>, s: S) -> Result<S::Ok, S::Error> {
32    value.as_ref().map(ExposeSecret::expose_secret).serialize(s)
33}
34
35#[derive(Debug, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct UserTokenDeletion {
38    // Wekan includes the supplied token in this message. Keep Debug secret-safe.
39    #[serde(deserialize_with = "deserialize_secret")]
40    pub message: SecretString,
41}
42
43fn deserialize_secret<'de, D: serde::Deserializer<'de>>(d: D) -> Result<SecretString, D::Error> {
44    String::deserialize(d).map(SecretString::from)
45}
46
47#[derive(Serialize)]
48struct CreateUserTokenRequest<'a> {
49    reason: &'a str,
50}
51
52impl WekanClient {
53    pub async fn create_user_token(
54        &self,
55        user_id: &str,
56        reason: &str,
57        credential: &SecretString,
58    ) -> Result<CreatedUserToken, ClientError> {
59        let mut endpoint = self.endpoint("api/createtoken", "token creation")?;
60        endpoint
61            .path_segments_mut()
62            .map_err(|()| ClientError::Protocol {
63                diagnostic: None,
64                message: "could not add the token user ID to the endpoint".to_owned(),
65                success_status_received: false,
66            })?
67            .push(user_id);
68        let body = self
69            .execute_success_request(
70                self.http
71                    .post(endpoint)
72                    .header(ACCEPT, "application/json")
73                    .bearer_auth(credential.expose_secret())
74                    .json(&CreateUserTokenRequest { reason }),
75            )
76            .await?;
77        let response: CreatedUserToken = decode_success(&body, "token creation", true)?;
78        if response.user_id.is_empty()
79            || response.user_id != user_id
80            || response.auth_token.expose_secret().is_empty()
81        {
82            return Err(ClientError::Protocol {
83                diagnostic: None,
84                message:
85                    "the token creation response contained an empty token or an unexpected user ID"
86                        .to_owned(),
87                success_status_received: true,
88            });
89        }
90        Ok(response)
91    }
92
93    pub async fn delete_user_token(
94        &self,
95        request: &DeleteUserTokenRequest,
96        credential: &SecretString,
97    ) -> Result<UserTokenDeletion, ClientError> {
98        let body = self
99            .execute_success_request(
100                self.http
101                    .post(self.endpoint("api/deletetoken", "token deletion")?)
102                    .header(ACCEPT, "application/json")
103                    .bearer_auth(credential.expose_secret())
104                    .json(request),
105            )
106            .await?;
107        let value = parse_json(&body, "token deletion")?;
108        // A message-only object is this route's success shape. The generic
109        // decoder treats message as an embedded-error marker on other routes.
110        let success = decode_value::<UserTokenDeletion>(value.clone(), "token deletion", true);
111        let response = match success {
112            Ok(response) => response,
113            Err(error) => {
114                if value.as_object().is_some_and(|o| {
115                    ["error", "reason", "statusCode", "errorType"]
116                        .iter()
117                        .any(|key| o.contains_key(*key))
118                }) {
119                    if let Some(embedded) = embedded_error(&value, StatusCode::OK)? {
120                        return Err(embedded);
121                    }
122                }
123                return Err(error);
124            }
125        };
126        Ok(response)
127    }
128}