Skip to content

Commit

Permalink
feat: webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
pxseu committed Oct 8, 2023
1 parent a0c067e commit f650c7e
Show file tree
Hide file tree
Showing 16 changed files with 799 additions and 396 deletions.
764 changes: 384 additions & 380 deletions Cargo.lock

Large diffs are not rendered by default.

17 changes: 13 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ futures-util = "0.3"
clap_complete = "4.1"
clap = { version = "4.1", features = ["derive"] }
fern = { version = "0.6", features = ["colored"] }
tokio = { version = "1.20", features = ["full"] }
# version > 1.29 makes us have 2x deps
tokio = { version = "=1.29", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
hyper = { version = "0.14", features = ["server"] }
ctrlc = { version = "3.2", features = ["termination"] }
Expand All @@ -65,6 +66,10 @@ async-compression = { version = "0.4", features = ["tokio", "gzip"] }

# *nix only deps
[target.'cfg(all(not(windows), not(macos)))'.dependencies]
hop = { version = "0.1", features = [
"chrono",
"rustls-tls-webpki-roots",
], default-features = false }
leap_client_rs = { version = "0.1", features = [
"zlib",
"rustls-tls-webpki-roots",
Expand All @@ -76,15 +81,19 @@ reqwest = { version = "0.11", features = [
], default-features = false }
tokio-rustls = { version = "0.24", default-features = false }
webpki = "0.22"
webpki-roots = "0.23"
async-tungstenite = { version = "0.22", features = [
webpki-roots = "0.25"
async-tungstenite = { version = "0.23", features = [
"tokio-runtime",
"tokio-rustls-webpki-roots",
] }


# windows only deps
[target.'cfg(any(windows, macos))'.dependencies]
hop = { version = "0.1", features = [
"chrono",
"native-tls",
], default-features = false }
reqwest = { version = "0.11", features = [
"json",
"multipart",
Expand All @@ -96,7 +105,7 @@ leap_client_rs = { version = "0.1", features = [
], default-features = false }
native-tls = "0.2"
tokio-native-tls = "0.3"
async-tungstenite = { version = "0.22", features = [
async-tungstenite = { version = "0.23", features = [
"tokio-runtime",
"tokio-native-tls",
] }
Expand Down
2 changes: 1 addition & 1 deletion src/commands/auth/login/browser_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub async fn browser_login() -> Result<String> {

let url = format!(
"{WEB_AUTH_URL}?{}",
vec!["callback", &format!("http://localhost:{port}/")].join("=")
["callback", &format!("http://localhost:{port}/")].join("=")
);

// lunch a web server to handle the auth request
Expand Down
4 changes: 4 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod secrets;
mod tunnel;
pub mod update;
mod volumes;
mod webhooks;
mod whoami;

use anyhow::Result;
Expand Down Expand Up @@ -59,6 +60,8 @@ pub enum Commands {
Tunnel(tunnel::Options),
#[clap(alias = "volume", alias = "v")]
Volumes(volumes::Options),
#[clap(alias = "webhook", alias = "wh")]
Webhooks(webhooks::Options),
#[clap(alias = "compose")]
FromCompose(from_compose::Options),
Backup(backup::Options),
Expand Down Expand Up @@ -102,6 +105,7 @@ pub async fn handle_command(command: Commands, mut state: State) -> Result<()> {
Commands::Payment(options) => payment::handle(options, state).await,
Commands::Volumes(options) => volumes::handle(options, state).await,
Commands::Backup(options) => backup::handle(options, state).await,
Commands::Webhooks(options) => webhooks::handle(options, state).await,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/projects/create/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub async fn get_payment_method_from_user(http: &HttpClient) -> Result<String> {

let url = format!(
"{WEB_PAYMENTS_URL}?{}",
vec!["callback", &format!("http://localhost:{port}/payment")].join("=")
["callback", &format!("http://localhost:{port}/payment")].join("=")
);

if let Err(why) = webbrowser::open(&url) {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/update/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub(self) mod checker;
pub mod checker;
#[cfg(feature = "update")]
mod command;
mod parse;
Expand Down
84 changes: 84 additions & 0 deletions src/commands/webhooks/create.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use anyhow::{Context, Result};
use clap::Parser;
use hop::webhooks::types::{PossibleEvents, EVENT_CATEGORIES, EVENT_NAMES};

use super::utils::string_to_event;
use crate::state::State;
use crate::utils::urlify;

#[derive(Debug, Parser)]
#[clap(about = "Create a new webhook")]
#[group(skip)]
pub struct Options {
#[clap(short, long, help = "The url to send the webhook to")]
pub url: Option<String>,
#[clap(short, long, help = "The events to send the webhook on", value_parser = string_to_event )]
pub events: Vec<PossibleEvents>,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let project = state.ctx.current_project_error()?;

let url = if let Some(url) = options.url {
url
} else {
dialoguer::Input::new()
.with_prompt("Webhook URL")
.interact_text()?
};

let events = if !options.events.is_empty() {
options.events
} else {
let mut events = vec![];
let mut start_idx = 0usize;

for (name, end_idx) in EVENT_CATEGORIES {
let end_idx = end_idx as usize + start_idx;

for (_, event) in &EVENT_NAMES[start_idx..end_idx] {
events.push(format!("{name}: {event}"))
}

start_idx = end_idx;
}

let dialoguer_events = loop {
let test = dialoguer::MultiSelect::new()
.with_prompt("Select events")
.items(&events)
.interact()?;

if !test.is_empty() {
break test;
}
};

EVENT_NAMES
.into_iter()
.enumerate()
.filter(|(idx, _)| dialoguer_events.contains(idx))
.map(|(_, (event, _))| event)
.collect()
};

let webhook = state
.hop
.webhooks
.create(&project.id, &url, &events)
.await?;

log::info!("Webhook successfully created. ID: {}\n", webhook.id);
log::info!("This is your webhook's secret, this is how you will authenticate traffic coming to your endpoint");
log::info!("Webhook Header: {}", urlify("X-Hop-Hooks-Signature"));
log::info!(
"Webhook Secret: {}",
urlify(
&webhook
.secret
.context("Webhook secret was expected to be present")?
)
);

Ok(())
}
39 changes: 39 additions & 0 deletions src/commands/webhooks/delete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use anyhow::{Context, Result};
use clap::Parser;

use crate::commands::webhooks::utils::format_webhooks;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Delete a webhook")]
#[group(skip)]
pub struct Options {
#[clap(short, long, help = "The id of the webhook")]
pub id: Option<String>,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let project = state.ctx.current_project_error()?;

let all = state.hop.webhooks.get_all(&project.id).await?;

let webhook = if let Some(id) = options.id {
all.into_iter()
.find(|webhook| webhook.id == id)
.context("Webhook not found")?
} else {
let formatted_webhooks = format_webhooks(&all, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a webhook to update")
.items(&formatted_webhooks)
.default(0)
.interact()?;

all[idx].clone()
};

state.hop.webhooks.delete(&project.id, &webhook.id).await?;

Ok(())
}
35 changes: 35 additions & 0 deletions src/commands/webhooks/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use anyhow::Result;
use clap::Parser;

use super::utils::format_webhooks;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "List webhooks")]
#[group(skip)]
pub struct Options {
#[clap(short, long, help = "Only print the IDs")]
pub quiet: bool,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let project = state.ctx.current_project_error()?;

let webhooks = state.hop.webhooks.get_all(&project.id).await?;

if options.quiet {
let ids = webhooks
.iter()
.map(|d| d.id.as_str())
.collect::<Vec<_>>()
.join(" ");

println!("{ids}");
} else {
let webhooks_fmt = format_webhooks(&webhooks, true);

println!("{}", webhooks_fmt.join("\n"));
}

Ok(())
}
43 changes: 43 additions & 0 deletions src/commands/webhooks/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
mod create;
mod delete;
mod list;
mod regenerate;
mod update;
mod utils;

use anyhow::Result;
use clap::{Parser, Subcommand};

use crate::state::State;

#[derive(Debug, Subcommand)]
pub enum Commands {
#[clap(name = "ls", alias = "list")]
List(list::Options),
#[clap(name = "new", alias = "create", alias = "add")]
Create(create::Options),
#[clap(name = "update", alias = "edit")]
Update(update::Options),
#[clap(name = "rm", alias = "delete", alias = "del")]
Delete(delete::Options),
#[clap(name = "regenerate", alias = "regen")]
Regenerate(regenerate::Options),
}

#[derive(Debug, Parser)]
#[clap(about = "Manage webhooks")]
#[group(skip)]
pub struct Options {
#[clap(subcommand)]
pub commands: Commands,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
match options.commands {
Commands::List(options) => list::handle(options, state).await,
Commands::Create(options) => create::handle(options, state).await,
Commands::Update(options) => update::handle(options, state).await,
Commands::Delete(options) => delete::handle(options, state).await,
Commands::Regenerate(options) => regenerate::handle(options, state).await,
}
}
48 changes: 48 additions & 0 deletions src/commands/webhooks/regenerate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use anyhow::{Context, Result};
use clap::Parser;

use crate::commands::webhooks::utils::format_webhooks;
use crate::state::State;
use crate::utils::urlify;

#[derive(Debug, Parser)]
#[clap(about = "Regenerate a webhook secret")]
#[group(skip)]
pub struct Options {
#[clap(short, long, help = "The id of the webhook")]
pub id: Option<String>,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let project = state.ctx.current_project_error()?;

let all = state.hop.webhooks.get_all(&project.id).await?;

let webhook = if let Some(id) = options.id {
all.into_iter()
.find(|webhook| webhook.id == id)
.context("Webhook not found")?
} else {
let formatted_webhooks = format_webhooks(&all, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a webhook to update")
.items(&formatted_webhooks)
.default(0)
.interact()?;

all[idx].clone()
};

let token = state
.hop
.webhooks
.regenerate_secret(&project.id, &webhook.id)
.await?;

log::info!("This is your webhook's secret, this is how you will authenticate traffic coming to your endpoint");
log::info!("Webhook Header: {}", urlify("X-Hop-Hooks-Signature"));
log::info!("Webhook Secret: {}", urlify(&token));

Ok(())
}
Loading

0 comments on commit f650c7e

Please sign in to comment.