Skip to main content

wekan_cli/client/admin/
problems.rs

1use crate::client::{ClientError, WekanClient, transport::decode_success};
2use reqwest::{Method, header::ACCEPT};
3use secrecy::{ExposeSecret, SecretString};
4use serde::{Deserialize, Serialize};
5use serde_json::Number;
6use std::collections::BTreeMap;
7
8pub const PROBLEM_STREAMS: [&str; 9] = [
9    "security",
10    "speed",
11    "tests",
12    "cpu",
13    "ram",
14    "disk",
15    "database",
16    "integrity",
17    "mail",
18];
19#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
20#[serde(deny_unknown_fields)]
21pub struct ProgressProblem {
22    pub kind: String,
23    pub active: bool,
24    pub message: String,
25}
26#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
27#[serde(deny_unknown_fields)]
28pub struct DetectedProblem {
29    pub id: String,
30    pub severity: String,
31    pub title: String,
32    pub detail: String,
33    pub count: Option<Number>,
34}
35#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
36#[serde(deny_unknown_fields)]
37pub struct NewProblemCount {
38    pub stream: String,
39    pub count: u64,
40}
41#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
42#[serde(
43    deny_unknown_fields,
44    rename_all(deserialize = "camelCase", serialize = "snake_case")
45)]
46pub struct ProblemsOverview {
47    pub any_in_progress: bool,
48    pub in_progress: Vec<ProgressProblem>,
49    pub any_problems: bool,
50    pub problems: Vec<DetectedProblem>,
51    pub new_problems: Vec<NewProblemCount>,
52    pub streams: Vec<String>,
53}
54#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
55#[serde(deny_unknown_fields)]
56pub struct EventActor {
57    pub kind: String,
58    pub value: String,
59    pub count: Number,
60    pub at: String,
61    pub family: Option<String>,
62}
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64#[serde(deny_unknown_fields)]
65pub struct EventLocation {
66    pub country: Option<String>,
67    pub city: Option<String>,
68    pub region: Option<String>,
69    pub latitude: Option<Number>,
70    pub longitude: Option<Number>,
71    pub via: Option<String>,
72}
73#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
74#[serde(
75    deny_unknown_fields,
76    rename_all(deserialize = "camelCase", serialize = "snake_case")
77)]
78pub struct ProblemEvent {
79    #[serde(rename(deserialize = "_id", serialize = "event_id"))]
80    pub event_id: String,
81    pub stream: String,
82    pub at: String,
83    pub severity: Option<String>,
84    pub category: Option<String>,
85    pub bleed: Option<String>,
86    pub action: Option<String>,
87    pub source: Option<String>,
88    pub cwe: Option<String>,
89    pub user_id: Option<String>,
90    pub username: Option<String>,
91    pub ip: Option<String>,
92    pub ipv4: Option<String>,
93    pub ipv6: Option<String>,
94    pub location: Option<EventLocation>,
95    pub count: Option<Number>,
96    pub first_at: Option<String>,
97    pub actors: Option<BTreeMap<String, EventActor>>,
98    pub actors_overflow: Option<Number>,
99    pub detail: Option<String>,
100    #[serde(rename(deserialize = "type", serialize = "event_type"))]
101    pub event_type: Option<String>,
102    pub db: Option<String>,
103    pub kind: Option<String>,
104    pub message: Option<String>,
105    pub api: Option<String>,
106    pub api_user_id: Option<String>,
107}
108#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
109#[serde(deny_unknown_fields)]
110pub struct ProblemPage {
111    pub stream: String,
112    pub total: u64,
113    pub limit: u64,
114    pub skip: u64,
115    pub rows: Vec<ProblemEvent>,
116}
117#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
118#[serde(deny_unknown_fields)]
119pub struct ProblemAcknowledgment {
120    pub acknowledged: String,
121}
122
123impl WekanClient {
124    pub async fn problems_overview(
125        &self,
126        token: &SecretString,
127    ) -> Result<ProblemsOverview, ClientError> {
128        self.admin_settings(
129            Method::GET,
130            "api/admin/problems",
131            None,
132            token,
133            "admin problems overview",
134        )
135        .await
136    }
137    pub async fn problem_stream(
138        &self,
139        stream: &str,
140        limit: u64,
141        skip: u64,
142        search: Option<&str>,
143        token: &SecretString,
144    ) -> Result<ProblemPage, ClientError> {
145        let mut url =
146            self.server()
147                .join("api/admin/problems")
148                .map_err(|_| ClientError::Protocol {
149                    message: "could not construct problem stream endpoint".into(),
150                    diagnostic: None,
151                    success_status_received: false,
152                })?;
153        url.path_segments_mut()
154            .expect("HTTP URL supports path segments")
155            .push(stream);
156        url.query_pairs_mut()
157            .append_pair("limit", &limit.to_string())
158            .append_pair("skip", &skip.to_string());
159        if let Some(search) = search {
160            url.query_pairs_mut().append_pair("search", search);
161        }
162        let body = self
163            .execute_success_request_with_limit(
164                self.http
165                    .get(url)
166                    .header(ACCEPT, "application/json")
167                    .bearer_auth(token.expose_secret()),
168                64 * 1024 * 1024,
169            )
170            .await?;
171        let page: ProblemPage = decode_success(&body, "admin problem stream", true)?;
172        if page.stream != stream {
173            return Err(ClientError::Protocol {
174                message: "the returned problem stream did not match the requested stream".into(),
175                diagnostic: None,
176                success_status_received: true,
177            });
178        }
179        Ok(page)
180    }
181    pub async fn acknowledge_problem_stream(
182        &self,
183        stream: &str,
184        token: &SecretString,
185    ) -> Result<ProblemAcknowledgment, ClientError> {
186        let mut url =
187            self.server()
188                .join("api/admin/problems")
189                .map_err(|_| ClientError::Protocol {
190                    message: "could not construct problem acknowledgment endpoint".into(),
191                    diagnostic: None,
192                    success_status_received: false,
193                })?;
194        url.path_segments_mut()
195            .expect("HTTP URL supports path segments")
196            .extend([stream, "acknowledge"]);
197        let body = self
198            .execute_success_request(
199                self.http
200                    .post(url)
201                    .header(ACCEPT, "application/json")
202                    .bearer_auth(token.expose_secret()),
203            )
204            .await?;
205        let response: ProblemAcknowledgment =
206            decode_success(&body, "admin problem acknowledgment", true)?;
207        if response.acknowledged != stream {
208            return Err(ClientError::Protocol {
209                message: "the acknowledged problem stream did not match the request".into(),
210                diagnostic: None,
211                success_status_received: true,
212            });
213        }
214        Ok(response)
215    }
216}