tempomat/tempo/
oauth.rs

1use crate::error::TempomatError;
2use reqwest::Client;
3use serde::{Deserialize, Serialize};
4use tracing::{debug, instrument};
5
6const CLIENT_ID: &str = "3dcfeda8e3aa43748cce54a61e6a3d3a";
7const CLIENT_SECRET: &str = "0A339C40026062C9EC06DBB01948B053C46B6888A1D0450E5859F453900077D9"; // Breaking the purpose of OAuth 😎
8
9const OAUTH_SERVER_PORT: u16 = 8734;
10pub const OAUTH_REDIRECT_URI: &str = "http://127.0.0.1:8734/cb";
11
12pub fn generate_access_link(instance: &str, redirect: &str) -> String {
13    format!("https://{instance}.atlassian.net/plugins/servlet/ac/io.tempo.jira/oauth-authorize/?client_id={CLIENT_ID}&redirect_uri={redirect}")
14}
15
16/// OAuth tokens for tempo
17#[derive(Deserialize, Debug, Clone, Serialize)]
18pub struct TempoAccessTokens {
19    pub access_token: String,
20    pub expires_in: usize,
21    pub token_type: String,
22    pub scope: String,
23    pub refresh_token: String,
24}
25
26#[derive(Serialize, Debug)]
27pub struct GetAccessTokens {
28    grant_type: &'static str,
29    client_id: &'static str,
30    client_secret: &'static str,
31    redirect_uri: &'static str,
32    code: Option<String>,
33    refresh_token: Option<String>,
34}
35
36impl GetAccessTokens {
37    pub fn get_auth_token(code: String) -> Self {
38        Self {
39            grant_type: "authorization_code",
40            client_id: CLIENT_ID,
41            client_secret: CLIENT_SECRET,
42            redirect_uri: OAUTH_REDIRECT_URI,
43            code: Some(code),
44            refresh_token: None,
45        }
46    }
47
48    pub fn refresh_token(refresh_token: String) -> Self {
49        Self {
50            grant_type: "refresh_token",
51            client_id: CLIENT_ID,
52            client_secret: CLIENT_SECRET,
53            redirect_uri: OAUTH_REDIRECT_URI,
54            refresh_token: Some(refresh_token),
55            code: None,
56        }
57    }
58
59    #[instrument(level = "trace")]
60    pub async fn get_tokens(&self) -> Result<TempoAccessTokens, TempomatError> {
61        let client = Client::new();
62        debug!("Sending request to get OAuth tokens...");
63        let response = client
64            .post("https://api.tempo.io/oauth/token")
65            .form(self)
66            .send()
67            .await?;
68        debug!("Success!");
69        response.json().await.map_err(Into::into)
70    }
71}
72
73impl TempoAccessTokens {
74    /// Revokes the current refresh token
75    #[instrument(level = "trace")]
76    pub async fn revoke(&self) -> Result<(), TempomatError> {
77        #[derive(Serialize)]
78        struct RequestTokenRemove {
79            token_type_hint: &'static str,
80            client_id: &'static str,
81            client_secret: &'static str,
82            token: String,
83        }
84
85        let client = Client::new();
86        let response = client
87            .post("https://api.tempo.io/oauth/revoke_token/")
88            .form(&RequestTokenRemove {
89                token_type_hint: "refresh_token",
90                client_id: CLIENT_ID,
91                client_secret: CLIENT_SECRET,
92                token: self.refresh_token.clone(),
93            })
94            .send()
95            .await?;
96
97        if !response.status().is_success() {
98            Err(TempomatError::OAuthRevokeFailed(response))?
99        }
100
101        Ok(())
102    }
103}
104
105pub mod actions {
106    use super::{
107        generate_access_link, server, GetAccessTokens, TempoAccessTokens, OAUTH_REDIRECT_URI,
108        OAUTH_SERVER_PORT,
109    };
110    use crate::{config::Config, error::TempomatError};
111    use tracing::instrument;
112
113    /// Create a new oauth token
114    #[instrument(level = "trace")]
115    pub async fn login(config: &Config) -> Result<TempoAccessTokens, TempomatError> {
116        // Start a server in the background
117        let server = server::get_code(([127, 0, 0, 1], OAUTH_SERVER_PORT).into());
118        let link = generate_access_link(&config.atlassian_instance, OAUTH_REDIRECT_URI);
119        // Start the oauth process by opening the initial link in the browser
120        let _ = open::that(&link);
121        println!("Click \"Accept\" and then \"Onwards\" in your browser tab, if nothing happened click this link: {link}");
122
123        let code = server.await?;
124
125        GetAccessTokens::get_auth_token(code).get_tokens().await
126    }
127
128    /// Refresh an existing oauth token
129    #[instrument(level = "trace")]
130    pub async fn refresh_token(
131        tokens: &TempoAccessTokens,
132    ) -> Result<TempoAccessTokens, TempomatError> {
133        GetAccessTokens::refresh_token(tokens.refresh_token.to_string())
134            .get_tokens()
135            .await
136    }
137}
138
139pub mod server {
140    use crate::error::TempomatError;
141    use axum::{
142        extract::{Query, State},
143        routing::get,
144        Router, Server,
145    };
146    use serde::Deserialize;
147    use std::{net::SocketAddr, sync::Arc};
148    use tokio::{
149        sync::{oneshot, Mutex, Notify},
150        task,
151    };
152    use tracing::{debug, error, instrument};
153
154    #[instrument(level = "trace", skip_all)]
155    pub async fn get_code(host: SocketAddr) -> Result<String, TempomatError> {
156        type ServerState = (Arc<Notify>, Arc<Mutex<Option<oneshot::Sender<String>>>>);
157        let handle = task::spawn(async move {
158            let (tx, rx) = oneshot::channel();
159            let notify_done = Arc::new(Notify::new());
160
161            debug!("Starting OAuth web serverr...");
162            let server = Server::bind(&host).serve(
163                Router::new()
164                    .route("/cb", get(handler))
165                    .with_state((notify_done.clone(), Arc::new(Mutex::new(Some(tx)))))
166                    .into_make_service(),
167            );
168
169            #[derive(Deserialize)]
170            struct CBQuery {
171                code: String,
172            }
173
174            #[instrument(level = "trace")]
175            async fn handler(
176                State((notify, send)): State<ServerState>,
177                Query(CBQuery { code }): Query<CBQuery>,
178            ) -> &'static str {
179                if let Some(send) = send.lock_owned().await.take() {
180                    debug!("Got oauth code, sendnig server shutdown signals");
181                    let _ = send.send(code);
182                    notify.notify_one();
183
184                    "Success! You can now close this tab"
185                } else {
186                    error!("Failed to get Notifier");
187                    "Something went terribly wrong, leave your house immediatly"
188                }
189            }
190
191            debug!("Waiting for the web server to get a response...");
192
193            let _graceful = server
194                .with_graceful_shutdown(async {
195                    notify_done.notified().await;
196                    debug!("Web server got shutdown signal, shutting down...");
197                })
198                .await;
199
200            debug!("Web server successfully shutted down");
201
202            rx.await.unwrap()
203        });
204
205        Ok(handle.await?)
206    }
207}