diff --git a/.github/workflows/run_migrations.yml b/.github/workflows/run_migrations.yml index a709689a1..b151de892 100644 --- a/.github/workflows/run_migrations.yml +++ b/.github/workflows/run_migrations.yml @@ -9,11 +9,13 @@ on: - '**up.sql' - '**down.sql' - '**db.rs' + - 'src/bin/**' pull_request: paths: - '**up.sql' - '**down.sql' - '**db.rs' + - 'src/bin/**' workflow_dispatch: env: diff --git a/CHANGELOG.md b/CHANGELOG.md index efc153707..75d835b19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [[0.13.6]](https://github.com/thoth-pub/thoth/releases/tag/v0.13.6) - 2025-01-28 +### Changed + - [667](https://github.com/thoth-pub/thoth/pull/667) - Refactor binary using new submodules `commands` and `arguments` + - [667](https://github.com/thoth-pub/thoth/pull/667) - Trigger `run\_migrations` github action when binary source changes + +### Added + - [667](https://github.com/thoth-pub/thoth/pull/667) - CLI subcommand `thoth account publishers` to modify which publisher(s) an account has access to + ## [[0.13.5]](https://github.com/thoth-pub/thoth/releases/tag/v0.13.5) - 2025-01-17 ### Changed - [665](https://github.com/thoth-pub/thoth/pull/665) - Removed unnecessary `map_or()` to comply with [`rustc 1.84.0`](https://github.com/rust-lang/rust/releases/tag/1.84.0) diff --git a/Cargo.lock b/Cargo.lock index 2b2fcaede..2e0cc447e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3459,11 +3459,12 @@ dependencies = [ [[package]] name = "thoth" -version = "0.13.5" +version = "0.13.6" dependencies = [ "clap", "dialoguer", "dotenv", + "lazy_static", "thoth-api", "thoth-api-server", "thoth-app-server", @@ -3474,7 +3475,7 @@ dependencies = [ [[package]] name = "thoth-api" -version = "0.13.5" +version = "0.13.6" dependencies = [ "actix-web", "argon2rs", @@ -3503,7 +3504,7 @@ dependencies = [ [[package]] name = "thoth-api-server" -version = "0.13.5" +version = "0.13.6" dependencies = [ "actix-cors", "actix-http", @@ -3521,7 +3522,7 @@ dependencies = [ [[package]] name = "thoth-app" -version = "0.13.5" +version = "0.13.6" dependencies = [ "chrono", "dotenv", @@ -3546,7 +3547,7 @@ dependencies = [ [[package]] name = "thoth-app-server" -version = "0.13.5" +version = "0.13.6" dependencies = [ "actix-cors", "actix-web", @@ -3556,7 +3557,7 @@ dependencies = [ [[package]] name = "thoth-client" -version = "0.13.5" +version = "0.13.6" dependencies = [ "chrono", "graphql_client", @@ -3572,7 +3573,7 @@ dependencies = [ [[package]] name = "thoth-errors" -version = "0.13.5" +version = "0.13.6" dependencies = [ "actix-web", "chrono", @@ -3595,7 +3596,7 @@ dependencies = [ [[package]] name = "thoth-export-server" -version = "0.13.5" +version = "0.13.6" dependencies = [ "actix-cors", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index 2c57467c2..be855b797 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth" -version = "0.13.5" +version = "0.13.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2021" license = "Apache-2.0" @@ -15,12 +15,13 @@ maintenance = { status = "actively-developed" } members = ["thoth-api", "thoth-api-server", "thoth-app", "thoth-app-server", "thoth-client", "thoth-errors", "thoth-export-server"] [dependencies] -thoth-api = { version = "=0.13.5", path = "thoth-api", features = ["backend"] } -thoth-api-server = { version = "=0.13.5", path = "thoth-api-server" } -thoth-app-server = { version = "=0.13.5", path = "thoth-app-server" } -thoth-errors = { version = "=0.13.5", path = "thoth-errors" } -thoth-export-server = { version = "=0.13.5", path = "thoth-export-server" } +thoth-api = { version = "=0.13.6", path = "thoth-api", features = ["backend"] } +thoth-api-server = { version = "=0.13.6", path = "thoth-api-server" } +thoth-app-server = { version = "=0.13.6", path = "thoth-app-server" } +thoth-errors = { version = "=0.13.6", path = "thoth-errors" } +thoth-export-server = { version = "=0.13.6", path = "thoth-export-server" } clap = { version = "4.5.21", features = ["cargo", "env"] } dialoguer = { version = "0.11.0", features = ["password"] } dotenv = "0.15.0" +lazy_static = "1.5.0" tokio = { version = "1.43.0", features = ["rt", "rt-multi-thread", "macros"] } diff --git a/src/bin/arguments/mod.rs b/src/bin/arguments/mod.rs new file mode 100644 index 000000000..53d75d51f --- /dev/null +++ b/src/bin/arguments/mod.rs @@ -0,0 +1,140 @@ +use clap::{value_parser, Arg, ArgAction}; + +pub fn database() -> Arg { + Arg::new("db") + .short('D') + .long("database-url") + .value_name("DATABASE_URL") + .env("DATABASE_URL") + .help("Full postgres database url, e.g. postgres://thoth:thoth@localhost/thoth") + .num_args(1) +} + +pub fn redis() -> Arg { + Arg::new("redis") + .short('R') + .long("redis-url") + .value_name("REDIS_URL") + .env("REDIS_URL") + .help("Full redis url, e.g. redis://localhost:6379") + .num_args(1) +} + +pub fn host(env_value: &'static str) -> Arg { + Arg::new("host") + .short('H') + .long("host") + .value_name("HOST") + .env(env_value) + .default_value("0.0.0.0") + .help("host to bind") + .num_args(1) +} + +pub fn port(default_value: &'static str, env_value: &'static str) -> Arg { + Arg::new("port") + .short('p') + .long("port") + .value_name("PORT") + .env(env_value) + .default_value(default_value) + .help("Port to bind") + .num_args(1) +} + +pub fn domain() -> Arg { + Arg::new("domain") + .short('d') + .long("domain") + .value_name("THOTH_DOMAIN") + .env("THOTH_DOMAIN") + .default_value("localhost") + .help("Authentication cookie domain") + .num_args(1) +} + +pub fn key() -> Arg { + Arg::new("key") + .short('k') + .long("secret-key") + .value_name("SECRET") + .env("SECRET_KEY") + .help("Authentication cookie secret key") + .num_args(1) +} + +pub fn session() -> Arg { + Arg::new("duration") + .short('s') + .long("session-length") + .value_name("DURATION") + .env("SESSION_DURATION_SECONDS") + .default_value("3600") + .help("Authentication cookie session duration (seconds)") + .num_args(1) + .value_parser(value_parser!(i64)) +} + +pub fn gql_url() -> Arg { + Arg::new("gql-url") + .short('u') + .long("gql-url") + .value_name("THOTH_GRAPHQL_API") + .env("THOTH_GRAPHQL_API") + .default_value("http://localhost:8000") + .help("Thoth GraphQL's, public facing, root URL.") + .num_args(1) +} + +pub fn gql_endpoint() -> Arg { + Arg::new("gql-endpoint") + .short('g') + .long("gql-endpoint") + .value_name("THOTH_GRAPHQL_ENDPOINT") + .env("THOTH_GRAPHQL_ENDPOINT") + .default_value("http://localhost:8000/graphql") + .help("Thoth GraphQL's endpoint") + .num_args(1) +} + +pub fn export_url() -> Arg { + Arg::new("export-url") + .short('u') + .long("export-url") + .value_name("THOTH_EXPORT_API") + .env("THOTH_EXPORT_API") + .default_value("http://localhost:8181") + .help("Thoth Export API's, public facing, root URL.") + .num_args(1) +} + +pub fn threads(env_value: &'static str) -> Arg { + Arg::new("threads") + .short('t') + .long("threads") + .value_name("THREADS") + .env(env_value) + .default_value("5") + .help("Number of HTTP workers to start") + .num_args(1) + .value_parser(value_parser!(usize)) +} + +pub fn keep_alive(env_value: &'static str) -> Arg { + Arg::new("keep-alive") + .short('K') + .long("keep-alive") + .value_name("THREADS") + .env(env_value) + .default_value("5") + .help("Number of seconds to wait for subsequent requests") + .num_args(1) + .value_parser(value_parser!(u64)) +} + +pub fn revert() -> Arg { + Arg::new("revert") + .long("revert") + .help("Revert all database migrations") + .action(ArgAction::SetTrue) +} diff --git a/src/bin/commands/account.rs b/src/bin/commands/account.rs new file mode 100644 index 000000000..629c637ed --- /dev/null +++ b/src/bin/commands/account.rs @@ -0,0 +1,175 @@ +use super::get_pg_pool; +use crate::arguments; +use clap::Command; +use dialoguer::{console::Term, theme::ColorfulTheme, Input, MultiSelect, Password, Select}; +use lazy_static::lazy_static; +use std::collections::HashSet; +use thoth::{ + api::{ + account::{ + model::{Account, LinkedPublisher}, + service::{ + all_emails, all_publishers, get_account, register as register_account, + update_password, + }, + }, + db::PgPool, + }, + errors::{ThothError, ThothResult}, +}; + +lazy_static! { + pub(crate) static ref COMMAND: Command = Command::new("account") + .about("Manage user accounts") + .arg(arguments::database()) + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand(Command::new("register").about("Create a new user account")) + .subcommand( + Command::new("publishers").about("Select which publisher(s) this account can manage"), + ) + .subcommand(Command::new("password").about("Reset a password")); +} + +pub fn register(arguments: &clap::ArgMatches) -> ThothResult<()> { + let pool = get_pg_pool(arguments); + + let name = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter given name") + .interact_on(&Term::stdout())?; + let surname = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter family name") + .interact_on(&Term::stdout())?; + let email = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter email address") + .interact_on(&Term::stdout())?; + let password = password_input()?; + let is_superuser: bool = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Is this a superuser account") + .default(false) + .interact_on(&Term::stdout())?; + let is_bot: bool = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Is this a bot account") + .default(false) + .interact_on(&Term::stdout())?; + + let account = register_account(&pool, name, surname, email, password, is_superuser, is_bot)?; + select_and_link_publishers(&pool, &account) +} + +pub fn publishers(arguments: &clap::ArgMatches) -> ThothResult<()> { + let pool = get_pg_pool(arguments); + let account = email_selection(&pool).and_then(|email| get_account(&email, &pool))?; + select_and_link_publishers(&pool, &account) +} + +pub fn password(arguments: &clap::ArgMatches) -> ThothResult<()> { + let pool = get_pg_pool(arguments); + let email = email_selection(&pool)?; + let password = password_input()?; + + update_password(&email, &password, &pool).map(|_| ()) +} + +fn email_selection(pool: &PgPool) -> ThothResult { + let all_emails = all_emails(pool).expect("No user accounts present in database."); + let email_labels: Vec = all_emails + .iter() + .map(|(email, is_superuser, is_bot, is_active)| { + let mut label = email.clone(); + if *is_superuser { + label.push_str(" 👑"); + } + if *is_bot { + label.push_str(" 🤖"); + } + if !is_active { + label.push_str(" ❌"); + } + label + }) + .collect(); + let email_selection = Select::with_theme(&ColorfulTheme::default()) + .items(&email_labels) + .default(0) + .with_prompt("Select a user account") + .interact_on(&Term::stdout())?; + all_emails + .get(email_selection) + .map(|(email, _, _, _)| email.clone()) + .ok_or_else(|| ThothError::InternalError("Invalid user selection".into())) +} + +fn password_input() -> ThothResult { + Password::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter password") + .with_confirmation("Confirm password", "Passwords do not match") + .interact_on(&Term::stdout()) + .map_err(Into::into) +} + +fn is_admin_input(publisher_name: &str) -> ThothResult { + Input::with_theme(&ColorfulTheme::default()) + .with_prompt(format!("Make user an admin of '{}'?", publisher_name)) + .default(false) + .interact_on(&Term::stdout()) + .map_err(Into::into) +} + +fn select_and_link_publishers(pool: &PgPool, account: &Account) -> ThothResult<()> { + let publishers = all_publishers(pool)?; + let publisher_accounts = account.get_publisher_accounts(pool)?; + let current_ids: HashSet<(_, _)> = publisher_accounts + .iter() + .map(|pa| (pa.publisher_id, pa.is_admin)) + .collect(); + + let items_checked: Vec<(_, _)> = publishers + .iter() + .map(|p| { + let is_admin = current_ids + .iter() + .find(|(id, _)| *id == p.publisher_id) + .is_some_and(|(_, admin)| *admin); + let is_linked = current_ids.iter().any(|(id, _)| *id == p.publisher_id); + let mut publisher = p.clone(); + if is_admin { + publisher.publisher_name = format!("{} 🔑", publisher.publisher_name); + } + (publisher, is_linked) + }) + .collect(); + + let chosen: Vec = MultiSelect::with_theme(&ColorfulTheme::default()) + .with_prompt("Select publishers to link this account to") + .items_checked(&items_checked) + .interact_on(&Term::stdout())?; + let chosen_ids: HashSet<_> = chosen + .iter() + .map(|&index| items_checked[index].0.publisher_id) + .collect(); + let to_add: Vec<_> = publishers + .iter() + .filter(|p| { + chosen_ids.contains(&p.publisher_id) + && !current_ids.iter().any(|(id, _)| id == &p.publisher_id) + }) + .collect(); + let to_remove: Vec<_> = publisher_accounts + .iter() + .filter(|pa| !chosen_ids.contains(&pa.publisher_id)) + .collect(); + + for publisher in to_add { + let is_admin: bool = is_admin_input(&publisher.publisher_name)?; + let linked_publisher = LinkedPublisher { + publisher_id: publisher.publisher_id, + is_admin, + }; + account.add_publisher_account(pool, linked_publisher)?; + } + for publisher_account in to_remove { + publisher_account.delete(pool)?; + } + Ok(()) +} diff --git a/src/bin/commands/cache.rs b/src/bin/commands/cache.rs new file mode 100644 index 000000000..c9ff9c29f --- /dev/null +++ b/src/bin/commands/cache.rs @@ -0,0 +1,42 @@ +use crate::arguments; +use crate::commands::get_redis_pool; +use clap::{ArgMatches, Command}; +use dialoguer::{console::Term, theme::ColorfulTheme, MultiSelect}; +use lazy_static::lazy_static; +use thoth::{ + api::redis::{del, scan_match}, + errors::{ThothError, ThothResult}, + ALL_SPECIFICATIONS, +}; + +lazy_static! { + pub(crate) static ref COMMAND: Command = Command::new("cache") + .about("Manage cached records") + .arg(arguments::redis()) + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand(Command::new("delete").about("Delete cached records")); +} + +pub fn delete(arguments: &ArgMatches) -> ThothResult<()> { + let pool = get_redis_pool(arguments); + let chosen: Vec = MultiSelect::with_theme(&ColorfulTheme::default()) + .items(&ALL_SPECIFICATIONS) + .with_prompt("Select cached specifications to delete") + .interact_on(&Term::stdout())?; + // run a separate tokio runtime to avoid interfering with actix's threads + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .build()?; + runtime.block_on(async { + for index in chosen { + let specification = ALL_SPECIFICATIONS.get(index).unwrap(); + let keys = scan_match(&pool, &format!("{}*", specification)).await?; + for key in keys { + del(&pool, &key).await?; + } + } + Ok::<(), ThothError>(()) + }) +} diff --git a/src/bin/commands/mod.rs b/src/bin/commands/mod.rs new file mode 100644 index 000000000..f6a585c9e --- /dev/null +++ b/src/bin/commands/mod.rs @@ -0,0 +1,65 @@ +use crate::arguments; +use clap::Command; +use lazy_static::lazy_static; +use thoth::{ + api::{ + db::{ + init_pool as init_pg_pool, revert_migrations as revert_db_migrations, + run_migrations as run_db_migrations, PgPool, + }, + redis::{init_pool as init_redis_pool, RedisPool}, + }, + errors::ThothResult, +}; + +pub(super) mod account; +pub(super) mod cache; +pub(super) mod start; + +lazy_static! { + pub(super) static ref INIT: Command = Command::new("init") + .about("Run the database migrations and start the thoth API server") + .arg(arguments::database()) + .arg(arguments::host("GRAPHQL_API_HOST")) + .arg(arguments::port("8000", "GRAPHQL_API_PORT")) + .arg(arguments::threads("GRAPHQL_API_THREADS")) + .arg(arguments::keep_alive("GRAPHQL_API_KEEP_ALIVE")) + .arg(arguments::gql_url()) + .arg(arguments::domain()) + .arg(arguments::key()) + .arg(arguments::session()); +} + +lazy_static! { + pub(super) static ref MIGRATE: Command = Command::new("migrate") + .about("Run the database migrations") + .arg(arguments::database()) + .arg(arguments::revert()); +} + +fn get_pg_pool(arguments: &clap::ArgMatches) -> PgPool { + let database_url = arguments.get_one::("db").unwrap(); + init_pg_pool(database_url) +} + +fn get_redis_pool(arguments: &clap::ArgMatches) -> RedisPool { + let redis_url = arguments.get_one::("redis").unwrap(); + init_redis_pool(redis_url) +} + +pub(super) fn migrate(arguments: &clap::ArgMatches) -> ThothResult<()> { + match arguments.get_flag("revert") { + true => revert_migrations(arguments), + false => run_migrations(arguments), + } +} + +pub(super) fn run_migrations(arguments: &clap::ArgMatches) -> ThothResult<()> { + let database_url = arguments.get_one::("db").unwrap(); + run_db_migrations(database_url) +} + +fn revert_migrations(arguments: &clap::ArgMatches) -> ThothResult<()> { + let database_url = arguments.get_one::("db").unwrap(); + revert_db_migrations(database_url) +} diff --git a/src/bin/commands/start.rs b/src/bin/commands/start.rs new file mode 100644 index 000000000..9ef2f3c8d --- /dev/null +++ b/src/bin/commands/start.rs @@ -0,0 +1,101 @@ +use crate::arguments; +use clap::{ArgMatches, Command}; +use lazy_static::lazy_static; +use thoth::{api_server, app_server, errors::ThothResult, export_server}; + +lazy_static! { + pub(crate) static ref COMMAND: Command = Command::new("start") + .about("Start an instance of Thoth API or GUI") + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + Command::new("graphql-api") + .about("Start the thoth GraphQL API server") + .arg(arguments::database()) + .arg(arguments::host("GRAPHQL_API_HOST")) + .arg(arguments::port("8000", "GRAPHQL_API_PORT")) + .arg(arguments::threads("GRAPHQL_API_THREADS")) + .arg(arguments::keep_alive("GRAPHQL_API_KEEP_ALIVE")) + .arg(arguments::gql_url()) + .arg(arguments::domain()) + .arg(arguments::key()) + .arg(arguments::session()), + ) + .subcommand( + Command::new("app") + .about("Start the thoth client GUI") + .arg(arguments::host("APP_HOST")) + .arg(arguments::port("8080", "APP_PORT")) + .arg(arguments::threads("APP_THREADS")) + .arg(arguments::keep_alive("APP_KEEP_ALIVE")), + ) + .subcommand( + Command::new("export-api") + .about("Start the thoth metadata export API") + .arg(arguments::redis()) + .arg(arguments::host("EXPORT_API_HOST")) + .arg(arguments::port("8181", "EXPORT_API_PORT")) + .arg(arguments::threads("EXPORT_API_THREADS")) + .arg(arguments::keep_alive("EXPORT_API_KEEP_ALIVE")) + .arg(arguments::export_url()) + .arg(arguments::gql_endpoint()), + ); +} + +pub fn graphql_api(arguments: &ArgMatches) -> ThothResult<()> { + let database_url = arguments.get_one::("db").unwrap().to_owned(); + let host = arguments.get_one::("host").unwrap().to_owned(); + let port = arguments.get_one::("port").unwrap().to_owned(); + let threads = *arguments.get_one::("threads").unwrap(); + let keep_alive = *arguments.get_one::("keep-alive").unwrap(); + let url = arguments.get_one::("gql-url").unwrap().to_owned(); + let domain = arguments.get_one::("domain").unwrap().to_owned(); + let secret_str = arguments.get_one::("key").unwrap().to_owned(); + let session_duration = *arguments.get_one::("duration").unwrap(); + api_server( + database_url, + host, + port, + threads, + keep_alive, + url, + domain, + secret_str, + session_duration, + ) + .map_err(|e| e.into()) +} + +pub fn app(arguments: &ArgMatches) -> ThothResult<()> { + let host = arguments.get_one::("host").unwrap().to_owned(); + let port = arguments.get_one::("port").unwrap().to_owned(); + let threads = *arguments.get_one::("threads").unwrap(); + let keep_alive = *arguments.get_one::("keep-alive").unwrap(); + app_server(host, port, threads, keep_alive).map_err(|e| e.into()) +} + +pub fn export_api(arguments: &ArgMatches) -> ThothResult<()> { + let redis_url = arguments.get_one::("redis").unwrap().to_owned(); + let host = arguments.get_one::("host").unwrap().to_owned(); + let port = arguments.get_one::("port").unwrap().to_owned(); + let threads = *arguments.get_one::("threads").unwrap(); + let keep_alive = *arguments.get_one::("keep-alive").unwrap(); + let url = arguments + .get_one::("export-url") + .unwrap() + .to_owned(); + let gql_endpoint = arguments + .get_one::("gql-endpoint") + .unwrap() + .to_owned(); + export_server( + redis_url, + host, + port, + threads, + keep_alive, + url, + gql_endpoint, + ) + .map_err(|e| e.into()) +} diff --git a/src/bin/thoth.rs b/src/bin/thoth.rs index 78b560d58..42597884b 100644 --- a/src/bin/thoth.rs +++ b/src/bin/thoth.rs @@ -1,445 +1,44 @@ -use clap::{crate_authors, crate_version, value_parser, Arg, ArgAction, Command}; -use dialoguer::{console::Term, theme::ColorfulTheme, Input, MultiSelect, Password, Select}; -use dotenv::dotenv; -use std::env; -use thoth::{ - api::{ - account::{ - model::{AccountData, LinkedPublisher}, - service::{all_emails, all_publishers, register, update_password}, - }, - db::{init_pool as init_pg_pool, revert_migrations, run_migrations}, - redis::{del, init_pool as init_redis_pool, scan_match}, - }, - api_server, app_server, - errors::{ThothError, ThothResult}, - export_server, ALL_SPECIFICATIONS, -}; - -fn database_argument() -> Arg { - Arg::new("db") - .short('D') - .long("database-url") - .value_name("DATABASE_URL") - .env("DATABASE_URL") - .help("Full postgres database url, e.g. postgres://thoth:thoth@localhost/thoth") - .num_args(1) -} - -fn redis_argument() -> Arg { - Arg::new("redis") - .short('R') - .long("redis-url") - .value_name("REDIS_URL") - .env("REDIS_URL") - .help("Full redis url, e.g. redis://localhost:6379") - .num_args(1) -} - -fn host_argument(env_value: &'static str) -> Arg { - Arg::new("host") - .short('H') - .long("host") - .value_name("HOST") - .env(env_value) - .default_value("0.0.0.0") - .help("host to bind") - .num_args(1) -} - -fn port_argument(default_value: &'static str, env_value: &'static str) -> Arg { - Arg::new("port") - .short('p') - .long("port") - .value_name("PORT") - .env(env_value) - .default_value(default_value) - .help("Port to bind") - .num_args(1) -} - -fn domain_argument() -> Arg { - Arg::new("domain") - .short('d') - .long("domain") - .value_name("THOTH_DOMAIN") - .env("THOTH_DOMAIN") - .default_value("localhost") - .help("Authentication cookie domain") - .num_args(1) -} - -fn key_argument() -> Arg { - Arg::new("key") - .short('k') - .long("secret-key") - .value_name("SECRET") - .env("SECRET_KEY") - .help("Authentication cookie secret key") - .num_args(1) -} - -fn session_argument() -> Arg { - Arg::new("duration") - .short('s') - .long("session-length") - .value_name("DURATION") - .env("SESSION_DURATION_SECONDS") - .default_value("3600") - .help("Authentication cookie session duration (seconds)") - .num_args(1) - .value_parser(value_parser!(i64)) -} - -fn gql_url_argument() -> Arg { - Arg::new("gql-url") - .short('u') - .long("gql-url") - .value_name("THOTH_GRAPHQL_API") - .env("THOTH_GRAPHQL_API") - .default_value("http://localhost:8000") - .help("Thoth GraphQL's, public facing, root URL.") - .num_args(1) -} - -fn gql_endpoint_argument() -> Arg { - Arg::new("gql-endpoint") - .short('g') - .long("gql-endpoint") - .value_name("THOTH_GRAPHQL_ENDPOINT") - .env("THOTH_GRAPHQL_ENDPOINT") - .default_value("http://localhost:8000/graphql") - .help("Thoth GraphQL's endpoint") - .num_args(1) -} - -fn export_url_argument() -> Arg { - Arg::new("export-url") - .short('u') - .long("export-url") - .value_name("THOTH_EXPORT_API") - .env("THOTH_EXPORT_API") - .default_value("http://localhost:8181") - .help("Thoth Export API's, public facing, root URL.") - .num_args(1) -} - -fn threads_argument(env_value: &'static str) -> Arg { - Arg::new("threads") - .short('t') - .long("threads") - .value_name("THREADS") - .env(env_value) - .default_value("5") - .help("Number of HTTP workers to start") - .num_args(1) - .value_parser(value_parser!(usize)) -} - -fn keep_alive_argument(env_value: &'static str) -> Arg { - Arg::new("keep-alive") - .short('K') - .long("keep-alive") - .value_name("THREADS") - .env(env_value) - .default_value("5") - .help("Number of seconds to wait for subsequent requests") - .num_args(1) - .value_parser(value_parser!(u64)) -} +mod arguments; +mod commands; -fn thoth_commands() -> Command { - Command::new(env!("CARGO_PKG_NAME")) - .version(crate_version!()) - .author(crate_authors!("\n")) +lazy_static::lazy_static! { + static ref THOTH: clap::Command = clap::Command::new(env!("CARGO_PKG_NAME")) + .version(clap::crate_version!()) + .author(clap::crate_authors!("\n")) .about(env!("CARGO_PKG_DESCRIPTION")) .subcommand_required(true) .arg_required_else_help(true) - .subcommand( - Command::new("migrate") - .about("Run the database migrations") - .arg(database_argument()) - .arg( - Arg::new("revert") - .long("revert") - .help("Revert all database migrations") - .action(ArgAction::SetTrue), - ), - ) - .subcommand( - Command::new("start") - .about("Start an instance of Thoth API or GUI") - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new("graphql-api") - .about("Start the thoth GraphQL API server") - .arg(database_argument()) - .arg(host_argument("GRAPHQL_API_HOST")) - .arg(port_argument("8000", "GRAPHQL_API_PORT")) - .arg(threads_argument("GRAPHQL_API_THREADS")) - .arg(keep_alive_argument("GRAPHQL_API_KEEP_ALIVE")) - .arg(gql_url_argument()) - .arg(domain_argument()) - .arg(key_argument()) - .arg(session_argument()), - ) - .subcommand( - Command::new("app") - .about("Start the thoth client GUI") - .arg(host_argument("APP_HOST")) - .arg(port_argument("8080", "APP_PORT")) - .arg(threads_argument("APP_THREADS")) - .arg(keep_alive_argument("APP_KEEP_ALIVE")), - ) - .subcommand( - Command::new("export-api") - .about("Start the thoth metadata export API") - .arg(redis_argument()) - .arg(host_argument("EXPORT_API_HOST")) - .arg(port_argument("8181", "EXPORT_API_PORT")) - .arg(threads_argument("EXPORT_API_THREADS")) - .arg(keep_alive_argument("EXPORT_API_KEEP_ALIVE")) - .arg(export_url_argument()) - .arg(gql_endpoint_argument()), - ), - ) - .subcommand( - Command::new("init") - .about("Run the database migrations and start the thoth API server") - .arg(database_argument()) - .arg(host_argument("GRAPHQL_API_HOST")) - .arg(port_argument("8000", "GRAPHQL_API_PORT")) - .arg(threads_argument("GRAPHQL_API_THREADS")) - .arg(keep_alive_argument("GRAPHQL_API_KEEP_ALIVE")) - .arg(gql_url_argument()) - .arg(domain_argument()) - .arg(key_argument()) - .arg(session_argument()), - ) - .subcommand( - Command::new("account") - .about("Manage user accounts") - .arg(database_argument()) - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand(Command::new("register").about("Create a new user account")) - .subcommand(Command::new("password").about("Reset a password")), - ) - .subcommand( - Command::new("cache") - .about("Manage cached records") - .arg(redis_argument()) - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand(Command::new("delete").about("Delete cached records")), - ) + .subcommand(commands::MIGRATE.clone()) + .subcommand(commands::start::COMMAND.clone()) + .subcommand(commands::INIT.clone()) + .subcommand(commands::account::COMMAND.clone()) + .subcommand(commands::cache::COMMAND.clone()); } -fn main() -> ThothResult<()> { +fn main() -> thoth::errors::ThothResult<()> { // load environment variables from `.env` - dotenv().ok(); + dotenv::dotenv().ok(); - match thoth_commands().get_matches().subcommand() { - Some(("start", start_matches)) => match start_matches.subcommand() { - Some(("graphql-api", api_matches)) => { - let database_url = api_matches.get_one::("db").unwrap().to_owned(); - let host = api_matches.get_one::("host").unwrap().to_owned(); - let port = api_matches.get_one::("port").unwrap().to_owned(); - let threads = *api_matches.get_one::("threads").unwrap(); - let keep_alive = *api_matches.get_one::("keep-alive").unwrap(); - let url = api_matches.get_one::("gql-url").unwrap().to_owned(); - let domain = api_matches.get_one::("domain").unwrap().to_owned(); - let secret_str = api_matches.get_one::("key").unwrap().to_owned(); - let session_duration = *api_matches.get_one::("duration").unwrap(); - api_server( - database_url, - host, - port, - threads, - keep_alive, - url, - domain, - secret_str, - session_duration, - ) - .map_err(|e| e.into()) - } - Some(("app", client_matches)) => { - let host = client_matches.get_one::("host").unwrap().to_owned(); - let port = client_matches.get_one::("port").unwrap().to_owned(); - let threads = *client_matches.get_one::("threads").unwrap(); - let keep_alive = *client_matches.get_one::("keep-alive").unwrap(); - app_server(host, port, threads, keep_alive).map_err(|e| e.into()) - } - Some(("export-api", client_matches)) => { - let redis_url = client_matches - .get_one::("redis") - .unwrap() - .to_owned(); - let host = client_matches.get_one::("host").unwrap().to_owned(); - let port = client_matches.get_one::("port").unwrap().to_owned(); - let threads = *client_matches.get_one::("threads").unwrap(); - let keep_alive = *client_matches.get_one::("keep-alive").unwrap(); - let url = client_matches - .get_one::("export-url") - .unwrap() - .to_owned(); - let gql_endpoint = client_matches - .get_one::("gql-endpoint") - .unwrap() - .to_owned(); - export_server( - redis_url, - host, - port, - threads, - keep_alive, - url, - gql_endpoint, - ) - .map_err(|e| e.into()) - } + match THOTH.clone().get_matches().subcommand() { + Some(("start", start_arguments)) => match start_arguments.subcommand() { + Some(("graphql-api", arguments)) => commands::start::graphql_api(arguments), + Some(("app", arguments)) => commands::start::app(arguments), + Some(("export-api", arguments)) => commands::start::export_api(arguments), _ => unreachable!(), }, - Some(("migrate", migrate_matches)) => { - let database_url = migrate_matches.get_one::("db").unwrap(); - match migrate_matches.get_flag("revert") { - true => revert_migrations(database_url), - false => run_migrations(database_url), - } - } - Some(("init", init_matches)) => { - let database_url = init_matches.get_one::("db").unwrap().to_owned(); - let host = init_matches.get_one::("host").unwrap().to_owned(); - let port = init_matches.get_one::("port").unwrap().to_owned(); - let threads = *init_matches.get_one::("threads").unwrap(); - let keep_alive = *init_matches.get_one::("keep-alive").unwrap(); - let url = init_matches - .get_one::("gql-url") - .unwrap() - .to_owned(); - let domain = init_matches.get_one::("domain").unwrap().to_owned(); - let secret_str = init_matches.get_one::("key").unwrap().to_owned(); - let session_duration = *init_matches.get_one::("duration").unwrap(); - run_migrations(&database_url)?; - api_server( - database_url, - host, - port, - threads, - keep_alive, - url, - domain, - secret_str, - session_duration, - ) - .map_err(|e| e.into()) - } - Some(("account", account_matches)) => { - let database_url = account_matches.get_one::("db").unwrap(); - match account_matches.subcommand() { - Some(("register", _)) => { - let pool = init_pg_pool(database_url); - - let name = Input::new() - .with_prompt("Enter given name") - .interact_on(&Term::stdout())?; - let surname = Input::new() - .with_prompt("Enter family name") - .interact_on(&Term::stdout())?; - let email = Input::new() - .with_prompt("Enter email address") - .interact_on(&Term::stdout())?; - let password = Password::new() - .with_prompt("Enter password") - .with_confirmation("Confirm password", "Passwords do not match") - .interact_on(&Term::stdout())?; - let is_superuser: bool = Input::new() - .with_prompt("Is this a superuser account") - .default(false) - .interact_on(&Term::stdout())?; - let is_bot: bool = Input::new() - .with_prompt("Is this a bot account") - .default(false) - .interact_on(&Term::stdout())?; - - let mut linked_publishers = vec![]; - if let Ok(publishers) = all_publishers(&pool) { - let chosen: Vec = MultiSelect::new() - .items(&publishers) - .with_prompt("Select publishers to link this account to") - .interact_on(&Term::stdout())?; - for index in chosen { - let publisher = publishers.get(index).unwrap(); - let is_admin: bool = Input::new() - .with_prompt(format!( - "Make user an admin of '{}'?", - publisher.publisher_name - )) - .default(false) - .interact_on(&Term::stdout())?; - let linked_publisher = LinkedPublisher { - publisher_id: publisher.publisher_id, - is_admin, - }; - linked_publishers.push(linked_publisher); - } - } - let account_data = AccountData { - name, - surname, - email, - password, - is_superuser, - is_bot, - }; - register(account_data, linked_publishers, &pool).map(|_| ()) - } - Some(("password", _)) => { - let pool = init_pg_pool(database_url); - let all_emails = - all_emails(&pool).expect("No user accounts present in database."); - let email_selection = Select::with_theme(&ColorfulTheme::default()) - .items(&all_emails) - .default(0) - .with_prompt("Select a user account") - .interact_on(&Term::stdout())?; - let password = Password::new() - .with_prompt("Enter new password") - .with_confirmation("Confirm password", "Passwords do not match") - .interact_on(&Term::stdout())?; - let email = all_emails.get(email_selection).unwrap(); - - update_password(email, &password, &pool).map(|_| ()) - } - _ => unreachable!(), - } + Some(("migrate", arguments)) => commands::migrate(arguments), + Some(("init", arguments)) => { + commands::run_migrations(arguments)?; + commands::start::graphql_api(arguments) } - Some(("cache", cache_matches)) => match cache_matches.subcommand() { - Some(("delete", _)) => { - let redis_url = cache_matches.get_one::("redis").unwrap(); - let pool = init_redis_pool(redis_url); - let chosen: Vec = MultiSelect::new() - .items(&ALL_SPECIFICATIONS) - .with_prompt("Select cached specifications to delete") - .interact_on(&Term::stdout())?; - // run a separate tokio runtime to avoid interfering with actix's threads - let runtime = tokio::runtime::Builder::new_multi_thread() - .worker_threads(1) - .enable_all() - .build()?; - runtime.block_on(async { - for index in chosen { - let specification = ALL_SPECIFICATIONS.get(index).unwrap(); - let keys = scan_match(&pool, &format!("{}*", specification)).await?; - for key in keys { - del(&pool, &key).await?; - } - } - Ok::<(), ThothError>(()) - }) - } + Some(("account", arguments)) => match arguments.subcommand() { + Some(("register", _)) => commands::account::register(arguments), + Some(("publishers", _)) => commands::account::publishers(arguments), + Some(("password", _)) => commands::account::password(arguments), + _ => unreachable!(), + }, + Some(("cache", arguments)) => match arguments.subcommand() { + Some(("delete", _)) => commands::cache::delete(arguments), _ => unreachable!(), }, _ => unreachable!(), @@ -448,5 +47,5 @@ fn main() -> ThothResult<()> { #[test] fn test_cli() { - thoth_commands().debug_assert(); + THOTH.clone().debug_assert(); } diff --git a/thoth-api-server/Cargo.toml b/thoth-api-server/Cargo.toml index 53238b16a..1a1659566 100644 --- a/thoth-api-server/Cargo.toml +++ b/thoth-api-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-api-server" -version = "0.13.5" +version = "0.13.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2021" license = "Apache-2.0" @@ -9,8 +9,8 @@ repository = "https://github.com/thoth-pub/thoth" readme = "README.md" [dependencies] -thoth-api = { version = "=0.13.5", path = "../thoth-api", features = ["backend"] } -thoth-errors = { version = "=0.13.5", path = "../thoth-errors" } +thoth-api = { version = "=0.13.6", path = "../thoth-api", features = ["backend"] } +thoth-errors = { version = "=0.13.6", path = "../thoth-errors" } actix-web = "4.9" actix-cors = "0.7.0" actix-http = "3.9.0" diff --git a/thoth-api/Cargo.toml b/thoth-api/Cargo.toml index aa05f876f..1f7515a00 100644 --- a/thoth-api/Cargo.toml +++ b/thoth-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-api" -version = "0.13.5" +version = "0.13.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2021" license = "Apache-2.0" @@ -15,7 +15,7 @@ maintenance = { status = "actively-developed" } backend = ["diesel", "diesel-derive-enum", "diesel_migrations", "futures", "actix-web", "jsonwebtoken", "deadpool-redis"] [dependencies] -thoth-errors = { version = "=0.13.5", path = "../thoth-errors" } +thoth-errors = { version = "=0.13.6", path = "../thoth-errors" } actix-web = { version = "4.9", optional = true } argon2rs = "0.2.5" isbn2 = "0.4.0" diff --git a/thoth-api/src/account/handler.rs b/thoth-api/src/account/handler.rs index 2a7966191..dfa36608c 100644 --- a/thoth-api/src/account/handler.rs +++ b/thoth-api/src/account/handler.rs @@ -7,33 +7,52 @@ use std::time::SystemTime; use std::time::UNIX_EPOCH; use uuid::Uuid; -use crate::account::model::Account; -use crate::account::model::AccountAccess; -use crate::account::model::AccountData; -use crate::account::model::DecodedToken; -use crate::account::model::LinkedPublisher; -use crate::account::model::NewAccount; -use crate::account::model::NewPassword; -use crate::account::model::PublisherAccount; -use crate::account::model::Token; -use crate::account::service::get_account; -use crate::account::util::make_hash; -use crate::account::util::make_salt; +use crate::account::{ + model::{ + Account, AccountAccess, AccountData, DecodedToken, LinkedPublisher, NewAccount, + NewPassword, NewPublisherAccount, PublisherAccount, Token, + }, + service::get_account, + util::{make_hash, make_salt}, +}; use crate::db::PgPool; use thoth_errors::{ThothError, ThothResult}; impl Account { pub fn get_permissions(&self, pool: &PgPool) -> ThothResult> { + let publisher_accounts = self.get_publisher_accounts(pool)?; + let permissions: Vec = + publisher_accounts.into_iter().map(|p| p.into()).collect(); + Ok(permissions) + } + + pub fn get_publisher_accounts(&self, pool: &PgPool) -> ThothResult> { use crate::schema::publisher_account::dsl::*; - let mut conn = pool.get().unwrap(); + let mut conn = pool.get()?; - let linked_publishers = publisher_account + let publisher_accounts = publisher_account .filter(account_id.eq(self.account_id)) .load::(&mut conn) .expect("Error loading publisher accounts"); - let permissions: Vec = - linked_publishers.into_iter().map(|p| p.into()).collect(); - Ok(permissions) + Ok(publisher_accounts) + } + + pub fn add_publisher_account( + &self, + pool: &PgPool, + linked_publisher: LinkedPublisher, + ) -> ThothResult { + use crate::schema::publisher_account::dsl::*; + let mut conn = pool.get()?; + let new_publisher_account = NewPublisherAccount { + account_id: self.account_id, + publisher_id: linked_publisher.publisher_id, + is_admin: linked_publisher.is_admin, + }; + diesel::insert_into(publisher_account) + .values(&new_publisher_account) + .get_result::(&mut conn) + .map_err(Into::into) } pub fn get_account_access(&self, linked_publishers: Vec) -> AccountAccess { @@ -46,7 +65,7 @@ impl Account { pub fn issue_token(&self, pool: &PgPool) -> ThothResult { const DEFAULT_TOKEN_VALIDITY: i64 = 24 * 60 * 60; - let mut connection = pool.get().unwrap(); + let mut connection = pool.get()?; dotenv().ok(); let linked_publishers: Vec = self.get_permissions(pool).unwrap_or_default(); @@ -72,7 +91,7 @@ impl Account { use crate::schema::account::dsl; let updated_account = diesel::update(dsl::account.find(self.account_id)) - .set(dsl::token.eq(token.unwrap())) + .set(dsl::token.eq(token?)) .get_result::(&mut connection) .expect("Unable to set token"); Ok(updated_account.token.unwrap()) @@ -176,3 +195,22 @@ impl NewPassword { Self { email, hash, salt } } } + +impl PublisherAccount { + pub fn delete(&self, pool: &PgPool) -> ThothResult<()> { + use crate::schema::publisher_account::dsl::*; + + pool.get()?.transaction(|connection| { + diesel::delete( + publisher_account.filter( + account_id + .eq(self.account_id) + .and(publisher_id.eq(self.publisher_id)), + ), + ) + .execute(connection) + .map(|_| ()) + .map_err(Into::into) + }) + } +} diff --git a/thoth-api/src/account/service.rs b/thoth-api/src/account/service.rs index 71d31a491..45943391c 100644 --- a/thoth-api/src/account/service.rs +++ b/thoth-api/src/account/service.rs @@ -1,14 +1,9 @@ use diesel::prelude::*; -use crate::account::model::Account; -use crate::account::model::AccountData; -use crate::account::model::AccountDetails; -use crate::account::model::LinkedPublisher; -use crate::account::model::NewAccount; -use crate::account::model::NewPassword; -use crate::account::model::NewPublisherAccount; -use crate::account::model::PublisherAccount; -use crate::account::util::verify; +use crate::account::{ + model::{Account, AccountData, AccountDetails, LinkedPublisher, NewAccount, NewPassword}, + util::verify, +}; use crate::db::PgPool; use crate::model::publisher::Publisher; use thoth_errors::{ThothError, ThothResult}; @@ -16,7 +11,7 @@ use thoth_errors::{ThothError, ThothResult}; pub fn login(user_email: &str, user_password: &str, pool: &PgPool) -> ThothResult { use crate::schema::account::dsl; - let mut conn = pool.get().unwrap(); + let mut conn = pool.get()?; let account = dsl::account .filter(dsl::email.eq(user_email)) .first::(&mut conn) @@ -32,7 +27,7 @@ pub fn login(user_email: &str, user_password: &str, pool: &PgPool) -> ThothResul pub fn get_account(email: &str, pool: &PgPool) -> ThothResult { use crate::schema::account::dsl; - let mut conn = pool.get().unwrap(); + let mut conn = pool.get()?; let account = dsl::account .filter(dsl::email.eq(email)) .first::(&mut conn) @@ -43,7 +38,7 @@ pub fn get_account(email: &str, pool: &PgPool) -> ThothResult { pub fn get_account_details(email: &str, pool: &PgPool) -> ThothResult { use crate::schema::account::dsl; - let mut conn = pool.get().unwrap(); + let mut conn = pool.get()?; let account = dsl::account .filter(dsl::email.eq(email)) .first::(&mut conn) @@ -64,44 +59,46 @@ pub fn get_account_details(email: &str, pool: &PgPool) -> ThothResult, pool: &PgPool, + name: String, + surname: String, + email: String, + password: String, + is_superuser: bool, + is_bot: bool, ) -> ThothResult { - use crate::schema; + use crate::schema::account::dsl; - let mut connection = pool.get().unwrap(); - let account: NewAccount = account_data.into(); - let created_account: Account = diesel::insert_into(schema::account::dsl::account) + let mut connection = pool.get()?; + let account: NewAccount = AccountData { + name, + surname, + email, + password, + is_superuser, + is_bot, + } + .into(); + let created_account: Account = diesel::insert_into(dsl::account) .values(&account) .get_result::(&mut connection)?; - for linked_publisher in linked_publishers { - let publisher_account = NewPublisherAccount { - account_id: created_account.account_id, - publisher_id: linked_publisher.publisher_id, - is_admin: linked_publisher.is_admin, - }; - diesel::insert_into(schema::publisher_account::dsl::publisher_account) - .values(&publisher_account) - .get_result::(&mut connection)?; - } Ok(created_account) } -pub fn all_emails(pool: &PgPool) -> ThothResult> { - let mut connection = pool.get().unwrap(); +pub fn all_emails(pool: &PgPool) -> ThothResult> { + let mut connection = pool.get()?; use crate::schema::account::dsl; let emails = dsl::account - .select(dsl::email) + .select((dsl::email, dsl::is_superuser, dsl::is_bot, dsl::is_active)) .order(dsl::email.asc()) - .load::(&mut connection) + .load::<(String, bool, bool, bool)>(&mut connection) .map_err(|_| ThothError::InternalError("Unable to load records".into()))?; Ok(emails) } pub fn all_publishers(pool: &PgPool) -> ThothResult> { - let mut connection = pool.get().unwrap(); + let mut connection = pool.get()?; use crate::schema::publisher::dsl; let publishers = dsl::publisher @@ -112,7 +109,7 @@ pub fn all_publishers(pool: &PgPool) -> ThothResult> { } pub fn update_password(email: &str, password: &str, pool: &PgPool) -> ThothResult { - let mut connection = pool.get().unwrap(); + let mut connection = pool.get()?; let new_password = NewPassword::new(email.to_string(), password.to_string()); use crate::schema::account::dsl; diff --git a/thoth-app-server/Cargo.toml b/thoth-app-server/Cargo.toml index 010bd6063..b57173257 100644 --- a/thoth-app-server/Cargo.toml +++ b/thoth-app-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-app-server" -version = "0.13.5" +version = "0.13.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2021" license = "Apache-2.0" diff --git a/thoth-app/Cargo.toml b/thoth-app/Cargo.toml index 314018911..74f5898a8 100644 --- a/thoth-app/Cargo.toml +++ b/thoth-app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-app" -version = "0.13.5" +version = "0.13.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2021" license = "Apache-2.0" @@ -29,8 +29,8 @@ semver = "1.0.23" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" uuid = { version = "1.11.0", features = ["serde", "v4", "js"] } -thoth-api = { version = "=0.13.5", path = "../thoth-api" } -thoth-errors = { version = "=0.13.5", path = "../thoth-errors" } +thoth-api = { version = "=0.13.6", path = "../thoth-api" } +thoth-errors = { version = "=0.13.6", path = "../thoth-errors" } [build-dependencies] dotenv = "0.15.0" diff --git a/thoth-client/Cargo.toml b/thoth-client/Cargo.toml index a36709797..c406ff1d2 100644 --- a/thoth-client/Cargo.toml +++ b/thoth-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-client" -version = "0.13.5" +version = "0.13.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2021" license = "Apache-2.0" @@ -10,8 +10,8 @@ readme = "README.md" build = "build.rs" [dependencies] -thoth-api = {version = "=0.13.5", path = "../thoth-api" } -thoth-errors = {version = "=0.13.5", path = "../thoth-errors" } +thoth-api = {version = "=0.13.6", path = "../thoth-api" } +thoth-errors = {version = "=0.13.6", path = "../thoth-errors" } graphql_client = "0.14.0" chrono = { version = "0.4.38", features = ["serde"] } reqwest = { version = "0.12", features = ["json"] } @@ -22,4 +22,4 @@ serde_json = "1.0" uuid = { version = "1.11.0", features = ["serde"] } [build-dependencies] -thoth-api = { version = "=0.13.5", path = "../thoth-api", features = ["backend"] } +thoth-api = { version = "=0.13.6", path = "../thoth-api", features = ["backend"] } diff --git a/thoth-errors/Cargo.toml b/thoth-errors/Cargo.toml index 6cee52f9b..7b9ab564c 100644 --- a/thoth-errors/Cargo.toml +++ b/thoth-errors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-errors" -version = "0.13.5" +version = "0.13.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2021" license = "Apache-2.0" diff --git a/thoth-export-server/Cargo.toml b/thoth-export-server/Cargo.toml index 47de15ea5..d715457e2 100644 --- a/thoth-export-server/Cargo.toml +++ b/thoth-export-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-export-server" -version = "0.13.5" +version = "0.13.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2021" license = "Apache-2.0" @@ -10,9 +10,9 @@ readme = "README.md" build = "build.rs" [dependencies] -thoth-api = { version = "=0.13.5", path = "../thoth-api" } -thoth-errors = { version = "=0.13.5", path = "../thoth-errors" } -thoth-client = { version = "=0.13.5", path = "../thoth-client" } +thoth-api = { version = "=0.13.6", path = "../thoth-api" } +thoth-errors = { version = "=0.13.6", path = "../thoth-errors" } +thoth-client = { version = "=0.13.6", path = "../thoth-client" } actix-web = "4.9" actix-cors = "0.7.0" cc_license = "0.1.0"