diff options
Diffstat (limited to 'src/keys.rs')
-rw-r--r-- | src/keys.rs | 129 |
1 files changed, 129 insertions, 0 deletions
diff --git a/src/keys.rs b/src/keys.rs new file mode 100644 index 0000000..33ac02e --- /dev/null +++ b/src/keys.rs @@ -0,0 +1,129 @@ +use eyre::eyre; +use std::{collections::BTreeMap, io::ErrorKind}; +use tokio::io::AsyncWriteExt; +use url::Url; + +#[derive(serde::Serialize, serde::Deserialize, Clone, Default)] +pub struct KeyInfo { + pub hosts: BTreeMap<String, LoginInfo>, +} + +impl KeyInfo { + pub async fn load() -> eyre::Result<Self> { + let path = directories::ProjectDirs::from("", "Cyborus", "forgejo-cli") + .ok_or_else(|| eyre!("Could not find data directory"))? + .data_dir() + .join("keys.json"); + let json = tokio::fs::read(path).await; + let this = match json { + Ok(x) => serde_json::from_slice::<Self>(&x)?, + Err(e) if e.kind() == ErrorKind::NotFound => { + eprintln!("keys file not found, creating"); + Self::default() + } + Err(e) => return Err(e.into()), + }; + Ok(this) + } + + pub async fn save(&self) -> eyre::Result<()> { + let json = serde_json::to_vec_pretty(self)?; + let dirs = directories::ProjectDirs::from("", "Cyborus", "forgejo-cli") + .ok_or_else(|| eyre!("Could not find data directory"))?; + let path = dirs.data_dir(); + + tokio::fs::create_dir_all(path).await?; + + tokio::fs::File::create(path.join("keys.json")) + .await? + .write_all(&json) + .await?; + + Ok(()) + } + + pub fn get_login(&mut self, url: &Url) -> eyre::Result<&mut LoginInfo> { + let host_str = url + .host_str() + .ok_or_else(|| eyre!("remote url does not have host"))?; + let domain = if let Some(port) = url.port() { + format!("{}:{}", host_str, port) + } else { + host_str.to_owned() + }; + + let login_info = self + .hosts + .get_mut(&domain) + .ok_or_else(|| eyre!("not signed in to {domain}"))?; + Ok(login_info) + } + + pub async fn get_api(&mut self, url: &Url) -> eyre::Result<forgejo_api::Forgejo> { + self.get_login(url)?.api_for(url).await.map_err(Into::into) + } +} + +#[derive(serde::Serialize, serde::Deserialize, Clone)] +#[serde(tag = "type")] +pub enum LoginInfo { + Application { + name: String, + token: String, + }, + OAuth { + name: String, + token: String, + refresh_token: String, + expires_at: time::OffsetDateTime, + }, +} + +impl LoginInfo { + pub fn username(&self) -> &str { + match self { + LoginInfo::Application { name, .. } => name, + LoginInfo::OAuth { name, .. } => name, + } + } + + pub async fn api_for(&mut self, url: &Url) -> eyre::Result<forgejo_api::Forgejo> { + match self { + LoginInfo::Application { token, .. } => { + let api = forgejo_api::Forgejo::new(forgejo_api::Auth::Token(token), url.clone())?; + Ok(api) + } + LoginInfo::OAuth { + token, + refresh_token, + expires_at, + .. + } => { + if time::OffsetDateTime::now_utc() >= *expires_at { + let api = forgejo_api::Forgejo::new(forgejo_api::Auth::None, url.clone())?; + let (client_id, client_secret) = crate::auth::get_client_info_for(url) + .ok_or_else(|| { + eyre::eyre!("Can't refresh token; no client info for {url}. How did this happen?") + })?; + let response = api + .oauth_get_access_token(forgejo_api::structs::OAuthTokenRequest::Refresh { + refresh_token, + client_id, + client_secret, + }) + .await?; + *token = response.access_token; + *refresh_token = response.refresh_token; + // A minute less, in case any weirdness happens at the exact moment it + // expires. Better to refresh slightly too soon than slightly too late. + let expires_in = std::time::Duration::from_secs( + response.expires_in.saturating_sub(60) as u64, + ); + *expires_at = time::OffsetDateTime::now_utc() + expires_in; + } + let api = forgejo_api::Forgejo::new(forgejo_api::Auth::Token(token), url.clone())?; + Ok(api) + } + } + } +} |