1use davisjr::prelude::*;
33use reqwest::{header::HeaderMap, ClientBuilder};
34use serde_derive::{Deserialize, Serialize};
35use std::sync::Arc;
36use tokio::sync::Mutex;
37
38pub const CALENDAR_SCOPE: &str = "https://www.googleapis.com/auth/calendar";
40pub const TOKEN_URL: &str = "https://oauth2.googleapis.com/token";
42pub const USER_URL: &str = "https://accounts.google.com/o/oauth2/v2/auth";
44
45pub type State = Arc<Mutex<ClientParameters>>;
47
48#[derive(Debug, Clone, Default, Serialize, Deserialize)]
50pub struct AccessToken {
51 pub token_type: Option<String>,
52 pub access_token: String,
53 pub expires_in: i64,
54 pub refresh_token: Option<String>,
55 pub refresh_token_expires_in: Option<i64>,
56 pub scope: Option<String>,
57}
58
59#[derive(Clone, Debug, Default)]
61pub struct ClientParameters {
62 pub client_id: String,
63 pub client_secret: String,
64 pub redirect_url: Option<String>,
65 pub access_key: Option<String>,
66 pub expires_at: Option<chrono::NaiveDateTime>,
67 pub refresh_token: Option<String>,
68 pub refresh_token_expires_at: Option<chrono::NaiveDateTime>,
69}
70
71async fn handler(
72 req: Request<Body>,
73 _resp: Option<Response<Body>>,
74 _params: Params,
75 app: App<State, NoState>,
76 state: NoState,
77) -> HTTPResult<NoState> {
78 let pairs = req
79 .uri()
80 .query()
81 .map(|s| {
82 s.split("&")
83 .map(|n| n.split("=").collect::<Vec<&str>>())
84 .collect::<Vec<Vec<&str>>>()
85 })
86 .unwrap_or(Vec::new());
87
88 let mut code: Option<String> = None;
89 let mut oauth_state: Option<String> = None;
90
91 for pair in pairs {
92 if pair[0] == "code" {
93 code = Some(pair[1].to_string());
94 } else if pair[0] == "state" {
95 oauth_state = Some(pair[1].to_string());
96 }
97
98 if code.is_some() && oauth_state.is_some() {
99 break;
100 }
101 }
102
103 let lock = app.state().await.unwrap();
104 let lock = lock.lock().await;
105 let mut lock = lock.lock().await;
106
107 let token =
108 request_access_token(lock.clone(), code.as_deref(), oauth_state.as_deref(), false).await?;
109 lock.access_key = Some(token.access_token);
110 lock.expires_at = Some(
111 chrono::Local::now().naive_utc()
112 + chrono::TimeDelta::try_seconds(token.expires_in).unwrap_or_default(),
113 );
114
115 if let Some(refresh_token) = token.refresh_token {
116 lock.refresh_token = Some(refresh_token);
117 if let Some(expires_in) = token.refresh_token_expires_in {
118 lock.refresh_token_expires_at = Some(
119 chrono::Local::now().naive_utc()
120 + chrono::TimeDelta::try_seconds(expires_in).unwrap_or_default(),
121 );
122 } else {
123 lock.refresh_token_expires_at = Some(
124 chrono::Local::now().naive_utc()
125 + chrono::TimeDelta::try_seconds(3600).unwrap_or_default(),
126 );
127 }
128 }
129
130 Ok((
131 req,
132 Some(Response::new(Body::from(
133 "Please close this browser tab. Thanks!".to_string(),
134 ))),
135 state,
136 ))
137}
138
139pub async fn request_access_token(
141 client_params: ClientParameters,
142 code: Option<&str>,
143 state: Option<&str>,
144 refresh: bool,
145) -> Result<AccessToken, Error> {
146 let grant = if refresh {
147 "refresh_token"
148 } else {
149 "authorization_code"
150 };
151
152 let mut params = vec![
153 ("grant_type", grant),
154 ("client_id", &client_params.client_id),
155 ("client_secret", &client_params.client_secret),
156 ];
157
158 let mut headers = HeaderMap::default();
159 headers.insert(
160 reqwest::header::ACCEPT,
161 reqwest::header::HeaderValue::from_static("application/json"),
162 );
163
164 let redirect_url = client_params.redirect_url.unwrap_or_default();
165 let token = if refresh {
166 client_params.refresh_token.unwrap()
167 } else {
168 Default::default()
169 };
170
171 if !refresh {
172 params.push(("code", code.unwrap()));
173 params.push(("redirect_uri", &redirect_url));
174 params.push(("state", state.unwrap()));
175 } else {
176 params.push(("refresh_token", &token));
177 }
178
179 let client = ClientBuilder::new()
180 .default_headers(headers)
181 .https_only(true)
182 .build()?;
183
184 Ok(client
185 .post(TOKEN_URL)
186 .form(¶ms)
187 .basic_auth(&client_params.client_id, Some(&client_params.client_secret))
188 .send()
189 .await?
190 .json()
191 .await?)
192}
193
194pub fn oauth_user_url(params: ClientParameters) -> String {
196 let u = uuid::Uuid::new_v4();
199 format!(
200 "{}?client_id={}&access_type=offline&response_type=code&redirect_uri={}&state={}&scope={}",
201 USER_URL,
202 params.client_id,
203 params.redirect_url.expect("Expected a redirect URL"),
204 u,
205 CALENDAR_SCOPE,
206 )
207}
208
209pub async fn oauth_listener(state: State) -> Result<String, ServerError> {
213 let mut app = App::with_state(state.clone());
214
215 app.get("/", compose_handler!(handler))?;
216
217 let lis = tokio::net::TcpListener::bind("localhost:0").await?;
220 let addr = lis.local_addr()?.clone();
221 drop(lis);
222
223 let mut lock = state.lock().await;
224 lock.redirect_url = Some(format!("http://{}", addr.to_string()));
225
226 tokio::spawn(async move { app.serve(&addr.to_string()).await.unwrap() });
227
228 Ok(addr.to_string())
229}