From b1eea3d382c1094215d14420d27ffdf1a42620fa Mon Sep 17 00:00:00 2001 From: PoiScript Date: Sun, 15 Sep 2019 15:57:10 +0800 Subject: [PATCH] feat(sync): implement Google OAuth 2.0 --- orgize-sync/Cargo.toml | 1 + orgize-sync/src/conf.rs | 2 +- orgize-sync/src/error.rs | 8 ++ orgize-sync/src/google/auth.rs | 137 +++++++++++++++++++++++++++++ orgize-sync/src/google/calendar.rs | 0 orgize-sync/src/google/mod.rs | 1 + orgize-sync/src/main.rs | 11 ++- 7 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 orgize-sync/src/google/auth.rs create mode 100644 orgize-sync/src/google/calendar.rs create mode 100644 orgize-sync/src/google/mod.rs diff --git a/orgize-sync/Cargo.toml b/orgize-sync/Cargo.toml index 85f27cf..8c64839 100644 --- a/orgize-sync/Cargo.toml +++ b/orgize-sync/Cargo.toml @@ -23,5 +23,6 @@ orgize = { path = "../orgize", default-features = false, features = ["chrono"] } serde = { version = "1.0.100", features = ["derive"] } serde_json = "1.0.40" structopt = "0.3.1" +tokio = "=0.2.0-alpha.4" toml = "0.5.3" url = "2.1.0" diff --git a/orgize-sync/src/conf.rs b/orgize-sync/src/conf.rs index 15ba584..1938908 100644 --- a/orgize-sync/src/conf.rs +++ b/orgize-sync/src/conf.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use app_dirs::{app_root, AppDataType, AppInfo}; use serde::{Deserialize, Serialize}; -use crate::conf::google_calendar::*; +pub use crate::conf::google_calendar::*; use crate::error::Result; const APP_INFO: AppInfo = AppInfo { diff --git a/orgize-sync/src/error.rs b/orgize-sync/src/error.rs index 211d41b..e114142 100644 --- a/orgize-sync/src/error.rs +++ b/orgize-sync/src/error.rs @@ -2,6 +2,7 @@ use app_dirs::AppDirsError; use dotenv::Error as EnvError; use isahc::http::Error as HttpError; use isahc::Error as IsahcError; +use serde_json::Error as JsonError; use std::convert::From; use std::io::Error as IOError; use toml::de::Error as TomlDeError; @@ -16,6 +17,7 @@ pub enum Error { IO(IOError), TomlDe(TomlDeError), TomlSer(TomlSerError), + Json(JsonError), Url(ParseError), } @@ -61,6 +63,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: JsonError) -> Self { + Error::Json(err) + } +} + impl From for Error { fn from(err: ParseError) -> Self { Error::Url(err) diff --git a/orgize-sync/src/google/auth.rs b/orgize-sync/src/google/auth.rs new file mode 100644 index 0000000..97cb01e --- /dev/null +++ b/orgize-sync/src/google/auth.rs @@ -0,0 +1,137 @@ +use chrono::{DateTime, Duration, Utc}; +use colored::Colorize; +use isahc::prelude::{Request, RequestExt, ResponseExt}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::io::{stdin, BufRead}; +use std::path::PathBuf; +use url::Url; + +use crate::conf::GoogleCalendarGlobalConf; +use crate::error::Result; + +#[derive(Serialize, Deserialize)] +pub struct Auth { + access_token: String, + expires_at: DateTime, + refresh_token: String, +} + +impl Auth { + pub async fn new(conf: &GoogleCalendarGlobalConf) -> Result { + let mut path = conf.token_dir.clone(); + path.push(&conf.token_filename); + if let Ok(json) = fs::read_to_string(path) { + Ok(serde_json::from_str(&json)?) + } else { + Auth::sign_in(conf).await + } + } + + pub fn save(&self, conf: &GoogleCalendarGlobalConf) -> Result<()> { + let mut path = conf.token_dir.clone(); + path.push(&conf.token_filename); + fs::write(path, serde_json::to_string(&self)?)?; + Ok(()) + } + + async fn sign_in(config: &GoogleCalendarGlobalConf) -> Result { + let url = Url::parse_with_params( + "https://accounts.google.com/o/oauth2/v2/auth", + &[ + ("client_id", &*config.client_id), + ("response_type", "code"), + ("access_type", "offline"), + ("redirect_uri", &*config.redirect_uri), + ("scope", "https://www.googleapis.com/auth/calendar"), + ], + )?; + + println!("Visit: {}", url.as_str().underline()); + println!("Follow the instructions and paste the code here:"); + + for line in stdin().lock().lines() { + let line = line?; + let code = line.trim(); + + if code.is_empty() { + continue; + } else if code == "q" { + panic!() + } + + let mut response = Request::post("https://www.googleapis.com/oauth2/v4/token") + .header("content-type", "application/x-www-form-urlencoded") + .body(format!( + "code={}&client_id={}&client_secret={}&grant_type={}&redirect_uri={}", + &code, + &config.client_id, + &config.client_secret, + "authorization_code", + "http://localhost" + ))? + .send_async() + .await?; + + if response.status().is_success() { + #[derive(Deserialize)] + struct ConfirmCodeResponse { + access_token: String, + expires_in: i64, + refresh_token: String, + } + + let json = response.json::()?; + + println!("Logging in successfully."); + + let auth = Auth { + access_token: json.access_token, + expires_at: Utc::now() + Duration::seconds(json.expires_in), + refresh_token: json.refresh_token, + }; + + auth.save(config)?; + + return Ok(auth); + } else { + panic!("Failed to authorize."); + } + } + + panic!("Failed to authorize."); + } + + pub async fn refresh(&mut self, config: &GoogleCalendarGlobalConf) -> Result<()> { + let mut response = Request::post("https://www.googleapis.com/oauth2/v4/token") + .header("content-type", "application/x-www-form-urlencoded") + .body(format!( + "client_id={}&client_secret={}&refresh_token={}&grant_type={}", + &config.client_id, &config.client_secret, self.refresh_token, "refresh_token", + ))? + .send_async() + .await?; + + if response.status().is_success() { + #[derive(Deserialize)] + struct RefreshTokenResponse { + access_token: String, + expires_in: i64, + } + + let json = response.json::()?; + + self.access_token = json.access_token; + self.expires_at = Utc::now() + Duration::seconds(json.expires_in); + self.save(config)?; + } else { + panic!(""); + } + + Ok(()) + } + + pub fn is_valid(&self) -> bool { + self.expires_at > Utc::now() + } +} diff --git a/orgize-sync/src/google/calendar.rs b/orgize-sync/src/google/calendar.rs new file mode 100644 index 0000000..e69de29 diff --git a/orgize-sync/src/google/mod.rs b/orgize-sync/src/google/mod.rs new file mode 100644 index 0000000..0e4a05d --- /dev/null +++ b/orgize-sync/src/google/mod.rs @@ -0,0 +1 @@ +pub mod auth; diff --git a/orgize-sync/src/main.rs b/orgize-sync/src/main.rs index 56b8721..d3d23a7 100644 --- a/orgize-sync/src/main.rs +++ b/orgize-sync/src/main.rs @@ -1,5 +1,6 @@ mod conf; mod error; +mod google; use std::fs; use std::path::PathBuf; @@ -10,6 +11,7 @@ use crate::conf::{ default_config_path, default_env_path, user_cache_path, user_config_path, Conf, EnvConf, }; use crate::error::Result; +use crate::google::auth::Auth; #[derive(StructOpt, Debug)] #[structopt(name = "orgize-sync")] @@ -38,7 +40,8 @@ enum Cmd { }, } -fn main() -> Result<()> { +#[tokio::main] +async fn main() -> Result<()> { let opt = Opt::from_args(); match opt.subcommand { @@ -81,7 +84,11 @@ fn main() -> Result<()> { } => { let conf = Conf::new(conf_path)?; - if cfg!(feature = "google_calendar") && !skip_google_calendar {} + if cfg!(feature = "google_calendar") && !skip_google_calendar { + if let Some(google_calendar) = conf.google_calendar { + let auth = Auth::new(&google_calendar).await; + } + } if cfg!(feature = "toggl") && !skip_toggl {} }