Revamp registration & auth process

master
Paul Woolcock 6 years ago
parent 3f7841da02
commit 0f5e295ea9
  1. 1
      Cargo.toml
  2. 20
      examples/register.rs
  3. 117
      src/apps.rs
  4. 3
      src/errors.rs
  5. 45
      src/lib.rs
  6. 117
      src/registration.rs

@ -17,6 +17,7 @@ serde = "1"
serde_derive = "1" serde_derive = "1"
serde_json = "1" serde_json = "1"
url = "1" url = "1"
try_from = "0.2.2"
[dependencies.chrono] [dependencies.chrono]
version = "0.4" version = "0.4"

@ -11,7 +11,7 @@ use std::{
use self::elefren::{ use self::elefren::{
apps::{ apps::{
AppBuilder, App,
Scopes Scopes
}, },
Mastodon, Mastodon,
@ -36,23 +36,21 @@ pub fn get_mastodon_data() -> Result<Mastodon, Box<Error>> {
} }
pub fn register() -> Result<Mastodon, Box<Error>> { pub fn register() -> Result<Mastodon, Box<Error>> {
let app = AppBuilder { let mut app = App::builder();
client_name: "elefren-examples", app.client_name("elefren-examples")
redirect_uris: "urn:ietf:wg:oauth:2.0:oob", .scopes(Scopes::All)
scopes: Scopes::All, .website("https://github.com/pwoolcoc/elefren");
website: Some("https://github.com/pwoolcoc/elefren"),
};
let website = read_line("Please enter your mastodon instance url:")?; let website = read_line("Please enter your mastodon instance url:")?;
let mut registration = Registration::new(website.trim()); let registration = Registration::new(website.trim());
registration.register(app)?; let registered = registration.register(app)?;
let url = registration.authorise()?; let url = registered.authorize_url()?;
println!("Click this link to authorize on Mastodon: {}", url); println!("Click this link to authorize on Mastodon: {}", url);
let input = read_line("Paste the returned authorization code: ")?; let input = read_line("Paste the returned authorization code: ")?;
let code = input.trim(); 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. // Save app data for using on the next run.
let toml = toml::to_string(&*mastodon)?; let toml = toml::to_string(&*mastodon)?;

@ -1,39 +1,130 @@
use std::fmt; 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 mod prelude {
pub use { pub use {
apps::{ apps::{
AppBuilder, App,
Scopes Scopes
}, },
registration::Registration 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<String>,
}
impl App {
pub fn builder<'a>() -> AppBuilder<'a> {
AppBuilder::new()
}
pub fn scopes(&self) -> Scopes {
self.scopes
}
}
/// Builder struct for defining your application. /// Builder struct for defining your application.
/// ``` /// ```
/// use std::error::Error;
/// use elefren::apps::prelude::*; /// use elefren::apps::prelude::*;
/// ///
/// let app = AppBuilder { /// # fn main() -> Result<(), Box<Error>> {
/// client_name: "elefren_test", /// let mut builder = App::builder();
/// redirect_uris: "urn:ietf:wg:oauth:2.0:oob", /// builder.client_name("elefren_test");
/// scopes: Scopes::Read, /// let app = builder.build()?;
/// website: None, /// # Ok(())
/// }; /// # }
/// ``` /// ```
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
pub struct AppBuilder<'a> { pub struct AppBuilder<'a> {
client_name: Option<Cow<'a, str>>,
redirect_uris: Option<Cow<'a, str>>,
scopes: Option<Scopes>,
website: Option<Cow<'a, str>>,
}
impl<'a> AppBuilder<'a> {
pub fn new() -> Self {
Default::default()
}
/// Name of the application. Will be displayed when the user is deciding to /// Name of the application. Will be displayed when the user is deciding to
/// grant permission. /// 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<I: Into<Cow<'a, str>>>(&mut self, name: I) -> &mut Self {
self.client_name = Some(name.into());
self
}
/// Where the user should be redirected after authorization /// 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<I: Into<Cow<'a, str>>>(&mut self, uris: I) -> &mut Self {
self.redirect_uris = Some(uris.into());
self
}
/// Permission scope of the application. /// 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. /// URL to the homepage of your application.
#[serde(skip_serializing_if="Option::is_none")] pub fn website<I: Into<Cow<'a, str>>>(&mut self, website: I) -> &mut Self {
pub website: Option<&'a str>, 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<App> {
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<App> for App {
type Err = Error;
fn try_into(self) -> Result<App> {
Ok(self)
}
}
impl<'a> TryInto<App> for AppBuilder<'a> {
type Err = Error;
fn try_into(self) -> Result<App> {
Ok(self.build()?)
}
} }
/// Permission scope of the application. /// Permission scope of the application.

@ -39,6 +39,8 @@ pub enum Error {
Server(StatusCode), Server(StatusCode),
/// MastodonBuilder error. /// MastodonBuilder error.
DataMissing, DataMissing,
/// AppBuilder error
MissingField(&'static str),
} }
impl fmt::Display for Error { impl fmt::Display for Error {
@ -66,6 +68,7 @@ impl error::Error for Error {
Error::ClientSecretRequired => "ClientSecretRequired", Error::ClientSecretRequired => "ClientSecretRequired",
Error::AccessTokenRequired => "AccessTokenRequired", Error::AccessTokenRequired => "AccessTokenRequired",
Error::DataMissing => "DataMissing", Error::DataMissing => "DataMissing",
Error::MissingField(_) => "MissingField",
} }
} }
} }

@ -12,20 +12,16 @@
//! use elefren::prelude::*; //! use elefren::prelude::*;
//! use elefren::apps::prelude::*; //! use elefren::apps::prelude::*;
//! //!
//! let app = AppBuilder { //! let mut app = App::builder();
//! client_name: "elefren_test", //! app.client_name("elefren_test");
//! redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
//! scopes: Scopes::Read,
//! website: None,
//! };
//! //!
//! let mut registration = Registration::new("https://mastodon.social"); //! let registration = Registration::new("https://mastodon.social")
//! registration.register(app)?; //! .register(app)?;
//! let url = registration.authorise()?; //! let url = registration.authorize_url()?;
//! // Here you now need to open the url in the browser //! // Here you now need to open the url in the browser
//! // And handle a the redirect url coming back with the code. //! // And handle a the redirect url coming back with the code.
//! let code = String::from("RETURNED_FROM_BROWSER"); //! 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); //! println!("{:?}", mastodon.get_home_timeline()?.initial_items);
//! # Ok(()) //! # Ok(())
@ -41,6 +37,7 @@
extern crate chrono; extern crate chrono;
extern crate reqwest; extern crate reqwest;
extern crate serde; extern crate serde;
extern crate try_from;
extern crate url; extern crate url;
use std::borrow::Cow; use std::borrow::Cow;
@ -156,34 +153,6 @@ pub trait MastodonClient {
} }
impl Mastodon { impl Mastodon {
fn from_registration<I>(base: I,
client_id: I,
client_secret: I,
redirect: I,
token: I,
client: Client)
-> Self
where I: Into<Cow<'static, str>>
{
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,]; methods![get, post, delete,];
fn route(&self, url: &str) -> String { fn route(&self, url: &str) -> String {

@ -1,17 +1,14 @@
use reqwest::Client; use reqwest::Client;
use try_from::TryInto;
use super::{Error, Mastodon, Result}; use {Data, Error, Mastodon, MastodonBuilder, Result};
use apps::{AppBuilder, Scopes}; use apps::{App, Scopes};
/// Handles registering your mastodon app to your instance. It is recommended /// Handles registering your mastodon app to your instance. It is recommended
/// you cache your data struct to avoid registering on every run. /// you cache your data struct to avoid registering on every run.
pub struct Registration { pub struct Registration {
base: String, base: String,
client: Client, client: Client,
client_id: Option<String>,
client_secret: Option<String>,
redirect: Option<String>,
scopes: Scopes,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -42,10 +39,6 @@ impl Registration {
Registration { Registration {
base: base.into(), base: base.into(),
client: Client::new(), client: Client::new(),
client_id: None,
client_secret: None,
redirect: None,
scopes: Scopes::Read,
} }
} }
@ -53,92 +46,96 @@ impl Registration {
/// ///
/// ```no_run /// ```no_run
/// # extern crate elefren; /// # extern crate elefren;
/// # fn main() { /// # fn main () -> elefren::Result<()> {
/// # try().unwrap();
/// # }
/// # fn try() -> elefren::Result<()> {
/// use elefren::prelude::*; /// use elefren::prelude::*;
/// use elefren::apps::prelude::*; /// use elefren::apps::prelude::*;
/// ///
/// let app = AppBuilder { /// let mut builder = App::builder();
/// client_name: "elefren_test", /// builder.client_name("elefren_test");
/// redirect_uris: "urn:ietf:wg:oauth:2.0:oob", /// let app = builder.build()?;
/// scopes: Scopes::Read,
/// website: None,
/// };
/// ///
/// let mut registration = Registration::new("https://mastodon.social"); /// let registration = Registration::new("https://mastodon.social");
/// registration.register(app)?; /// let registered = registration.register(app)?;
/// let url = registration.authorise()?; /// let url = registered.authorize_url()?;
/// // Here you now need to open the url in the browser /// // Here you now need to open the url in the browser
/// // And handle a the redirect url coming back with the code. /// // And handle a the redirect url coming back with the code.
/// let code = String::from("RETURNED_FROM_BROWSER"); /// 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); /// println!("{:?}", mastodon.get_home_timeline()?.initial_items);
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn register(&mut self, app_builder: AppBuilder) -> Result<()> { pub fn register<I: TryInto<App>>(self, app: I) -> Result<Registered>
where Error: From<<I as TryInto<App>>::Err>
{
let app = app.try_into()?;
let url = format!("{}/api/v1/apps", self.base); let url = format!("{}/api/v1/apps", self.base);
self.scopes = app_builder.scopes; let oauth: OAuth = self.client
let app: OAuth = self.client.post(&url).form(&app_builder).send()?.json()?; .post(&url)
.form(&app)
self.client_id = Some(app.client_id); .send()?
self.client_secret = Some(app.client_secret); .json()?;
self.redirect = Some(app.redirect_uri);
Ok(Registered {
Ok(()) 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 /// Returns the full url needed for authorisation. This needs to be opened
/// in a browser. /// in a browser.
pub fn authorise(&mut self) -> Result<String> { pub fn authorize_url(&self) -> Result<String> {
self.is_registered()?;
let url = format!( let url = format!(
"{}/oauth/authorize?client_id={}&redirect_uri={}&scope={}&response_type=code", "{}/oauth/authorize?client_id={}&redirect_uri={}&scope={}&response_type=code",
self.base, self.base,
self.client_id.clone().unwrap(), self.client_id,
self.redirect.clone().unwrap(), self.redirect,
self.scopes, self.scopes,
); );
Ok(url) 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 /// Create an access token from the client id, client secret, and code
/// provided by the authorisation url. /// provided by the authorisation url.
pub fn create_access_token(self, code: String) -> Result<Mastodon> { pub fn complete(self, code: String) -> Result<Mastodon> {
self.is_registered()?;
let url = format!( let url = format!(
"{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&redirect_uri={}", "{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&redirect_uri={}",
self.base, self.base,
self.client_id.clone().unwrap(), self.client_id,
self.client_secret.clone().unwrap(), self.client_secret,
code, code,
self.redirect.clone().unwrap() self.redirect
); );
let token: AccessToken = self.client.post(&url).send()?.json()?; let token: AccessToken = self.client.post(&url).send()?.json()?;
Ok(Mastodon::from_registration(self.base, let data = Data {
self.client_id.unwrap(), base: self.base.into(),
self.client_secret.unwrap(), client_id: self.client_id.into(),
self.redirect.unwrap(), client_secret: self.client_secret.into(),
token.access_token, redirect: self.redirect.into(),
self.client)) 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,
}

Loading…
Cancel
Save