feat(sync): implement Google OAuth 2.0
This commit is contained in:
parent
dca85d0f75
commit
b1eea3d382
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
137
orgize-sync/src/google/auth.rs
Normal file
137
orgize-sync/src/google/auth.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
0
orgize-sync/src/google/calendar.rs
Normal file
0
orgize-sync/src/google/calendar.rs
Normal file
1
orgize-sync/src/google/mod.rs
Normal file
1
orgize-sync/src/google/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod auth;
|
|
@ -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 {}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue