summaryrefslogtreecommitdiffstats
path: root/src/keys.rs
blob: 33ac02e05ed866bbd3223f6f6b96d654fc7816df (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
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)
            }
        }
    }
}