Skip to content

Commit

Permalink
Merge pull request #60 from danielSanchezQ/vit-station-cli
Browse files Browse the repository at this point in the history
Vit station cli
  • Loading branch information
Daniel Sanchez committed Aug 3, 2020
2 parents 2b04db9 + 86102c5 commit 44a07fc
Show file tree
Hide file tree
Showing 16 changed files with 388 additions and 20 deletions.
17 changes: 15 additions & 2 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
@@ -1,5 +1,6 @@
[workspace]
members = [
"vit-servicing-station-cli",
"vit-servicing-station-lib",
"vit-servicing-station-server",
"vit-servicing-station-tests"
Expand Down
39 changes: 38 additions & 1 deletion README.md
Expand Up @@ -87,4 +87,41 @@ Some considerations when using the playground:
* There is no need of an `API-Token` for accessing it. But, the token is needed for any request performed by the playground.
In order to use your API-Token just add it inside the json configuration for headers in the bottom left part of the webpage. For example: `{ "API-Token" : "your api token here" }`
* Since the API-Token is needed for any request. Both the **docs** and the **schema** will not be available till the header is filled.
* Depending on the deployment and/or your server configuration you may need to configure properly the **CORS** settings in order to make the playground work as expected.
* Depending on the deployment and/or your server configuration you may need to configure properly the **CORS** settings in order to make the playground work as expected.
## CLI
The `vit-servicing-station-cli` is an accompanying tool to interact with some of the ecosystem.
Right now it offers the following commands:
### api-token
#### generate
It is possible to generate api tokens (URL safe base64 encoded) with a simple command. For example:
```bash
❯ ./vit-servicing-station-cli api-token generate
F-4QxU3FrbH7qg
```
It can be combined with two (optional) arguments:
* `--n` number of tokens to generate
* `--size` length (in **bytes**) of the tokens
#### add
We can add a token to some db using the tool too:
```bash
./vit-servicing-station-cli api-token add --db-url ../../db/vit_station_new.db --tokens 1CNDAo43fo4ktQ 0wNbdTDMJCFcnw
```
We need to provide the url to the database where we want it to be inserted (with `--db-url`) and the tokens we want too
insert (with `--tokens` followed by the tokens).
Notice that the token is in the same URL safe base64 encoding as we generated in the previous command.
**If not specifying** the `--tokens` argument the cli will read the input from the standard input the tokens we want to insert.
This enables the command to be piped from another command, for example:
```bash
./vit-servicing-station-cli api-token generate --size 10 --n 10 | ./vit-servicing-station-cli api-token add --db-url ../../db/vit_station_new.db
```
19 changes: 19 additions & 0 deletions vit-servicing-station-cli/Cargo.toml
@@ -0,0 +1,19 @@
[package]
name = "vit-servicing-station-cli"
version = "0.1.0"
authors = ["danielsanchezq <daniel.sanchez@iohk.io>"]
edition = "2018"
license = "MIT OR Apache-2.0"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
base64 = "0.12.1"
chrono = "0.4.13"
rand = "0.7.3"
structopt = "0.3.14"
vit-servicing-station-lib = { path = "../vit-servicing-station-lib" }

[dev-dependencies]
diesel = { version = "1.4.4", features = ["sqlite", "r2d2"] }
diesel_migrations = "1.4.0"
195 changes: 195 additions & 0 deletions vit-servicing-station-cli/src/app.rs
@@ -0,0 +1,195 @@
use chrono::{Duration, Utc};
use rand::Rng;
use std::collections::HashSet;
use std::io;
use std::iter::FromIterator;
use structopt::StructOpt;
use vit_servicing_station_lib::{
db::{
load_db_connection_pool, models::api_tokens::APITokenData,
queries::api_tokens::insert_token_data, DBConnection,
},
v0::api_token::APIToken,
};

pub trait ExecTask {
type ResultValue;
fn exec(&self) -> std::io::Result<<Self as ExecTask>::ResultValue>;
}

#[derive(StructOpt)]
pub enum CLIApp {
/// API token related operations
APIToken(APITokenCmd),
}

#[derive(Debug, PartialEq, StructOpt)]
pub enum APITokenCmd {
/// Add provided tokens to database. If --tokens is not provided the binary will read them from the `stdin`
Add {
/// List of tokens in URL safe base64. If --tokens is not provided the binary will read them from the `stdin`
#[structopt(long = "tokens")]
tokens: Option<Vec<String>>,

/// URL of the vit-servicing-station database to interact with
#[structopt(long = "db-url")]
db_url: String,
},

/// Generate API tokens, URL safe base64 encoded.
Generate {
/// Number of tokens to generate
#[structopt(long = "n", default_value = "1")]
n: usize,

/// Size of the token
#[structopt(long = "size", default_value = "10")]
size: usize,
},
}

impl APITokenCmd {
fn generate(n: usize, size: usize) -> Vec<String> {
(0..n)
.map(|_| {
let random_bytes: Vec<u8> =
(0..size).map(|_| rand::thread_rng().gen::<u8>()).collect();
base64::encode_config(random_bytes, base64::URL_SAFE_NO_PAD)
})
.collect()
}

fn add_tokens_from_stream(db_conn: &DBConnection) -> io::Result<()> {
let mut base64_tokens: Vec<String> = Vec::new();
let mut input = String::new();
while let Ok(n) = io::stdin().read_line(&mut input) {
if n == 0 {
break;
}
// pop the trailing `\n`
input.pop();
base64_tokens.push(input.clone());
}
APITokenCmd::add_tokens(&base64_tokens, db_conn)
}

fn add_tokens(base64_tokens: &[String], db_conn: &DBConnection) -> io::Result<()> {
// filter duplicated tokens
let base64_tokens: HashSet<String> = HashSet::from_iter(base64_tokens.iter().cloned());
for base64_token in base64_tokens {
let token =
base64::decode_config(&base64_token, base64::URL_SAFE_NO_PAD).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"base64 encoded token `{}` is not valid due to:\n {}",
base64_token, e
),
)
})?;
let api_token_data = APITokenData {
token: APIToken::new(token),
creation_time: Utc::now().timestamp(),
expire_time: (Utc::now() + Duration::days(365)).timestamp(),
};
insert_token_data(api_token_data, db_conn)
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{}", e)))?;
}
Ok(())
}

fn handle_api_token_add(tokens: &Option<Vec<String>>, db_url: &str) -> io::Result<()> {
// check if db file exists
if !std::path::Path::new(db_url).exists() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{} url does not exists", db_url.to_string()),
));
}
let pool = load_db_connection_pool(db_url)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("{}", e)))?;
let db_conn = pool
.get()
.map_err(|e| io::Error::new(io::ErrorKind::NotConnected, format!("{}", e)))?;

match tokens {
// if not tokens are provided then listen to stdin for input ones
None => APITokenCmd::add_tokens_from_stream(&db_conn),
// process the provided tokens
Some(tokens) => APITokenCmd::add_tokens(tokens, &db_conn),
}
}

fn handle_generate(n: usize, size: usize) -> io::Result<()> {
let tokens = APITokenCmd::generate(n, size);
for token in tokens {
println!("{}", token);
}
Ok(())
}
}

impl ExecTask for APITokenCmd {
type ResultValue = ();

fn exec(&self) -> std::io::Result<()> {
match self {
APITokenCmd::Add { tokens, db_url } => {
APITokenCmd::handle_api_token_add(tokens, db_url)
}
APITokenCmd::Generate { n, size } => APITokenCmd::handle_generate(*n, *size),
}
}
}

impl ExecTask for CLIApp {
type ResultValue = ();

fn exec(&self) -> std::io::Result<Self::ResultValue> {
match self {
CLIApp::APIToken(api_token) => api_token.exec(),
}
}
}

#[cfg(test)]
mod test {
use super::*;
use vit_servicing_station_lib::db::{
load_db_connection_pool, migrations::initialize_db_with_migration,
queries::api_tokens::query_token_data_by_token,
};

#[test]
fn generate_token() {
let size = 10;
let n = 10;
let tokens = APITokenCmd::generate(n, size);
assert_eq!(tokens.len(), n);
tokens.iter().for_each(|token| {
assert_eq!(
base64::decode_config(token, base64::URL_SAFE_NO_PAD)
.unwrap()
.len(),
size
)
})
}

#[test]
fn add_token() {
let tokens = APITokenCmd::generate(10, 10);
let connection_pool = load_db_connection_pool("").unwrap();
initialize_db_with_migration(&connection_pool);
let db_conn = connection_pool.get().unwrap();
APITokenCmd::add_tokens(&tokens, &db_conn).unwrap();
for token in tokens
.iter()
.map(|t| base64::decode_config(t, base64::URL_SAFE_NO_PAD).unwrap())
{
assert!(query_token_data_by_token(token.as_ref(), &db_conn)
.unwrap()
.is_some());
}
}
}
15 changes: 15 additions & 0 deletions vit-servicing-station-cli/src/main.rs
@@ -0,0 +1,15 @@
mod app;

use app::*;
use structopt::StructOpt;

fn main() {
let app = CLIApp::from_args();
match app.exec() {
Ok(()) => (),
Err(e) => {
println!("Error: {}", e);
std::process::exit(1);
}
}
}
2 changes: 1 addition & 1 deletion vit-servicing-station-lib/Cargo.toml
Expand Up @@ -14,6 +14,7 @@ async-trait = "0.1.33"
base64 = "0.12.1"
chrono = { version = "0.4", features = ["serde"] }
diesel = { version = "1.4.4", features = ["sqlite", "r2d2"] }
diesel_migrations = "1.4.0"
dotenv = "0.9.0"
itertools = "0.9.0"
serde = { version = "1.0", features = ["derive"] }
Expand All @@ -30,5 +31,4 @@ warp = { version = "0.2", features = ["tls"] }
libsqlite3-sys = { version = "0.9.1", features = ["bundled"] }

[dev-dependencies]
diesel_migrations = "1.4.0"
tempfile = "3"
File renamed without changes.
5 changes: 2 additions & 3 deletions vit-servicing-station-lib/src/db/mod.rs
@@ -1,11 +1,9 @@
pub mod migrations;
pub mod models;
pub mod queries;
pub mod schema;
pub mod views_schema;

#[cfg(test)]
pub mod testing;

use diesel::r2d2::{ConnectionManager, Pool};
use diesel::sqlite::SqliteConnection;
use diesel::Connection;
Expand All @@ -14,6 +12,7 @@ pub type DBConnectionPool = Pool<ConnectionManager<SqliteConnection>>;
pub type Error = Box<dyn std::error::Error + Send + Sync>;
// TODO: Right now this is forced as the current backend. But it should be abstracted so it works for any diesel::Backend
type DB = diesel::sqlite::Sqlite;
pub type DBConnection = SqliteConnection;

// ⚠ WARNING ⚠ : This query is sqlite specific, would need to be changed if backend changes
const TEST_CONN_QUERY: &str = "
Expand Down
21 changes: 20 additions & 1 deletion vit-servicing-station-lib/src/db/models/api_tokens.rs
@@ -1,6 +1,6 @@
use crate::db::{schema::api_tokens, DB};
use crate::v0::api_token::APIToken;
use diesel::Queryable;
use diesel::{ExpressionMethods, Insertable, Queryable};

#[derive(Debug, Clone)]
pub struct APITokenData {
Expand All @@ -27,3 +27,22 @@ impl Queryable<api_tokens::SqlType, DB> for APITokenData {
}
}
}

// This warning is disabled here. Values is only referenced as a type here. It should be ok not to
// split the types definitions.
#[allow(clippy::type_complexity)]
impl Insertable<api_tokens::table> for APITokenData {
type Values = (
diesel::dsl::Eq<api_tokens::token, Vec<u8>>,
diesel::dsl::Eq<api_tokens::creation_time, i64>,
diesel::dsl::Eq<api_tokens::expire_time, i64>,
);

fn values(self) -> Self::Values {
(
api_tokens::token.eq(self.token.as_ref().to_vec()),
api_tokens::creation_time.eq(self.creation_time),
api_tokens::expire_time.eq(self.expire_time),
)
}
}

0 comments on commit 44a07fc

Please sign in to comment.