use anyhow::Error;
use compio::buf::BufResult;
use cyper::Client as HttpClient;
use dirs::config_dir;
use github_app_auth::{GithubAuthParams, InstallationAccessToken};
use http::header::HeaderMap;
use http::header::{HeaderName, HeaderValue};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
const CONFIG_FILE: &str = "faasta/github_auth.json";
const USER_AGENT: &str = "faasta-cli";
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct AuthConfig {
pub app_id: u64,
pub installation_id: u64,
pub private_key: Vec<u8>,
pub user_id: Option<String>,
pub project_hmacs: HashMap<String, String>, }
pub struct GitHubAuth {
config: AuthConfig,
token: Option<InstallationAccessToken>,
config_path: PathBuf,
}
impl GitHubAuth {
pub async fn new() -> Result<Self, Error> {
let config_path = Self::get_config_path()?;
let config = Self::load_config(&config_path).await?;
Ok(Self {
config,
token: None,
config_path,
})
}
pub async fn authenticate(&mut self) -> Result<(), Error> {
if self.config.app_id == 0
|| self.config.installation_id == 0
|| self.config.private_key.is_empty()
{
return Err(anyhow::anyhow!(
"GitHub App not configured. Run 'cargo faasta auth setup' first."
));
}
self.token = Some(
InstallationAccessToken::new(GithubAuthParams {
user_agent: USER_AGENT.into(),
private_key: self.config.private_key.clone(),
app_id: self.config.app_id,
installation_id: self.config.installation_id,
})
.await?,
);
if self.config.user_id.is_none() {
self.fetch_and_store_user_id().await?;
}
Ok(())
}
pub async fn header(&mut self) -> Result<HeaderMap, Error> {
if self.token.is_none() {
self.authenticate().await?;
}
let oauth_headers = self.token.as_mut().unwrap().header().await?;
let mut headers = HeaderMap::new();
for (name, value) in oauth_headers.iter() {
let hn = HeaderName::from_bytes(name.as_str().as_bytes())?;
let hv = HeaderValue::from_bytes(value.as_bytes())?;
headers.insert(hn, hv);
}
Ok(headers)
}
pub async fn store_project_hmac(
&mut self,
project_name: &str,
hmac: &str,
) -> Result<(), Error> {
self.config
.project_hmacs
.insert(project_name.to_string(), hmac.to_string());
self.save_config().await?;
Ok(())
}
pub fn get_project_hmac(&self, project_name: &str) -> Option<&String> {
self.config.project_hmacs.get(project_name)
}
pub fn owns_project(&self, project_name: &str) -> bool {
self.config.project_hmacs.contains_key(project_name)
}
pub fn get_owned_projects(&self) -> Vec<String> {
self.config.project_hmacs.keys().cloned().collect()
}
pub fn has_reached_project_limit(&self) -> bool {
self.config.project_hmacs.len() >= 5
}
pub async fn setup(
&mut self,
app_id: u64,
installation_id: u64,
private_key: Vec<u8>,
) -> Result<(), Error> {
self.config.app_id = app_id;
self.config.installation_id = installation_id;
self.config.private_key = private_key;
self.save_config().await?;
Ok(())
}
async fn fetch_and_store_user_id(&mut self) -> Result<(), Error> {
let oauth_headers = self.token.as_mut().unwrap().header().await?;
let mut header = HeaderMap::new();
for (name, value) in oauth_headers.iter() {
let hn = HeaderName::from_bytes(name.as_str().as_bytes())?;
let hv = HeaderValue::from_bytes(value.as_bytes())?;
header.insert(hn, hv);
}
let response = HttpClient::new()
.get("https://api.github.com/app")?
.headers(header)
.send()
.await?;
if response.status().is_success() {
let app_info: serde_json::Value = response.json().await?;
if let Some(id) = app_info.get("id").and_then(|v| v.as_str()) {
self.config.user_id = Some(id.to_string());
self.save_config().await?;
}
}
Ok(())
}
fn get_config_path() -> Result<PathBuf, Error> {
let mut path =
config_dir().ok_or_else(|| anyhow::anyhow!("Could not find user config directory"))?;
path.push(CONFIG_FILE);
if let Some(parent) = path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
Ok(path)
}
async fn load_config(path: &Path) -> Result<AuthConfig, Error> {
if path.exists() {
let data = compio::fs::read(path).await?;
let content = String::from_utf8(data)?;
Ok(serde_json::from_str(&content)?)
} else {
let default_config = AuthConfig::default();
let content = serde_json::to_string_pretty(&default_config)?;
let BufResult(result, _) = compio::fs::write(path, content.into_bytes()).await;
result?;
Ok(default_config)
}
}
pub async fn save_config(&self) -> Result<(), Error> {
let content = serde_json::to_string_pretty(&self.config)?;
let BufResult(result, _) = compio::fs::write(&self.config_path, content.into_bytes()).await;
result?;
Ok(())
}
pub fn is_configured(&self) -> bool {
self.config.app_id != 0
&& self.config.installation_id != 0
&& !self.config.private_key.is_empty()
}
pub fn get_user_id(&self) -> Option<&str> {
self.config.user_id.as_deref()
}
}