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_json = "1"
url = "1"
try_from = "0.2.2"
[dependencies.chrono]
version = "0.4"

@ -11,7 +11,7 @@ use std::{
use self::elefren::{
apps::{
AppBuilder,
App,
Scopes
},
Mastodon,
@ -36,23 +36,21 @@ pub fn get_mastodon_data() -> Result<Mastodon, Box<Error>> {
}
pub fn register() -> Result<Mastodon, Box<Error>> {
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)?;

@ -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<String>,
}
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<Error>> {
/// 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<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
/// 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
/// (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.
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<I: Into<Cow<'a, str>>>(&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<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.

@ -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",
}
}
}

@ -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<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,];
fn route(&self, url: &str) -> String {

@ -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<String>,
client_secret: Option<String>,
redirect: Option<String>,
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<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);
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<String> {
self.is_registered()?;
pub fn authorize_url(&self) -> Result<String> {
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<Mastodon> {
self.is_registered()?;
pub fn complete(self, code: String) -> Result<Mastodon> {
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,
}

Loading…
Cancel
Save