1use std::collections::HashMap;
16
17use reqwest::{multipart, RequestBuilder, StatusCode};
18use serde::de::DeserializeOwned;
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21
22use crate::config::AuthConfig;
23use crate::error::{CasdoorError, Result};
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
27pub struct Response {
28 #[serde(default)]
29 pub status: String,
30 #[serde(default)]
31 pub msg: String,
32 #[serde(default)]
33 pub sub: String,
34 #[serde(default)]
35 pub name: String,
36 #[serde(default)]
37 pub data: Value,
38 #[serde(default)]
39 pub data2: Value,
40 #[serde(default)]
41 pub data3: Value,
42}
43
44pub enum PostBody<'a> {
46 Raw(Vec<u8>),
49 Form(HashMap<String, String>),
51 File(&'a [u8]),
53}
54
55#[derive(Debug, Clone)]
76pub struct Client {
77 pub config: AuthConfig,
79 pub custom_headers: HashMap<String, String>,
81 pub access_token: Option<String>,
87 http: reqwest::Client,
88}
89
90impl Client {
91 pub fn new(
93 endpoint: impl Into<String>,
94 client_id: impl Into<String>,
95 client_secret: impl Into<String>,
96 certificate: impl Into<String>,
97 organization_name: impl Into<String>,
98 application_name: impl Into<String>,
99 ) -> Self {
100 Self::from_config(AuthConfig::new(
101 endpoint,
102 client_id,
103 client_secret,
104 certificate,
105 organization_name,
106 application_name,
107 ))
108 }
109
110 pub fn from_config(config: AuthConfig) -> Self {
112 Self {
113 config,
114 custom_headers: HashMap::new(),
115 access_token: None,
116 http: reqwest::Client::new(),
117 }
118 }
119
120 pub fn from_toml(path: &str) -> Result<Self> {
122 Ok(Self::from_config(AuthConfig::from_toml(path)?))
123 }
124
125 pub fn with_access_token(&self, access_token: impl Into<String>) -> Self {
146 Self {
147 access_token: Some(access_token.into()),
148 ..self.clone()
149 }
150 }
151
152 pub fn with_custom_header(&self, key: impl Into<String>, value: impl Into<String>) -> Self {
154 let mut client = self.clone();
155 client.custom_headers.insert(key.into(), value.into());
156 client
157 }
158
159 pub fn with_http_client(&self, http: reqwest::Client) -> Self {
162 Self {
163 http,
164 ..self.clone()
165 }
166 }
167
168 pub(crate) fn http(&self) -> &reqwest::Client {
170 &self.http
171 }
172
173 pub fn get_url(&self, action: &str, query: &[(&str, &str)]) -> String {
176 let query = query
177 .iter()
178 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
179 .collect::<Vec<_>>()
180 .join("&");
181
182 format!("{}/api/{}?{}", self.config.endpoint, action, query)
183 }
184
185 pub fn get_id(&self, name: &str) -> String {
190 get_id(name, &self.config.organization_name)
191 }
192
193 pub async fn do_get_response(&self, url: &str) -> Result<Response> {
195 let bytes = self.do_get_bytes_raw_without_check(url).await?;
196 let response: Response = serde_json::from_slice(&bytes)?;
197
198 if response.status != "ok" {
199 return Err(CasdoorError::Casdoor(response.msg));
200 }
201
202 Ok(response)
203 }
204
205 pub async fn do_get_bytes_raw(&self, url: &str) -> Result<Vec<u8>> {
207 let bytes = self.do_get_bytes_raw_without_check(url).await?;
208
209 if let Ok(response) = serde_json::from_slice::<Response>(&bytes) {
210 if response.status == "error" {
211 return Err(CasdoorError::Casdoor(response.msg));
212 }
213 }
214
215 Ok(bytes)
216 }
217
218 pub async fn do_post(
220 &self,
221 action: &str,
222 query: &[(&str, &str)],
223 body: PostBody<'_>,
224 ) -> Result<Response> {
225 let url = self.get_url(action, query);
226 let bytes = self.do_post_bytes_raw(&url, body).await?;
227
228 let response: Response = serde_json::from_slice(&bytes)?;
229 if response.status != "ok" {
230 return Err(CasdoorError::Casdoor(response.msg));
231 }
232
233 Ok(response)
234 }
235
236 pub async fn do_post_bytes_raw(&self, url: &str, body: PostBody<'_>) -> Result<Vec<u8>> {
238 let mut req = self.http.post(url);
239
240 req = match body {
241 PostBody::Raw(bytes) => req
242 .header(reqwest::header::CONTENT_TYPE, "text/plain;charset=UTF-8")
243 .body(bytes),
244 PostBody::Form(fields) => {
245 let form = fields
246 .into_iter()
247 .fold(multipart::Form::new(), |form, (k, v)| form.text(k, v));
248 req.multipart(form)
249 }
250 PostBody::File(bytes) => {
251 let part = multipart::Part::bytes(bytes.to_vec()).file_name("file");
252 req.multipart(multipart::Form::new().part("file", part))
253 }
254 };
255
256 read_body(self.set_headers(req).send().await?).await
257 }
258
259 async fn do_get_bytes_raw_without_check(&self, url: &str) -> Result<Vec<u8>> {
260 let req = self.set_headers(self.http.get(url));
261 read_body(req.send().await?).await
262 }
263
264 fn set_headers(&self, req: RequestBuilder) -> RequestBuilder {
269 let mut req = match &self.access_token {
270 Some(token) if !token.is_empty() => req.bearer_auth(token),
271 _ => req.basic_auth(&self.config.client_id, Some(&self.config.client_secret)),
272 };
273
274 for (key, value) in &self.custom_headers {
275 req = req.header(key, value);
276 }
277
278 req
279 }
280
281 pub(crate) async fn get_object<T: DeserializeOwned>(
284 &self,
285 action: &str,
286 query: &[(&str, &str)],
287 ) -> Result<Option<T>> {
288 let url = self.get_url(action, query);
289 let response = self.do_get_response(&url).await?;
290
291 if response.data.is_null() {
292 return Ok(None);
293 }
294
295 Ok(Some(serde_json::from_value(response.data)?))
296 }
297
298 pub(crate) async fn get_list<T: DeserializeOwned>(
300 &self,
301 action: &str,
302 query: &[(&str, &str)],
303 ) -> Result<Vec<T>> {
304 let url = self.get_url(action, query);
305 let response = self.do_get_response(&url).await?;
306
307 if response.data.is_null() {
308 return Ok(Vec::new());
309 }
310
311 Ok(serde_json::from_value(response.data)?)
312 }
313
314 pub(crate) async fn get_pagination<T: DeserializeOwned>(
317 &self,
318 action: &str,
319 owner: &str,
320 p: i32,
321 page_size: i32,
322 query: &[(&str, &str)],
323 ) -> Result<(Vec<T>, i32)> {
324 let (p, page_size) = (p.to_string(), page_size.to_string());
325
326 let mut full_query = vec![
327 ("owner", owner),
328 ("p", p.as_str()),
329 ("pageSize", page_size.as_str()),
330 ];
331 full_query.extend_from_slice(query);
332
333 let url = self.get_url(action, &full_query);
334 let response = self.do_get_response(&url).await?;
335
336 let objects = if response.data.is_null() {
337 Vec::new()
338 } else {
339 serde_json::from_value(response.data).map_err(|_| CasdoorError::InvalidData)?
340 };
341
342 let count = response.data2.as_f64().ok_or(CasdoorError::InvalidData)? as i32;
343
344 Ok((objects, count))
345 }
346
347 pub(crate) async fn modify_object<T: Serialize>(
350 &self,
351 action: &str,
352 id: &str,
353 object: &T,
354 columns: &[&str],
355 ) -> Result<(Response, bool)> {
356 let columns = columns.join(",");
357
358 let mut query = vec![("id", id)];
359 if !columns.is_empty() {
360 query.push(("columns", columns.as_str()));
361 }
362
363 let post_bytes = serde_json::to_vec(object)?;
364 let response = self
365 .do_post(action, &query, PostBody::Raw(post_bytes))
366 .await?;
367
368 let affected = response.data.as_str() == Some("Affected");
369 Ok((response, affected))
370 }
371}
372
373pub(crate) fn get_admin_id(name: &str) -> String {
376 get_id(name, "admin")
377}
378
379fn get_id(name: &str, default_owner: &str) -> String {
380 if name.contains('/') {
381 return name.to_string();
382 }
383
384 format!("{default_owner}/{name}")
385}
386
387pub(crate) fn get_owner(owner: &str, default_owner: &str) -> String {
392 if owner.is_empty() {
393 return default_owner.to_string();
394 }
395
396 owner.to_string()
397}
398
399async fn read_body(response: reqwest::Response) -> Result<Vec<u8>> {
400 let status = response.status();
401 let bytes = response.bytes().await?;
402
403 if status != StatusCode::OK && status != StatusCode::FORBIDDEN {
404 return Err(CasdoorError::HttpStatus {
405 status: status.as_u16(),
406 body: String::from_utf8_lossy(&bytes).into_owned(),
407 });
408 }
409
410 Ok(bytes.to_vec())
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 fn client() -> Client {
418 Client::new(
419 "http://localhost:8000",
420 "client-id",
421 "client-secret",
422 "",
423 "built-in",
424 "app-built-in",
425 )
426 }
427
428 #[test]
429 fn get_url_escapes_the_query_parameters() {
430 assert_eq!(
431 client().get_url("get-user", &[("id", "built-in/admin")]),
432 "http://localhost:8000/api/get-user?id=built-in%2Fadmin"
433 );
434
435 assert_eq!(
436 client().get_url("get-global-users", &[]),
437 "http://localhost:8000/api/get-global-users?"
438 );
439 }
440
441 #[test]
442 fn get_id_only_adds_the_owner_of_an_unqualified_name() {
443 assert_eq!(client().get_id("admin"), "built-in/admin");
444 assert_eq!(client().get_id("other-org/alice"), "other-org/alice");
445 assert_eq!(get_admin_id("app-built-in"), "admin/app-built-in");
446 }
447
448 #[test]
449 fn get_owner_falls_back_to_the_default_owner() {
450 assert_eq!(get_owner("", "built-in"), "built-in");
451 assert_eq!(get_owner("other-org", "built-in"), "other-org");
452 }
453
454 #[test]
455 fn with_access_token_keeps_the_original_client_unchanged() {
456 let client = client();
457 let user_client = client.with_access_token("access-token");
458
459 assert_eq!(user_client.access_token.as_deref(), Some("access-token"));
460 assert_eq!(client.access_token, None);
461 }
462}