Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CLI] Leo CLI is now on StructOpt and Anyhow (reopened) #632

Merged
merged 5 commits into from
Feb 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/leo-login-logout.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,5 @@ jobs:
leo login -u "$USER" -p "$PASS"
leo add argus4130/xnor
leo remove xnor
leo clean
leo logout

42 changes: 42 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ default-features = false
[dependencies.snarkvm-utilities]
version = "0.0.3"

[dependencies.anyhow]
version = "1.0"

[dependencies.structopt]
version = "0.3"

[dependencies.clap]
version = "2.33.3"

Expand Down
202 changes: 202 additions & 0 deletions leo/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Copyright (C) 2019-2021 Aleo Systems Inc.
// This file is part of the Leo library.

// The Leo library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// The Leo library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.

use anyhow::{anyhow, Error, Result};
use reqwest::{
blocking::{Client, Response},
Method,
StatusCode,
};
use serde::Serialize;

/// Trait describes API Routes and Request bodies, struct which implements
/// Route MUST also support Serialize to be usable in Api::run_route(r: Route)
pub trait Route {
/// Whether to use bearer auth or not. Some routes may have additional
/// features for logged-in users, so authorization token should be sent
/// if it is created of course
const AUTH: bool;

/// HTTP method to use when requesting
const METHOD: Method;

/// URL path without first forward slash (e.g. v1/package/fetch)
const PATH: &'static str;

/// Output type for this route. For login it is simple - String
/// But for other routes may be more complex.
type Output;

/// Process reqwest Response and turn it into Output
fn process(&self, res: Response) -> Result<Self::Output>;

/// Transform specific status codes into correct errors for this route.
/// For example 404 on package fetch should mean that 'Package is not found'
fn status_to_err(&self, _status: StatusCode) -> Error {
anyhow!("Unidentified API error")
}
}

/// REST API handler with reqwest::blocking inside
#[derive(Clone, Debug)]
pub struct Api {
host: String,
client: Client,
/// Authorization token for API requests
auth_token: Option<String>,
}

impl Api {
/// Create new instance of API, set host and Client is going to be
/// created and set automatically
pub fn new(host: String, auth_token: Option<String>) -> Api {
Api {
client: Client::new(),
auth_token,
host,
}
}

/// Get token for bearer auth, should be passed into Api through Context
pub fn auth_token(&self) -> Option<String> {
self.auth_token.clone()
}

/// Set authorization token for future requests
pub fn set_auth_token(&mut self, token: String) {
self.auth_token = Some(token);
}

/// Run specific route struct. Turn struct into request body
/// and use type constants and Route implementation to get request params
pub fn run_route<T>(&self, route: T) -> Result<T::Output>
where
T: Route,
T: Serialize,
{
let mut res = self.client.request(T::METHOD, &format!("{}{}", self.host, T::PATH));

// add body for POST and PUT requests
if T::METHOD == Method::POST || T::METHOD == Method::PUT {
res = res.json(&route);
};

// if Route::Auth is true and token is present - pass it
if T::AUTH && self.auth_token().is_some() {
res = res.bearer_auth(&self.auth_token().unwrap());
};

// only one error is possible here
let res = res.send().map_err(|_| anyhow!("Unable to connect to Aleo PM"))?;

// where magic begins
route.process(res)
}
}

// --------------------------------------------------
// | Defining routes |
// --------------------------------------------------

/// Handler for 'fetch' route - fetch packages from Aleo PM
/// Route: POST /v1/package/fetch
#[derive(Serialize, Debug)]
pub struct Fetch {
pub author: String,
pub package_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}

impl Route for Fetch {
type Output = Response;

const AUTH: bool = true;
const METHOD: Method = Method::POST;
const PATH: &'static str = "api/package/fetch";

fn process(&self, res: Response) -> Result<Self::Output> {
// check status code first
if res.status() != 200 {
return Err(self.status_to_err(res.status()));
};

Ok(res)
}

fn status_to_err(&self, status: StatusCode) -> Error {
match status {
StatusCode::BAD_REQUEST => anyhow!("Package is not found - check author and/or package name"),
// TODO: we should return 404 on not found author/package
// and return BAD_REQUEST if data format is incorrect or some of the arguments
// were not passed
StatusCode::NOT_FOUND => anyhow!("Package is hidden"),
_ => anyhow!("Unknown API error: {}", status),
}
}
}

/// Handler for 'login' route - send username and password and receive JWT
/// Route: POST /v1/account/authenticate
#[derive(Serialize)]
pub struct Login {
pub email_username: String,
pub password: String,
}

impl Route for Login {
type Output = Response;

const AUTH: bool = false;
const METHOD: Method = Method::POST;
const PATH: &'static str = "api/account/authenticate";

fn process(&self, res: Response) -> Result<Self::Output> {
if res.status() != 200 {
return Err(self.status_to_err(res.status()));
}

Ok(res)
}

fn status_to_err(&self, status: StatusCode) -> Error {
match status {
StatusCode::BAD_REQUEST => anyhow!("This username is not yet registered or the password is incorrect"),
// TODO: NOT_FOUND here should be replaced, this error code has no relation to what this route is doing
StatusCode::NOT_FOUND => anyhow!("Incorrect password"),
_ => anyhow!("Unknown API error: {}", status),
}
}
}

/// Handler for 'my_profile' route. Meant to be used to get profile details but
/// in current application is used to check if user is logged in. Any non-200 response
/// is treated as Unauthorized
#[derive(Serialize)]
pub struct Profile {}

impl Route for Profile {
type Output = bool;

const AUTH: bool = true;
const METHOD: Method = Method::GET;
const PATH: &'static str = "api/account/my_profile";

fn process(&self, res: Response) -> Result<Self::Output> {
// this may be extended for more precise error handling
Ok(res.status() == 200)
}
}
Loading