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}