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"; const 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#[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 #[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 #[instrument(level = "trace")]
115 pub async fn login(config: &Config) -> Result<TempoAccessTokens, TempomatError> {
116 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 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 #[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}