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_json = "1.0.40"
|
||||
structopt = "0.3.1"
|
||||
tokio = "=0.2.0-alpha.4"
|
||||
toml = "0.5.3"
|
||||
url = "2.1.0"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<TomlSerError> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<JsonError> for Error {
|
||||
fn from(err: JsonError) -> Self {
|
||||
Error::Json(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseError> for Error {
|
||||
fn from(err: ParseError) -> Self {
|
||||
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 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 {}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue