-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #667 from thoth-pub/feature/publihser_account_cli_…
…helpers Feature/publihser account cli helpers
- Loading branch information
Showing
12 changed files
with
652 additions
and
485 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String> { | ||
let all_emails = all_emails(pool).expect("No user accounts present in database."); | ||
let email_labels: Vec<String> = 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<String> { | ||
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<bool> { | ||
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<usize> = 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(()) | ||
} |
Oops, something went wrong.