use log::{debug, info};
use server::User;
use url::Url;
pub mod projects;
pub mod security;
pub mod server;
pub mod snapshot;
pub use server::ServerInfo;
use crate::{KONARR_VERSION, KonarrError};
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Pagination<T>
where
T: serde::Serialize + Send,
{
pub data: Vec<T>,
pub pages: u64,
pub total: u64,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct ApiError {
pub message: String,
pub details: Option<String>,
pub status: u16,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum ApiResponse<T>
where
T: serde::Serialize + Send,
{
Ok(T),
Error(ApiError),
}
#[derive(Debug, Clone)]
pub struct KonarrClient {
version: String,
url: Url,
client: reqwest::Client,
token: Option<String>,
credentials: Option<(String, String)>,
}
impl KonarrClient {
pub fn new(url: impl Into<Url>) -> Self {
let url = url.into();
let client = reqwest::Client::builder()
.cookie_store(true)
.build()
.unwrap();
log::debug!("Setting up Konarr Client for {}", url);
Self {
version: KONARR_VERSION.to_string(),
client,
url,
token: None,
credentials: None,
}
}
pub fn init() -> KonarrClientBuilder {
KonarrClientBuilder::new()
}
pub fn version(&self) -> &str {
&self.version
}
pub fn url(&self) -> &Url {
&self.url
}
pub(crate) fn base(&self, path: &str) -> Result<Url, url::ParseError> {
let base = self.url.path().trim_end_matches('/');
self.url.join(&format!("{}{}", base, path))
}
pub async fn is_authenticated(&self) -> bool {
self.server()
.await
.map(|svr| svr.user.is_some())
.unwrap_or(false)
}
pub async fn server(&self) -> Result<ServerInfo, crate::KonarrError> {
debug!("Getting Server Information");
self.get("/").await?.json().await.map_err(KonarrError::from)
}
pub async fn get_projects(
&self,
) -> Result<Pagination<projects::KonarrProject>, crate::KonarrError> {
projects::KonarrProjects::list(self).await
}
pub async fn user(&self) -> Result<Option<User>, crate::KonarrError> {
debug!("Getting User Information");
Ok(self.server().await?.user)
}
pub async fn login(&mut self) -> Result<(), KonarrError> {
if let Some((username, password)) = &self.credentials {
info!("Logging in as {}", username);
let response = self
.post(
"/auth/login",
&serde_json::json!({
"username": username,
"password": password,
}),
)
.await?;
if response.status().is_success() {
info!("Login Successful");
Ok(())
} else {
Err(KonarrError::UnknownError("Login Failed".to_string()))
}
} else {
Err(KonarrError::UnknownError(
"No Credentials Provided".to_string(),
))
}
}
pub async fn login_with_credentials(
&self,
username: &str,
password: &str,
) -> Result<(), KonarrError> {
self.client
.post(self.base("/auth/login")?)
.json(&serde_json::json!({
"username": username,
"password": password,
}))
.send()
.await?;
Ok(())
}
pub async fn get(&self, path: &str) -> Result<reqwest::Response, reqwest::Error> {
self.client
.get(self.base(path).unwrap())
.header("Authorization", self.token.clone().unwrap_or_default())
.send()
.await
}
pub async fn post<T>(&self, path: &str, json: T) -> Result<reqwest::Response, reqwest::Error>
where
T: serde::Serialize + Send,
{
self.client
.post(self.base(path).unwrap())
.header("Authorization", self.token.clone().unwrap_or_default())
.json(&json)
.send()
.await
}
pub async fn patch<T>(&self, path: &str, json: T) -> Result<reqwest::Response, reqwest::Error>
where
T: serde::Serialize + Send,
{
self.client
.patch(self.base(path).unwrap())
.header("Authorization", self.token.clone().unwrap_or_default())
.json(&json)
.send()
.await
}
pub async fn delete(&self, path: &str) -> Result<reqwest::Response, reqwest::Error> {
self.client
.delete(self.base(path).unwrap())
.header("Authorization", self.token.clone().unwrap_or_default())
.send()
.await
}
}
#[derive(Debug, Default)]
pub struct KonarrClientBuilder {
url: Option<Url>,
token: Option<String>,
credentials: Option<(String, String)>,
}
impl KonarrClientBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn base(mut self, url: impl Into<String>) -> Result<Self, crate::KonarrError> {
self.url = Some(Url::parse(&url.into())?);
Ok(self)
}
pub fn token(mut self, token: String) -> Self {
self.token = Some(token);
self
}
pub fn credentials(mut self, username: String, password: String) -> Self {
self.credentials = Some((username, password));
self
}
pub fn build(self) -> Result<KonarrClient, KonarrError> {
if let Some(url) = self.url {
let client = reqwest::Client::builder()
.cookie_store(true)
.timeout(std::time::Duration::from_secs(30))
.build()
.unwrap();
Ok(KonarrClient {
version: KONARR_VERSION.to_string(),
client,
url,
token: self.token,
credentials: self.credentials,
})
} else {
Err(KonarrError::UnknownError("Base URL not set".to_string()))
}
}
}