refactor: merge Authentication + Api
This commit is contained in:
parent
ce3552cdc7
commit
e405ef83f8
|
@ -1,6 +1,6 @@
|
|||
use clap::Parser;
|
||||
use std::env;
|
||||
use teslatte::auth::{AccessToken, Authentication};
|
||||
use teslatte::auth::AccessToken;
|
||||
use teslatte::vehicles::SetChargeLimit;
|
||||
use teslatte::Api;
|
||||
|
||||
|
@ -8,19 +8,11 @@ use teslatte::Api;
|
|||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let access_token = match env::var("TESLA_ACCESS_TOKEN") {
|
||||
Ok(t) => AccessToken(t),
|
||||
Err(_) => {
|
||||
let auth = Authentication::new().unwrap();
|
||||
let (access_token, refresh_token) = auth.interactive_get_access_token().await.unwrap();
|
||||
println!("Access token: {}", access_token.0);
|
||||
println!("Refresh token: {}", refresh_token.0);
|
||||
access_token
|
||||
}
|
||||
let api = match env::var("TESLA_ACCESS_TOKEN") {
|
||||
Ok(t) => Api::new(AccessToken(t), None),
|
||||
Err(_) => Api::from_interactive_url().await.unwrap(),
|
||||
};
|
||||
|
||||
let api = Api::new(&access_token);
|
||||
|
||||
let vehicles = api.vehicles().await.unwrap();
|
||||
dbg!(&vehicles);
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use clap::{Args, Parser, Subcommand};
|
|||
use cli_vehicle::VehicleArgs;
|
||||
use miette::{miette, IntoDiagnostic, WrapErr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use teslatte::auth::{AccessToken, Authentication, RefreshToken};
|
||||
use teslatte::auth::{AccessToken, RefreshToken};
|
||||
use teslatte::calendar_history::{CalendarHistoryValues, HistoryKind, HistoryPeriod};
|
||||
use teslatte::energy::EnergySiteId;
|
||||
use teslatte::powerwall::{PowerwallEnergyHistoryValues, PowerwallId};
|
||||
|
@ -179,9 +179,8 @@ async fn main() -> miette::Result<()> {
|
|||
|
||||
match args.command {
|
||||
Command::Auth { save } => {
|
||||
let auth = Authentication::new()?;
|
||||
let (access_token, refresh_token) = auth.interactive_get_access_token().await?;
|
||||
updated_tokens(save, access_token, refresh_token);
|
||||
let api = Api::from_interactive_url().await?;
|
||||
updated_tokens(save, &api);
|
||||
}
|
||||
Command::Refresh { refresh_token } => {
|
||||
let (save, refresh_token) = match refresh_token {
|
||||
|
@ -192,20 +191,22 @@ async fn main() -> miette::Result<()> {
|
|||
}
|
||||
};
|
||||
|
||||
let auth = Authentication::new()?;
|
||||
let response = auth.refresh_access_token(&refresh_token).await?;
|
||||
updated_tokens(save, response.access_token, refresh_token);
|
||||
let api = Api::from_refresh_token(&refresh_token).await?;
|
||||
updated_tokens(save, &api);
|
||||
}
|
||||
Command::Api(api_args) => {
|
||||
let access_token = match &api_args.access_token {
|
||||
Some(a) => a.clone(),
|
||||
let (access_token, refresh_token) = match &api_args.access_token {
|
||||
Some(a) => (a.clone(), None),
|
||||
None => {
|
||||
let config = Config::load();
|
||||
config.access_token.clone()
|
||||
(
|
||||
config.access_token.clone(),
|
||||
Some(config.refresh_token.clone()),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let api = Api::new(&access_token);
|
||||
let api = Api::new(access_token, refresh_token);
|
||||
match api_args.command {
|
||||
ApiCommand::Vehicles => {
|
||||
dbg!(api.vehicles().await?);
|
||||
|
@ -228,9 +229,11 @@ async fn main() -> miette::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn updated_tokens(save: bool, access_token: AccessToken, refresh_token: RefreshToken) {
|
||||
println!("Access token: {}", access_token.0);
|
||||
println!("Refresh token: {}", refresh_token.0);
|
||||
fn updated_tokens(save: bool, api: &Api) {
|
||||
let access_token = api.access_token.clone();
|
||||
let refresh_token = api.refresh_token.clone().unwrap();
|
||||
println!("Access token: {}", access_token);
|
||||
println!("Refresh token: {}", refresh_token);
|
||||
if save {
|
||||
Config {
|
||||
access_token,
|
||||
|
|
79
src/auth.rs
79
src/auth.rs
|
@ -1,72 +1,45 @@
|
|||
use crate::TeslatteError;
|
||||
use crate::TeslatteError::UnhandledReqwestError;
|
||||
use crate::{Api, TeslatteError};
|
||||
use derive_more::{Display, FromStr};
|
||||
use rand::Rng;
|
||||
use reqwest::Client;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{stdin, stdout, Write};
|
||||
use std::str::FromStr;
|
||||
use url::Url;
|
||||
|
||||
const AUTHORIZE_URL: &str = "https://auth.tesla.com/oauth2/v3/authorize";
|
||||
const TOKEN_URL: &str = "https://auth.tesla.com/oauth2/v3/token";
|
||||
|
||||
pub struct Authentication {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromStr, Display)]
|
||||
pub struct AccessToken(pub String);
|
||||
|
||||
impl FromStr for AccessToken {
|
||||
type Err = TeslatteError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(AccessToken(s.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromStr, Display)]
|
||||
pub struct RefreshToken(pub String);
|
||||
|
||||
impl FromStr for RefreshToken {
|
||||
type Err = TeslatteError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(RefreshToken(s.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Authentication {
|
||||
pub fn new() -> Result<Self, TeslatteError> {
|
||||
let client = Client::builder()
|
||||
.cookie_store(false)
|
||||
.build()
|
||||
.map_err(UnhandledReqwestError)?;
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
/// Currently the only way to get an access token via this library.
|
||||
pub async fn interactive_get_access_token(
|
||||
&self,
|
||||
) -> Result<(AccessToken, RefreshToken), TeslatteError> {
|
||||
let login_form = self.get_login_url_for_user().await;
|
||||
impl Api {
|
||||
/// Currently the only way to "authenticate" to an access token for this library.
|
||||
pub async fn from_interactive_url() -> Result<Api, TeslatteError> {
|
||||
let login_form = Self::get_login_url_for_user().await;
|
||||
dbg!(&login_form);
|
||||
|
||||
let callback_url =
|
||||
ask_input("Enter the URL of the 404 error page after you've logged in: ");
|
||||
let callback_code = Self::extract_callback_code_from_url(&callback_url)?;
|
||||
let bearer = Self::exchange_auth_for_bearer(&login_form.code, &callback_code).await?;
|
||||
let access_token = AccessToken(bearer.access_token);
|
||||
let refresh_token = RefreshToken(bearer.refresh_token);
|
||||
Ok(Api::new(access_token, Some(refresh_token)))
|
||||
}
|
||||
|
||||
let bearer = self
|
||||
.exchange_auth_for_bearer(&login_form.code, &callback_code)
|
||||
.await?;
|
||||
let refresh_token = bearer.refresh_token.clone();
|
||||
|
||||
Ok((
|
||||
AccessToken(bearer.access_token),
|
||||
RefreshToken(refresh_token),
|
||||
pub async fn from_refresh_token(refresh_token: &RefreshToken) -> Result<Api, TeslatteError> {
|
||||
let response = Self::refresh_token(&refresh_token).await?;
|
||||
Ok(Api::new(
|
||||
response.access_token,
|
||||
Some(response.refresh_token),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_login_url_for_user(&self) -> LoginForm {
|
||||
pub async fn get_login_url_for_user() -> LoginForm {
|
||||
let code = Code::new();
|
||||
let state = random_string(8);
|
||||
let url = Self::login_url(&code, &state);
|
||||
|
@ -74,7 +47,6 @@ impl Authentication {
|
|||
}
|
||||
|
||||
async fn exchange_auth_for_bearer(
|
||||
&self,
|
||||
code: &Code,
|
||||
callback_code: &str,
|
||||
) -> Result<BearerTokenResponse, TeslatteError> {
|
||||
|
@ -86,11 +58,10 @@ impl Authentication {
|
|||
code_verifier: code.verifier.clone(),
|
||||
redirect_uri: "https://auth.tesla.com/void/callback".into(),
|
||||
};
|
||||
self.post(url, &payload).await
|
||||
Self::auth_post(url, &payload).await
|
||||
}
|
||||
|
||||
pub async fn refresh_access_token(
|
||||
&self,
|
||||
pub async fn refresh_token(
|
||||
refresh_token: &RefreshToken,
|
||||
) -> Result<RefreshTokenResponse, TeslatteError> {
|
||||
let url = "https://auth.tesla.com/oauth2/v3/token";
|
||||
|
@ -100,16 +71,15 @@ impl Authentication {
|
|||
refresh_token: refresh_token.0.clone(),
|
||||
scope: "openid email offline_access".into(),
|
||||
};
|
||||
self.post(url, &payload).await
|
||||
Self::auth_post(url, &payload).await
|
||||
}
|
||||
|
||||
async fn post<'a, S, D>(&self, url: &str, payload: &S) -> Result<D, TeslatteError>
|
||||
async fn auth_post<'a, S, D>(url: &str, payload: &S) -> Result<D, TeslatteError>
|
||||
where
|
||||
S: Serialize,
|
||||
D: DeserializeOwned,
|
||||
{
|
||||
let response = self
|
||||
.client
|
||||
let response = Client::new()
|
||||
.post(url)
|
||||
.header("Accept", "application/json")
|
||||
.json(payload)
|
||||
|
@ -187,7 +157,6 @@ pub struct LoginForm {
|
|||
state: String,
|
||||
}
|
||||
|
||||
// These can be probably &str.
|
||||
#[derive(Debug, Serialize)]
|
||||
struct BearerTokenRequest {
|
||||
grant_type: String,
|
||||
|
|
68
src/lib.rs
68
src/lib.rs
|
@ -1,4 +1,4 @@
|
|||
use crate::auth::AccessToken;
|
||||
use crate::auth::{AccessToken, RefreshToken};
|
||||
use crate::error::TeslatteError;
|
||||
use chrono::{DateTime, SecondsFormat, TimeZone};
|
||||
use derive_more::{Display, FromStr};
|
||||
|
@ -15,7 +15,7 @@ pub mod error;
|
|||
pub mod powerwall;
|
||||
pub mod vehicles;
|
||||
|
||||
const API_URL: &str = "https://owner-api.teslamotors.com";
|
||||
const API_URL: &str = "https://owner-api.teslamotors.com/api/1";
|
||||
|
||||
trait Values {
|
||||
fn format(&self, url: &str) -> String;
|
||||
|
@ -34,14 +34,16 @@ pub struct VehicleId(u64);
|
|||
pub struct ExternalVehicleId(u64);
|
||||
|
||||
pub struct Api {
|
||||
access_token: AccessToken,
|
||||
pub access_token: AccessToken,
|
||||
pub refresh_token: Option<RefreshToken>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl Api {
|
||||
pub fn new(access_token: &AccessToken) -> Self {
|
||||
pub fn new(access_token: AccessToken, refresh_token: Option<RefreshToken>) -> Self {
|
||||
Api {
|
||||
access_token: access_token.clone(),
|
||||
access_token,
|
||||
refresh_token,
|
||||
client: Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
|
@ -86,44 +88,47 @@ impl Api {
|
|||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn post<S>(&self, path: &str, body: S) -> Result<(), TeslatteError>
|
||||
async fn post<S>(&self, url: &str, body: S) -> Result<(), TeslatteError>
|
||||
where
|
||||
S: Serialize + Debug,
|
||||
{
|
||||
trace!("Fetching");
|
||||
let url = format!("{}{}", API_URL, path);
|
||||
let request = || {
|
||||
let req_ctx = || {
|
||||
let payload =
|
||||
serde_json::to_string(&body).expect("Should not fail creating the request struct.");
|
||||
format!("POST {} {payload}", &url)
|
||||
};
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.access_token.0))
|
||||
.header("Accept", "application/json")
|
||||
|
||||
let mut request = self.client.post(url).header("Accept", "application/json");
|
||||
let auth = true;
|
||||
if auth {
|
||||
request = request.header("Authorization", format!("Bearer {}", self.access_token.0));
|
||||
}
|
||||
let response =
|
||||
request
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|source| TeslatteError::FetchError {
|
||||
source,
|
||||
request: request(),
|
||||
request: req_ctx(),
|
||||
})?;
|
||||
|
||||
let body = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|source| TeslatteError::FetchError {
|
||||
source,
|
||||
request: request(),
|
||||
request: req_ctx(),
|
||||
})?;
|
||||
let json = Self::parse_json::<PostResponse, _>(&body, request)?;
|
||||
let json = Self::parse_json::<PostResponse, _>(&body, req_ctx)?;
|
||||
trace!(?json);
|
||||
|
||||
if json.result {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(TeslatteError::ServerError {
|
||||
request: request(),
|
||||
request: req_ctx(),
|
||||
msg: json.reason,
|
||||
description: None,
|
||||
})
|
||||
|
@ -198,8 +203,8 @@ struct Empty {}
|
|||
/// GET /api/1/[url]
|
||||
macro_rules! get {
|
||||
($name:ident, $return_type:ty, $url:expr) => {
|
||||
pub async fn $name(&self) -> Result<$return_type, TeslatteError> {
|
||||
let url = format!("/api/1{}", $url);
|
||||
pub async fn $name(&self) -> Result<$return_type, crate::error::TeslatteError> {
|
||||
let url = format!("{}{}", crate::API_URL, $url);
|
||||
self.get(&url).await
|
||||
}
|
||||
};
|
||||
|
@ -211,9 +216,12 @@ pub(crate) use get;
|
|||
/// Pass in the URL as a format string with one arg, which has to impl Display.
|
||||
macro_rules! get_arg {
|
||||
($name:ident, $return_type:ty, $url:expr, $arg_type:ty) => {
|
||||
pub async fn $name(&self, arg: &$arg_type) -> miette::Result<$return_type, TeslatteError> {
|
||||
pub async fn $name(
|
||||
&self,
|
||||
arg: &$arg_type,
|
||||
) -> miette::Result<$return_type, crate::error::TeslatteError> {
|
||||
let url = format!($url, arg);
|
||||
let url = format!("/api/1{}", url);
|
||||
let url = format!("{}{}", crate::API_URL, url);
|
||||
self.get(&url).await
|
||||
}
|
||||
};
|
||||
|
@ -223,9 +231,12 @@ pub(crate) use get_arg;
|
|||
/// GET /api/1/[url] with a struct.
|
||||
macro_rules! get_args {
|
||||
($name:ident, $return_type:ty, $url:expr, $args:ty) => {
|
||||
pub async fn $name(&self, values: &$args) -> miette::Result<$return_type, TeslatteError> {
|
||||
pub async fn $name(
|
||||
&self,
|
||||
values: &$args,
|
||||
) -> miette::Result<$return_type, crate::error::TeslatteError> {
|
||||
let url = values.format($url);
|
||||
let url = format!("/api/1{}", url);
|
||||
let url = format!("{}{}", crate::API_URL, url);
|
||||
self.get(&url).await
|
||||
}
|
||||
};
|
||||
|
@ -239,9 +250,9 @@ macro_rules! post_arg {
|
|||
&self,
|
||||
arg: &$arg_type,
|
||||
data: &$request_type,
|
||||
) -> miette::Result<(), TeslatteError> {
|
||||
) -> miette::Result<(), crate::error::TeslatteError> {
|
||||
let url = format!($url, arg);
|
||||
let url = format!("/api/1{}", url);
|
||||
let url = format!("{}{}", crate::API_URL, url);
|
||||
self.post(&url, data).await
|
||||
}
|
||||
};
|
||||
|
@ -251,9 +262,12 @@ pub(crate) use post_arg;
|
|||
/// Post like above but with an empty body using the Empty struct.
|
||||
macro_rules! post_arg_empty {
|
||||
($name:ident, $url:expr, $arg_type:ty) => {
|
||||
pub async fn $name(&self, arg: &$arg_type) -> miette::Result<(), TeslatteError> {
|
||||
pub async fn $name(
|
||||
&self,
|
||||
arg: &$arg_type,
|
||||
) -> miette::Result<(), crate::error::TeslatteError> {
|
||||
let url = format!($url, arg);
|
||||
let url = format!("/api/1{}", url);
|
||||
let url = format!("{}{}", crate::API_URL, url);
|
||||
self.post(&url, &Empty {}).await
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue