use std::path::PathBuf;
use serde::Deserialize;
use serde::Serialize;
use tracing::debug;
use tracing::warn;
use crate::io::storage::LocalStorage;
use crate::io::storage::Storage;
use crate::paths::AUTH_CREDENTIALS;
use crate::paths::AUTH_TOKENS;
use crate::Res;
#[derive(Deserialize, Serialize, Debug)]
pub struct Tokens {
pub access_token: String,
pub refresh_token: String,
pub expires_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct Credentials {
pub access_key: String,
pub secret_key: String,
pub token: String,
pub expires_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct AuthIo<S: Storage = LocalStorage> {
storage: S,
dir: PathBuf,
}
impl<S: Storage> AuthIo<S> {
fn tokens_path(&self) -> PathBuf {
self.dir.join(AUTH_TOKENS)
}
fn credentials_path(&self) -> PathBuf {
self.dir.join(AUTH_CREDENTIALS)
}
pub async fn read_tokens(&self) -> Res<Option<Tokens>> {
let tokens_path = self.tokens_path();
debug!("⏳ Reading auth tokens from {:?}", tokens_path);
if !self.storage.exists(&tokens_path).await {
debug!("No tokens file found");
return Ok(None);
}
let contents = self.storage.read_file(&tokens_path).await?;
let tokens = serde_json::from_slice(&contents)?;
debug!("✔️ Successfully read tokens");
Ok(Some(tokens))
}
pub async fn write_tokens(&self, tokens: &Tokens) -> Res {
let tokens_path = self.tokens_path();
debug!("⏳ Writing auth tokens to {:?}", tokens_path);
let contents = serde_json::to_vec(tokens)?;
self.storage.write_file(&tokens_path, &contents).await?;
debug!("✔️ Successfully wrote tokens: {:?}", tokens);
Ok(())
}
pub async fn read_credentials(&self) -> Res<Option<Credentials>> {
let credentials_path = self.credentials_path();
debug!("⏳ Reading credentials from {:?}", credentials_path);
if !self.storage.exists(&credentials_path).await {
warn!("No credentials file found");
return Ok(None);
}
let contents = self.storage.read_file(&credentials_path).await?;
let credentials: Credentials = serde_json::from_slice(&contents)?;
if credentials.expires_at <= chrono::Utc::now() {
warn!("❌ Credentials have expired");
return Ok(None);
}
debug!("✔️ Successfully read valid credentials");
Ok(Some(credentials))
}
pub async fn write_credentials(&self, credentials: &Credentials) -> Res {
let credentials_path = self.credentials_path();
debug!("⏳ Writing credentials to {:?}", credentials_path);
let contents = serde_json::to_vec(credentials)?;
self.storage
.write_file(&credentials_path, &contents)
.await?;
debug!("✔️ Successfully wrote credentials: {:?}", credentials);
Ok(())
}
}
impl<S: Storage> AuthIo<S> {
pub fn new(storage: S, dir: PathBuf) -> Self {
AuthIo { storage, dir }
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_log::test;
use chrono::Utc;
use crate::io::storage::mocks::MockStorage;
#[test(tokio::test)]
async fn test_write_read_tokens() -> Res {
let storage = MockStorage::default();
let dir = storage.temp_dir.path().to_path_buf();
let auth = AuthIo::new(storage, dir);
let tokens = auth.read_tokens().await?;
assert!(tokens.is_none());
let test_tokens = Tokens {
access_token: "test_access".to_string(),
refresh_token: "test_refresh".to_string(),
expires_at: Utc::now(),
};
auth.write_tokens(&test_tokens).await?;
let read_tokens = auth.read_tokens().await?.unwrap();
assert_eq!(read_tokens.access_token, test_tokens.access_token);
assert_eq!(read_tokens.refresh_token, test_tokens.refresh_token);
assert_eq!(read_tokens.expires_at, test_tokens.expires_at);
Ok(())
}
#[test(tokio::test)]
async fn test_credentials() -> Res {
let storage = MockStorage::default();
let dir = storage.temp_dir.path().to_path_buf();
let auth = AuthIo::new(storage, dir);
let creds = auth.read_credentials().await?;
assert!(creds.is_none());
let expired_creds = Credentials {
access_key: "expired_key".to_string(),
secret_key: "expired_secret".to_string(),
token: "expired_token".to_string(),
expires_at: Utc::now() - chrono::Duration::minutes(1),
};
auth.write_credentials(&expired_creds).await?;
assert!(auth.read_credentials().await?.is_none());
let valid_creds = Credentials {
access_key: "test_key".to_string(),
secret_key: "test_secret".to_string(),
token: "test_token".to_string(),
expires_at: Utc::now() + chrono::Duration::minutes(1),
};
auth.write_credentials(&valid_creds).await?;
let read_creds = auth.read_credentials().await?.unwrap();
assert_eq!(read_creds.access_key, valid_creds.access_key);
assert_eq!(read_creds.secret_key, valid_creds.secret_key);
assert_eq!(read_creds.token, valid_creds.token);
assert_eq!(read_creds.expires_at, valid_creds.expires_at);
Ok(())
}
}