feat(sync): implement Google OAuth 2.0

This commit is contained in:
PoiScript 2019-09-15 15:57:10 +08:00
parent dca85d0f75
commit b1eea3d382
7 changed files with 157 additions and 3 deletions

View file

@ -23,5 +23,6 @@ orgize = { path = "../orgize", default-features = false, features = ["chrono"] }
serde = { version = "1.0.100", features = ["derive"] } serde = { version = "1.0.100", features = ["derive"] }
serde_json = "1.0.40" serde_json = "1.0.40"
structopt = "0.3.1" structopt = "0.3.1"
tokio = "=0.2.0-alpha.4"
toml = "0.5.3" toml = "0.5.3"
url = "2.1.0" url = "2.1.0"

View file

@ -5,7 +5,7 @@ use std::path::PathBuf;
use app_dirs::{app_root, AppDataType, AppInfo}; use app_dirs::{app_root, AppDataType, AppInfo};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::conf::google_calendar::*; pub use crate::conf::google_calendar::*;
use crate::error::Result; use crate::error::Result;
const APP_INFO: AppInfo = AppInfo { const APP_INFO: AppInfo = AppInfo {

View file

@ -2,6 +2,7 @@ use app_dirs::AppDirsError;
use dotenv::Error as EnvError; use dotenv::Error as EnvError;
use isahc::http::Error as HttpError; use isahc::http::Error as HttpError;
use isahc::Error as IsahcError; use isahc::Error as IsahcError;
use serde_json::Error as JsonError;
use std::convert::From; use std::convert::From;
use std::io::Error as IOError; use std::io::Error as IOError;
use toml::de::Error as TomlDeError; use toml::de::Error as TomlDeError;
@ -16,6 +17,7 @@ pub enum Error {
IO(IOError), IO(IOError),
TomlDe(TomlDeError), TomlDe(TomlDeError),
TomlSer(TomlSerError), TomlSer(TomlSerError),
Json(JsonError),
Url(ParseError), Url(ParseError),
} }
@ -61,6 +63,12 @@ impl From<TomlSerError> for Error {
} }
} }
impl From<JsonError> for Error {
fn from(err: JsonError) -> Self {
Error::Json(err)
}
}
impl From<ParseError> for Error { impl From<ParseError> for Error {
fn from(err: ParseError) -> Self { fn from(err: ParseError) -> Self {
Error::Url(err) Error::Url(err)

View file

@ -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<Utc>,
refresh_token: String,
}
impl Auth {
pub async fn new(conf: &GoogleCalendarGlobalConf) -> Result<Self> {
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<Self> {
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::<ConfirmCodeResponse>()?;
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::<RefreshTokenResponse>()?;
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()
}
}

View file

View file

@ -0,0 +1 @@
pub mod auth;

View file

@ -1,5 +1,6 @@
mod conf; mod conf;
mod error; mod error;
mod google;
use std::fs; use std::fs;
use std::path::PathBuf; 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, default_config_path, default_env_path, user_cache_path, user_config_path, Conf, EnvConf,
}; };
use crate::error::Result; use crate::error::Result;
use crate::google::auth::Auth;
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
#[structopt(name = "orgize-sync")] #[structopt(name = "orgize-sync")]
@ -38,7 +40,8 @@ enum Cmd {
}, },
} }
fn main() -> Result<()> { #[tokio::main]
async fn main() -> Result<()> {
let opt = Opt::from_args(); let opt = Opt::from_args();
match opt.subcommand { match opt.subcommand {
@ -81,7 +84,11 @@ fn main() -> Result<()> {
} => { } => {
let conf = Conf::new(conf_path)?; 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 {} if cfg!(feature = "toggl") && !skip_toggl {}
} }