From 0f5e295ea97962fe96fa6f59e9a3541697984021 Mon Sep 17 00:00:00 2001 From: Paul Woolcock Date: Thu, 23 Aug 2018 06:39:26 -0400 Subject: [PATCH] Revamp registration & auth process --- Cargo.toml | 1 + examples/register.rs | 20 ++++---- src/apps.rs | 117 ++++++++++++++++++++++++++++++++++++++----- src/errors.rs | 3 ++ src/lib.rs | 45 +++-------------- src/registration.rs | 117 +++++++++++++++++++++---------------------- 6 files changed, 181 insertions(+), 122 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 27c391b..6c59282 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ serde = "1" serde_derive = "1" serde_json = "1" url = "1" +try_from = "0.2.2" [dependencies.chrono] version = "0.4" diff --git a/examples/register.rs b/examples/register.rs index 7f6b058..f042291 100644 --- a/examples/register.rs +++ b/examples/register.rs @@ -11,7 +11,7 @@ use std::{ use self::elefren::{ apps::{ - AppBuilder, + App, Scopes }, Mastodon, @@ -36,23 +36,21 @@ pub fn get_mastodon_data() -> Result> { } pub fn register() -> Result> { - let app = AppBuilder { - client_name: "elefren-examples", - redirect_uris: "urn:ietf:wg:oauth:2.0:oob", - scopes: Scopes::All, - website: Some("https://github.com/pwoolcoc/elefren"), - }; + let mut app = App::builder(); + app.client_name("elefren-examples") + .scopes(Scopes::All) + .website("https://github.com/pwoolcoc/elefren"); let website = read_line("Please enter your mastodon instance url:")?; - let mut registration = Registration::new(website.trim()); - registration.register(app)?; - let url = registration.authorise()?; + let registration = Registration::new(website.trim()); + let registered = registration.register(app)?; + let url = registered.authorize_url()?; println!("Click this link to authorize on Mastodon: {}", url); let input = read_line("Paste the returned authorization code: ")?; let code = input.trim(); - let mastodon = registration.create_access_token(code.to_string())?; + let mastodon = registered.complete(code.to_string())?; // Save app data for using on the next run. let toml = toml::to_string(&*mastodon)?; diff --git a/src/apps.rs b/src/apps.rs index 8c89199..eb380b2 100644 --- a/src/apps.rs +++ b/src/apps.rs @@ -1,39 +1,130 @@ use std::fmt; +use std::borrow::Cow; +use try_from::TryInto; + +use errors::{Error, Result}; + +/// Provides the necessary types for registering an App and getting the +/// necessary auth information pub mod prelude { pub use { apps::{ - AppBuilder, + App, Scopes }, registration::Registration }; } +/// Represents an application that can be registered with a mastodon instance +#[derive(Debug, Default, Serialize)] +pub struct App { + client_name: String, + redirect_uris: String, + scopes: Scopes, + #[serde(skip_serializing_if="Option::is_none")] + website: Option, +} + +impl App { + pub fn builder<'a>() -> AppBuilder<'a> { + AppBuilder::new() + } + + pub fn scopes(&self) -> Scopes { + self.scopes + } +} + /// Builder struct for defining your application. /// ``` +/// use std::error::Error; /// use elefren::apps::prelude::*; /// -/// let app = AppBuilder { -/// client_name: "elefren_test", -/// redirect_uris: "urn:ietf:wg:oauth:2.0:oob", -/// scopes: Scopes::Read, -/// website: None, -/// }; +/// # fn main() -> Result<(), Box> { +/// let mut builder = App::builder(); +/// builder.client_name("elefren_test"); +/// let app = builder.build()?; +/// # Ok(()) +/// # } /// ``` #[derive(Debug, Default, Serialize)] pub struct AppBuilder<'a> { + client_name: Option>, + redirect_uris: Option>, + scopes: Option, + website: Option>, +} + +impl<'a> AppBuilder<'a> { + pub fn new() -> Self { + Default::default() + } + /// Name of the application. Will be displayed when the user is deciding to /// grant permission. - pub client_name: &'a str, + /// + /// In order to turn this builder into an App, this needs to be provided + pub fn client_name>>(&mut self, name: I) -> &mut Self { + self.client_name = Some(name.into()); + self + } + /// Where the user should be redirected after authorization - /// (for no redirect, use `urn:ietf:wg:oauth:2.0:oob`) - pub redirect_uris: &'a str, + /// + /// If none is specified, the default is `urn:ietf:wg:oauth:2.0:oob` + pub fn redirect_uris>>(&mut self, uris: I) -> &mut Self { + self.redirect_uris = Some(uris.into()); + self + } + /// Permission scope of the application. - pub scopes: Scopes, + /// + /// IF none is specified, the default is Scopes::Read + pub fn scopes(&mut self, scopes: Scopes) -> &mut Self { + self.scopes = Some(scopes); + self + } + /// URL to the homepage of your application. - #[serde(skip_serializing_if="Option::is_none")] - pub website: Option<&'a str>, + pub fn website>>(&mut self, website: I) -> &mut Self { + self.website = Some(website.into()); + self + } + + /// Attempts to convert this build into an `App` + /// + /// Will fail if no `client_name` was provided + pub fn build(self) -> Result { + Ok(App { + client_name: self.client_name + .ok_or_else(|| Error::MissingField("client_name"))? + .into(), + redirect_uris: self.redirect_uris + .unwrap_or_else(|| "urn:ietf:wg:oauth:2.0:oob".into()) + .into(), + scopes: self.scopes + .unwrap_or_else(|| Scopes::Read), + website: self.website.map(|s| s.into()), + }) + } +} + +impl TryInto for App { + type Err = Error; + + fn try_into(self) -> Result { + Ok(self) + } +} + +impl<'a> TryInto for AppBuilder<'a> { + type Err = Error; + + fn try_into(self) -> Result { + Ok(self.build()?) + } } /// Permission scope of the application. diff --git a/src/errors.rs b/src/errors.rs index 104fee8..bae7fa3 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -39,6 +39,8 @@ pub enum Error { Server(StatusCode), /// MastodonBuilder error. DataMissing, + /// AppBuilder error + MissingField(&'static str), } impl fmt::Display for Error { @@ -66,6 +68,7 @@ impl error::Error for Error { Error::ClientSecretRequired => "ClientSecretRequired", Error::AccessTokenRequired => "AccessTokenRequired", Error::DataMissing => "DataMissing", + Error::MissingField(_) => "MissingField", } } } diff --git a/src/lib.rs b/src/lib.rs index 5beca56..a60e89d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,20 +12,16 @@ //! use elefren::prelude::*; //! use elefren::apps::prelude::*; //! -//! let app = AppBuilder { -//! client_name: "elefren_test", -//! redirect_uris: "urn:ietf:wg:oauth:2.0:oob", -//! scopes: Scopes::Read, -//! website: None, -//! }; +//! let mut app = App::builder(); +//! app.client_name("elefren_test"); //! -//! let mut registration = Registration::new("https://mastodon.social"); -//! registration.register(app)?; -//! let url = registration.authorise()?; +//! let registration = Registration::new("https://mastodon.social") +//! .register(app)?; +//! let url = registration.authorize_url()?; //! // Here you now need to open the url in the browser //! // And handle a the redirect url coming back with the code. //! let code = String::from("RETURNED_FROM_BROWSER"); -//! let mastodon = registration.create_access_token(code)?; +//! let mastodon = registration.complete(code)?; //! //! println!("{:?}", mastodon.get_home_timeline()?.initial_items); //! # Ok(()) @@ -41,6 +37,7 @@ extern crate chrono; extern crate reqwest; extern crate serde; +extern crate try_from; extern crate url; use std::borrow::Cow; @@ -156,34 +153,6 @@ pub trait MastodonClient { } impl Mastodon { - fn from_registration(base: I, - client_id: I, - client_secret: I, - redirect: I, - token: I, - client: Client) - -> Self - where I: Into> - { - let data = Data { - base: base.into(), - client_id: client_id.into(), - client_secret: client_secret.into(), - redirect: redirect.into(), - token: token.into(), - - }; - - let mut headers = Headers::new(); - headers.set(Authorization(Bearer { token: (*data.token).to_owned() })); - - Mastodon { - client: client, - headers: headers, - data: data, - } - } - methods![get, post, delete,]; fn route(&self, url: &str) -> String { diff --git a/src/registration.rs b/src/registration.rs index a885de3..df2ef20 100644 --- a/src/registration.rs +++ b/src/registration.rs @@ -1,17 +1,14 @@ use reqwest::Client; +use try_from::TryInto; -use super::{Error, Mastodon, Result}; -use apps::{AppBuilder, Scopes}; +use {Data, Error, Mastodon, MastodonBuilder, Result}; +use apps::{App, Scopes}; /// Handles registering your mastodon app to your instance. It is recommended /// you cache your data struct to avoid registering on every run. pub struct Registration { base: String, client: Client, - client_id: Option, - client_secret: Option, - redirect: Option, - scopes: Scopes, } #[derive(Deserialize)] @@ -42,10 +39,6 @@ impl Registration { Registration { base: base.into(), client: Client::new(), - client_id: None, - client_secret: None, - redirect: None, - scopes: Scopes::Read, } } @@ -53,92 +46,96 @@ impl Registration { /// /// ```no_run /// # extern crate elefren; - /// # fn main() { - /// # try().unwrap(); - /// # } - /// # fn try() -> elefren::Result<()> { + /// # fn main () -> elefren::Result<()> { /// use elefren::prelude::*; /// use elefren::apps::prelude::*; /// - /// let app = AppBuilder { - /// client_name: "elefren_test", - /// redirect_uris: "urn:ietf:wg:oauth:2.0:oob", - /// scopes: Scopes::Read, - /// website: None, - /// }; + /// let mut builder = App::builder(); + /// builder.client_name("elefren_test"); + /// let app = builder.build()?; /// - /// let mut registration = Registration::new("https://mastodon.social"); - /// registration.register(app)?; - /// let url = registration.authorise()?; + /// let registration = Registration::new("https://mastodon.social"); + /// let registered = registration.register(app)?; + /// let url = registered.authorize_url()?; /// // Here you now need to open the url in the browser /// // And handle a the redirect url coming back with the code. /// let code = String::from("RETURNED_FROM_BROWSER"); - /// let mastodon = registration.create_access_token(code)?; + /// let mastodon = registered.complete(code)?; /// /// println!("{:?}", mastodon.get_home_timeline()?.initial_items); /// # Ok(()) /// # } /// ``` - pub fn register(&mut self, app_builder: AppBuilder) -> Result<()> { + pub fn register>(self, app: I) -> Result + where Error: From<>::Err> + { + let app = app.try_into()?; let url = format!("{}/api/v1/apps", self.base); - self.scopes = app_builder.scopes; - let app: OAuth = self.client.post(&url).form(&app_builder).send()?.json()?; - - self.client_id = Some(app.client_id); - self.client_secret = Some(app.client_secret); - self.redirect = Some(app.redirect_uri); - - Ok(()) + let oauth: OAuth = self.client + .post(&url) + .form(&app) + .send()? + .json()?; + + Ok(Registered { + base: self.base, + client: self.client, + client_id: oauth.client_id, + client_secret: oauth.client_secret, + redirect: oauth.redirect_uri, + scopes: app.scopes(), + }) } +} +impl Registered { /// Returns the full url needed for authorisation. This needs to be opened /// in a browser. - pub fn authorise(&mut self) -> Result { - self.is_registered()?; - + pub fn authorize_url(&self) -> Result { let url = format!( "{}/oauth/authorize?client_id={}&redirect_uri={}&scope={}&response_type=code", self.base, - self.client_id.clone().unwrap(), - self.redirect.clone().unwrap(), + self.client_id, + self.redirect, self.scopes, ); Ok(url) } - fn is_registered(&self) -> Result<()> { - if self.client_id.is_none() { - Err(Error::ClientIdRequired) - } else if self.client_secret.is_none() { - Err(Error::ClientSecretRequired) - } else { - Ok(()) - } - } - /// Create an access token from the client id, client secret, and code /// provided by the authorisation url. - pub fn create_access_token(self, code: String) -> Result { - self.is_registered()?; + pub fn complete(self, code: String) -> Result { let url = format!( "{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&redirect_uri={}", self.base, - self.client_id.clone().unwrap(), - self.client_secret.clone().unwrap(), + self.client_id, + self.client_secret, code, - self.redirect.clone().unwrap() + self.redirect ); let token: AccessToken = self.client.post(&url).send()?.json()?; - Ok(Mastodon::from_registration(self.base, - self.client_id.unwrap(), - self.client_secret.unwrap(), - self.redirect.unwrap(), - token.access_token, - self.client)) + let data = Data { + base: self.base.into(), + client_id: self.client_id.into(), + client_secret: self.client_secret.into(), + redirect: self.redirect.into(), + token: token.access_token.into(), + }; + + let mut builder = MastodonBuilder::new(); + builder.client(self.client).data(data); + Ok(builder.build()?) } } - +pub struct Registered { + base: String, + client: Client, + client_id: String, + client_secret: String, + redirect: String, + scopes: Scopes, +}