summaryrefslogtreecommitdiffstats
path: root/src/keys.rs
blob: 5239df62151cc532c38fd5229fa564bc215fdc44 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
use eyre::eyre;
use forgejo_api::{Auth, Forgejo};
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>,
    #[serde(default)]
    pub aliases: BTreeMap<String, String>,
}

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) -> Option<&mut LoginInfo> {
        let host = crate::host_with_port(url);
        let login_info = self.hosts.get_mut(host)?;
        Some(login_info)
    }

    pub async fn get_api(&mut self, url: &Url) -> eyre::Result<Forgejo> {
        match self.get_login(url) {
            Some(login) => login.api_for(url).await,
            None => Forgejo::new(Auth::None, url.clone()).map_err(Into::into),
        }
    }

    pub fn deref_alias(&self, url: url::Url) -> url::Url {
        match self.aliases.get(crate::host_with_port(&url)) {
            Some(replacement) => {
                let s = format!(
                    "{}{}{}",
                    &url[..url::Position::BeforeHost],
                    replacement,
                    &url[url::Position::AfterPort..]
                );
                url::Url::parse(&s).unwrap()
            }
            None => url,
        }
    }
}

#[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> {
        match self {
            LoginInfo::Application { token, .. } => {
                let api = Forgejo::new(Auth::Token(token), url.clone())?;
                Ok(api)
            }
            LoginInfo::OAuth {
                token,
                refresh_token,
                expires_at,
                ..
            } => {
                if time::OffsetDateTime::now_utc() >= *expires_at {
                    let api = Forgejo::new(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::new(Auth::Token(token), url.clone())?;
                Ok(api)
            }
        }
    }
}