2021-04-21 11:13:51 -06:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
2021-04-13 12:14:07 -06:00
|
|
|
use chrono::Utc;
|
2021-04-21 11:13:51 -06:00
|
|
|
use eyre::{eyre, Result};
|
|
|
|
use reqwest::header::{HeaderMap, AUTHORIZATION, USER_AGENT};
|
|
|
|
use reqwest::{StatusCode, Url};
|
2021-04-20 10:07:11 -06:00
|
|
|
use sodiumoxide::crypto::secretbox;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-04-21 11:13:51 -06:00
|
|
|
use atuin_common::api::{
|
|
|
|
AddHistoryRequest, CountResponse, LoginResponse, RegisterResponse, SyncHistoryResponse,
|
|
|
|
};
|
2021-04-20 14:53:07 -06:00
|
|
|
use atuin_common::utils::hash_str;
|
|
|
|
|
2021-04-21 11:13:51 -06:00
|
|
|
use crate::encryption::{decode_key, decrypt};
|
2021-04-20 14:53:07 -06:00
|
|
|
use crate::history::History;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-04-21 11:13:51 -06:00
|
|
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
|
|
|
|
|
|
// TODO: remove all references to the encryption key from this
|
|
|
|
// It should be handled *elsewhere*
|
|
|
|
|
2021-04-13 12:14:07 -06:00
|
|
|
pub struct Client<'a> {
|
2021-04-20 10:07:11 -06:00
|
|
|
sync_addr: &'a str,
|
|
|
|
token: &'a str,
|
|
|
|
key: secretbox::Key,
|
|
|
|
client: reqwest::Client,
|
2021-04-13 12:14:07 -06:00
|
|
|
}
|
|
|
|
|
2021-04-21 11:13:51 -06:00
|
|
|
pub fn register(
|
|
|
|
address: &str,
|
|
|
|
username: &str,
|
|
|
|
email: &str,
|
|
|
|
password: &str,
|
|
|
|
) -> Result<RegisterResponse> {
|
|
|
|
let mut map = HashMap::new();
|
|
|
|
map.insert("username", username);
|
|
|
|
map.insert("email", email);
|
|
|
|
map.insert("password", password);
|
|
|
|
|
|
|
|
let url = format!("{}/user/{}", address, username);
|
|
|
|
let resp = reqwest::blocking::get(url)?;
|
|
|
|
|
|
|
|
if resp.status().is_success() {
|
|
|
|
return Err(eyre!("username already in use"));
|
|
|
|
}
|
|
|
|
|
|
|
|
let url = format!("{}/register", address);
|
|
|
|
let client = reqwest::blocking::Client::new();
|
|
|
|
let resp = client
|
|
|
|
.post(url)
|
|
|
|
.header(USER_AGENT, format!("atuin/{}", VERSION))
|
|
|
|
.json(&map)
|
|
|
|
.send()?;
|
|
|
|
|
|
|
|
if !resp.status().is_success() {
|
|
|
|
return Err(eyre!("failed to register user"));
|
|
|
|
}
|
|
|
|
|
|
|
|
let session = resp.json::<RegisterResponse>()?;
|
|
|
|
Ok(session)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn login(address: &str, username: &str, password: &str) -> Result<LoginResponse> {
|
|
|
|
let mut map = HashMap::new();
|
|
|
|
map.insert("username", username);
|
|
|
|
map.insert("password", password);
|
|
|
|
|
|
|
|
let url = format!("{}/login", address);
|
|
|
|
let client = reqwest::blocking::Client::new();
|
|
|
|
|
|
|
|
let resp = client
|
|
|
|
.post(url)
|
|
|
|
.header(USER_AGENT, format!("atuin/{}", VERSION))
|
|
|
|
.json(&map)
|
|
|
|
.send()?;
|
|
|
|
|
|
|
|
if resp.status() != reqwest::StatusCode::OK {
|
|
|
|
return Err(eyre!("invalid login details"));
|
|
|
|
}
|
|
|
|
|
|
|
|
let session = resp.json::<LoginResponse>()?;
|
|
|
|
Ok(session)
|
|
|
|
}
|
|
|
|
|
2021-04-13 12:14:07 -06:00
|
|
|
impl<'a> Client<'a> {
|
2021-04-21 11:13:51 -06:00
|
|
|
pub fn new(sync_addr: &'a str, token: &'a str, key: String) -> Result<Self> {
|
|
|
|
Ok(Client {
|
2021-04-20 10:07:11 -06:00
|
|
|
sync_addr,
|
|
|
|
token,
|
2021-04-21 11:13:51 -06:00
|
|
|
key: decode_key(key)?,
|
2021-04-20 10:07:11 -06:00
|
|
|
client: reqwest::Client::new(),
|
2021-04-21 11:13:51 -06:00
|
|
|
})
|
2021-04-13 12:14:07 -06:00
|
|
|
}
|
|
|
|
|
2021-04-20 10:07:11 -06:00
|
|
|
pub async fn count(&self) -> Result<i64> {
|
|
|
|
let url = format!("{}/sync/count", self.sync_addr);
|
|
|
|
let url = Url::parse(url.as_str())?;
|
|
|
|
let token = format!("Token {}", self.token);
|
|
|
|
let token = token.parse()?;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-04-20 10:07:11 -06:00
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
headers.insert(AUTHORIZATION, token);
|
|
|
|
|
2021-04-21 11:13:51 -06:00
|
|
|
let resp = self
|
|
|
|
.client
|
|
|
|
.get(url)
|
|
|
|
.header(USER_AGENT, format!("atuin/{}", VERSION))
|
|
|
|
.headers(headers)
|
|
|
|
.send()
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
if resp.status() != StatusCode::OK {
|
|
|
|
return Err(eyre!("failed to get count (are you logged in?)"));
|
|
|
|
}
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-04-20 10:07:11 -06:00
|
|
|
let count = resp.json::<CountResponse>().await?;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
|
|
|
Ok(count.count)
|
|
|
|
}
|
|
|
|
|
2021-04-20 10:07:11 -06:00
|
|
|
pub async fn get_history(
|
2021-04-13 12:14:07 -06:00
|
|
|
&self,
|
|
|
|
sync_ts: chrono::DateTime<Utc>,
|
|
|
|
history_ts: chrono::DateTime<Utc>,
|
|
|
|
host: Option<String>,
|
|
|
|
) -> Result<Vec<History>> {
|
|
|
|
let host = match host {
|
|
|
|
None => hash_str(&format!("{}:{}", whoami::hostname(), whoami::username())),
|
|
|
|
Some(h) => h,
|
|
|
|
};
|
|
|
|
|
|
|
|
let url = format!(
|
|
|
|
"{}/sync/history?sync_ts={}&history_ts={}&host={}",
|
2021-04-20 10:07:11 -06:00
|
|
|
self.sync_addr,
|
|
|
|
urlencoding::encode(sync_ts.to_rfc3339().as_str()),
|
|
|
|
urlencoding::encode(history_ts.to_rfc3339().as_str()),
|
2021-04-13 12:14:07 -06:00
|
|
|
host,
|
|
|
|
);
|
|
|
|
|
2021-04-20 10:07:11 -06:00
|
|
|
let resp = self
|
|
|
|
.client
|
2021-04-13 12:14:07 -06:00
|
|
|
.get(url)
|
2021-04-20 10:07:11 -06:00
|
|
|
.header(AUTHORIZATION, format!("Token {}", self.token))
|
2021-04-21 11:13:51 -06:00
|
|
|
.header(USER_AGENT, format!("atuin/{}", VERSION))
|
2021-04-20 10:07:11 -06:00
|
|
|
.send()
|
|
|
|
.await?;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-04-20 10:07:11 -06:00
|
|
|
let history = resp.json::<SyncHistoryResponse>().await?;
|
2021-04-13 12:14:07 -06:00
|
|
|
let history = history
|
|
|
|
.history
|
|
|
|
.iter()
|
|
|
|
.map(|h| serde_json::from_str(h).expect("invalid base64"))
|
2021-04-20 10:07:11 -06:00
|
|
|
.map(|h| decrypt(&h, &self.key).expect("failed to decrypt history! check your key"))
|
2021-04-13 12:14:07 -06:00
|
|
|
.collect();
|
|
|
|
|
|
|
|
Ok(history)
|
|
|
|
}
|
|
|
|
|
2021-04-20 10:07:11 -06:00
|
|
|
pub async fn post_history(&self, history: &[AddHistoryRequest]) -> Result<()> {
|
|
|
|
let url = format!("{}/history", self.sync_addr);
|
|
|
|
let url = Url::parse(url.as_str())?;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-04-20 10:07:11 -06:00
|
|
|
self.client
|
2021-04-13 12:14:07 -06:00
|
|
|
.post(url)
|
|
|
|
.json(history)
|
2021-04-20 10:07:11 -06:00
|
|
|
.header(AUTHORIZATION, format!("Token {}", self.token))
|
2021-04-21 11:13:51 -06:00
|
|
|
.header(USER_AGENT, format!("atuin/{}", VERSION))
|
2021-04-20 10:07:11 -06:00
|
|
|
.send()
|
|
|
|
.await?;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2021-04-21 11:13:51 -06:00
|
|
|
|
|
|
|
pub async fn login(&self, username: &str, password: &str) -> Result<LoginResponse> {
|
|
|
|
let mut map = HashMap::new();
|
|
|
|
map.insert("username", username);
|
|
|
|
map.insert("password", password);
|
|
|
|
|
|
|
|
let url = format!("{}/login", self.sync_addr);
|
|
|
|
let resp = self
|
|
|
|
.client
|
|
|
|
.post(url)
|
|
|
|
.json(&map)
|
|
|
|
.header(USER_AGENT, format!("atuin/{}", VERSION))
|
|
|
|
.send()
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
if resp.status() != reqwest::StatusCode::OK {
|
|
|
|
return Err(eyre!("invalid login details"));
|
|
|
|
}
|
|
|
|
|
|
|
|
let session = resp.json::<LoginResponse>().await?;
|
|
|
|
|
|
|
|
Ok(session)
|
|
|
|
}
|
2021-04-13 12:14:07 -06:00
|
|
|
}
|