diff --git a/.idea/trifid.iml b/.idea/trifid.iml
index 2b729a4..b4614b5 100644
--- a/.idea/trifid.iml
+++ b/.idea/trifid.iml
@@ -8,6 +8,7 @@
+
diff --git a/Cargo.lock b/Cargo.lock
index 6b489f0..c01a55a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -781,6 +781,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "checked_int_cast"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919"
+
[[package]]
name = "chrono"
version = "0.4.24"
@@ -993,6 +999,31 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "crossterm"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "libc",
+ "mio",
+ "parking_lot 0.12.1",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -2347,6 +2378,25 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "qr2term"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c2a1e77b5cd714b04247ad912b7c8fe9a1fe1d58425048249def91bcf690e4c"
+dependencies = [
+ "crossterm",
+ "qrcode",
+]
+
+[[package]]
+name = "qrcode"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f"
+dependencies = [
+ "checked_int_cast",
+]
+
[[package]]
name = "quick-protobuf"
version = "0.8.1"
@@ -2972,6 +3022,27 @@ dependencies = [
"lazy_static",
]
+[[package]]
+name = "signal-hook"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
@@ -3242,6 +3313,20 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
+[[package]]
+name = "tfcli"
+version = "0.1.0"
+dependencies = [
+ "clap 4.2.7",
+ "dirs 5.0.1",
+ "ipnet",
+ "qr2term",
+ "reqwest",
+ "serde",
+ "tokio",
+ "url",
+]
+
[[package]]
name = "tfclient"
version = "0.1.8"
@@ -3371,9 +3456,21 @@ dependencies = [
"pin-project-lite",
"signal-hook-registry",
"socket2",
+ "tokio-macros",
"windows-sys 0.48.0",
]
+[[package]]
+name = "tokio-macros"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.16",
+]
+
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
diff --git a/Cargo.toml b/Cargo.toml
index 9b3de10..17eb7c0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,5 +5,6 @@ members = [
"trifid-api/trifid_api_entities",
"tfclient",
"trifid-pki",
- "dnapi-rs"
+ "dnapi-rs",
+ "tfcli"
]
\ No newline at end of file
diff --git a/tfcli/Cargo.toml b/tfcli/Cargo.toml
new file mode 100644
index 0000000..a6ef972
--- /dev/null
+++ b/tfcli/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "tfcli"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+clap = { version = "4", features = ["derive", "env"] }
+url = "2.3.1"
+reqwest = { version = "0.11.17", features = ["json"] }
+serde = { version = "1", features = ["derive"] }
+tokio = { version = "1", features = ["full"] }
+dirs = "5.0.1"
+qr2term = "0.3.1"
+ipnet = "2.7.2"
\ No newline at end of file
diff --git a/tfcli/src/account.rs b/tfcli/src/account.rs
new file mode 100644
index 0000000..7a7724f
--- /dev/null
+++ b/tfcli/src/account.rs
@@ -0,0 +1,239 @@
+use std::error::Error;
+use std::fs;
+use serde::{Deserialize, Serialize};
+use url::Url;
+use crate::AccountCommands;
+use crate::api::APIErrorResponse;
+
+pub async fn account_main(command: AccountCommands, server: Url) -> Result<(), Box> {
+ match command {
+ AccountCommands::Create { email } => create_account(email, server).await,
+ AccountCommands::MagicLink { magic_link_token } => auth_magic_link(magic_link_token, server).await,
+ AccountCommands::MfaSetup {} => create_mfa_authenticator(server).await,
+ AccountCommands::MfaSetupFinish {code, token} => finish_mfa_authenticator(token, code, server).await,
+ AccountCommands::Mfa {code} => mfa_auth(code, server).await
+ }
+}
+
+#[derive(Serialize)]
+pub struct CreateAccountBody {
+ pub email: String
+}
+
+pub async fn create_account(email: String, server: Url) -> Result<(), Box> {
+ let client = reqwest::Client::new();
+ let res = client.post(server.join("/v1/signup")?).json(&CreateAccountBody { email }).send().await?;
+
+ if res.status().is_success() {
+ println!("Account created successfully, check your email.");
+ println!("Finish creating your account with 'tfcli account magic-link --magic-link-token [magic-link-token]'.");
+ } else {
+
+ let resp: APIErrorResponse = res.json().await?;
+
+ eprintln!("[error] Error creating account: {} {}", resp.errors[0].code, resp.errors[0].message);
+
+ std::process::exit(1);
+ }
+
+ Ok(())
+}
+
+#[derive(Serialize)]
+pub struct MagicLinkBody {
+ #[serde(rename = "magicLinkToken")]
+ pub magic_link_token: String
+}
+
+#[derive(Deserialize)]
+pub struct MagicLinkSuccess {
+ pub data: MagicLinkSuccessBody
+}
+#[derive(Deserialize)]
+pub struct MagicLinkSuccessBody {
+ #[serde(rename = "sessionToken")]
+ pub session_token: String
+}
+
+pub async fn auth_magic_link(magic_token: String, server: Url) -> Result<(), Box> {
+ let client = reqwest::Client::new();
+ let res = client.post(server.join("/v1/auth/verify-magic-link")?).json(&MagicLinkBody { magic_link_token: magic_token }).send().await?;
+
+ if res.status().is_success() {
+ let resp: MagicLinkSuccess = res.json().await?;
+
+ let token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
+
+ fs::write(&token_store, resp.data.session_token)?;
+
+ println!("Session token saved to {}", token_store.display());
+ println!("You will likely need to authorize with 2fa, run 'tfcli account mfa --code=[code]' to finish logging in, or 'tfcli account mfa-setup' to setup 2FA.");
+ } else {
+ let resp: APIErrorResponse = res.json().await?;
+
+ eprintln!("[error] Error getting session token: {} {}", resp.errors[0].code, resp.errors[0].message);
+
+ std::process::exit(1);
+ }
+
+ Ok(())
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct WhoamiResponse {
+ pub data: WhoamiResponseData,
+ pub metadata: WhoamiResponseMetadata,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct WhoamiResponseData {
+ #[serde(rename = "actorType")]
+ pub actor_type: String,
+ pub actor: WhoamiResponseDataActor,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct WhoamiResponseDataActor {
+ pub id: String,
+ #[serde(rename = "organizationID")]
+ pub organization_id: Option,
+ pub email: String,
+ #[serde(rename = "createdAt")]
+ pub created_at: String,
+ #[serde(rename = "hasTOTPAuthenticator")]
+ pub has_totp_authenticator: bool,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct WhoamiResponseMetadata {}
+
+#[derive(Deserialize)]
+pub struct CreateMfaResponse {
+ pub data: CreateMfaResponseData
+}
+#[derive(Deserialize)]
+pub struct CreateMfaResponseData {
+ #[serde(rename = "totpToken")]
+ pub totp_token: String,
+ pub secret: String,
+ pub url: String
+}
+
+pub async fn create_mfa_authenticator(server: Url) -> Result<(), Box> {
+ let client = reqwest::Client::new();
+
+ // load session token
+ let token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
+ let session_token = fs::read_to_string(&token_store)?;
+
+ // do we have mfa already?
+ let whoami: WhoamiResponse = client.get(server.join("/v2/whoami")?).bearer_auth(&session_token).send().await?.json().await?;
+
+ if whoami.data.actor.has_totp_authenticator {
+ eprintln!("[error] user already has a totp authenticator, cannot add another one");
+ std::process::exit(1);
+ }
+
+ let res = client.post(server.join("/v1/totp-authenticators")?).bearer_auth(&session_token).body("{}").send().await?;
+
+ if res.status().is_success() {
+ let resp: CreateMfaResponse = res.json().await?;
+
+ println!("--- TOTP SETUP INFORMATION ---");
+ println!("To complete setup, you'll need a TOTP-compatible app, such as Google Authenticator or Authy.");
+ println!("Scan the following code with your authenticator app:");
+ qr2term::print_qr(resp.data.url)?;
+ println!("Alternatively, enter the following secret into your authenticator app: '{}'", resp.data.secret);
+ println!("Once done, enable TOTP by running the following command with the code shown on your authenticator app:");
+ println!("tfcli account mfa-setup-finish --token {} --code [CODE IN AUTHENTICATOR]", resp.data.totp_token);
+ println!("This code will expire in 10 minutes.");
+ } else {
+ let resp: APIErrorResponse = res.json().await?;
+
+ eprintln!("[error] Error adding MFA to account: {} {}", resp.errors[0].code, resp.errors[0].message);
+
+ std::process::exit(1);
+ }
+
+ Ok(())
+}
+
+#[derive(Serialize)]
+pub struct MfaVerifyBody {
+ #[serde(rename = "totpToken")]
+ pub totp_token: String,
+ pub code: String
+}
+
+#[derive(Deserialize)]
+pub struct MFASuccess {
+ pub data: MFASuccessBody
+}
+#[derive(Deserialize)]
+pub struct MFASuccessBody {
+ #[serde(rename = "authToken")]
+ pub auth_token: String
+}
+
+pub async fn finish_mfa_authenticator(token: String, code: String, server: Url) -> Result<(), Box> {
+ let client = reqwest::Client::new();
+
+ // load session token
+ let token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
+ let session_token = fs::read_to_string(&token_store)?;
+
+ let res = client.post(server.join("/v1/verify-totp-authenticators")?).json(&MfaVerifyBody {totp_token: token, code }).bearer_auth(session_token).send().await?;
+
+ if res.status().is_success() {
+ let resp: MFASuccess = res.json().await?;
+
+ let token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
+
+ fs::write(&token_store, resp.data.auth_token)?;
+
+ println!("Auth token saved to {}", token_store.display());
+ println!("You are now fully logged-in. This 2fa token will expire in about 10 minutes, at which point you will need to authenticate again with 'tfcli account mfa'");
+ } else {
+ let resp: APIErrorResponse = res.json().await?;
+
+ eprintln!("[error] Error verifying MFA code: {} {}", resp.errors[0].code, resp.errors[0].message);
+
+ std::process::exit(1);
+ }
+
+ Ok(())
+}
+
+#[derive(Serialize)]
+pub struct MfaAuthBody {
+ pub code: String
+}
+
+pub async fn mfa_auth(code: String, server: Url) -> Result<(), Box> {
+ let client = reqwest::Client::new();
+
+ // load session token
+ let token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
+ let session_token = fs::read_to_string(&token_store)?;
+
+ let res = client.post(server.join("/v1/auth/totp")?).json(&MfaAuthBody { code }).bearer_auth(session_token).send().await?;
+
+ if res.status().is_success() {
+ let resp: MFASuccess = res.json().await?;
+
+ let token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
+
+ fs::write(&token_store, resp.data.auth_token)?;
+
+ println!("Auth token saved to {}", token_store.display());
+ println!("You are now fully logged-in. This 2fa token will expire in about 10 minutes, at which point you will need to authenticate again with 'tfcli account mfa'");
+ } else {
+ let resp: APIErrorResponse = res.json().await?;
+
+ eprintln!("[error] Error verifying MFA code: {} {}", resp.errors[0].code, resp.errors[0].message);
+
+ std::process::exit(1);
+ }
+
+ Ok(())
+}
diff --git a/tfcli/src/api.rs b/tfcli/src/api.rs
new file mode 100644
index 0000000..5f9f913
--- /dev/null
+++ b/tfcli/src/api.rs
@@ -0,0 +1,12 @@
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+pub struct APIErrorResponse {
+ pub errors: Vec
+}
+#[derive(Deserialize)]
+pub struct APIError {
+ pub code: String,
+ pub message: String,
+ pub path: Option
+}
\ No newline at end of file
diff --git a/tfcli/src/main.rs b/tfcli/src/main.rs
new file mode 100644
index 0000000..c1d2c86
--- /dev/null
+++ b/tfcli/src/main.rs
@@ -0,0 +1,137 @@
+use std::error::Error;
+use std::fs;
+use clap::{Parser, Subcommand};
+use ipnet::Ipv4Net;
+use url::Url;
+use crate::account::account_main;
+use crate::network::network_main;
+use crate::org::org_main;
+
+pub mod account;
+pub mod api;
+pub mod network;
+pub mod org;
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+#[command(propagate_version = true)]
+pub struct Args {
+ #[command(subcommand)]
+ command: Commands,
+ #[clap(short, long, env = "TFCLI_SERVER")]
+ /// The base URL of your trifid-api instance. Defaults to the value in $XDG_CONFIG_HOME/tfcli-server-url.conf or the TFCLI_SERVER environment variable.
+ server: Option
+}
+
+#[derive(Subcommand, Debug)]
+pub enum Commands {
+ /// Manage your trifid account
+ Account {
+ #[command(subcommand)]
+ command: AccountCommands
+ },
+ /// Manage the networks associated with your trifid account
+ Network {
+ #[command(subcommand)]
+ command: NetworkCommands
+ },
+ /// Manage the organization associated with your trifid account
+ Org {
+ #[command(subcommand)]
+ command: OrgCommands
+ }
+}
+
+#[derive(Subcommand, Debug)]
+pub enum AccountCommands {
+ /// Create a new trifid account on the designated server
+ Create {
+ #[clap(short, long)]
+ email: String
+ },
+ /// Log in to your account with a magic-link token acquired via email or the trifid-api logs.
+ MagicLink {
+ #[clap(short, long)]
+ magic_link_token: String
+ },
+ /// Create a new TOTP authenticator on this account to enable authorizing with 2fa and performing all management tasks.
+ MfaSetup {},
+ /// Finish creating a new TOTP authenticator by inputting the code shown on your authenticator app.
+ MfaSetupFinish {
+ #[clap(short, long)]
+ code: String,
+ #[clap(short, long)]
+ token: String
+ },
+ /// Create a new short-lived authentication token by inputting the code shown on your authenticator app.
+ Mfa {
+ #[clap(short, long)]
+ code: String
+ }
+}
+
+#[derive(Subcommand, Debug)]
+pub enum NetworkCommands {
+ /// List all networks associated with your trifid account.
+ List {},
+ /// Lookup a specific network by ID.
+ Lookup {
+ #[clap(short, long)]
+ id: String
+ }
+}
+
+#[derive(Subcommand, Debug)]
+pub enum OrgCommands {
+ /// Create an organization on your trifid-api server. NOTE: This command ONLY works on trifid-api servers. It will NOT work on original DN servers.
+ Create {
+ #[clap(short, long)]
+ cidr: Ipv4Net
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ match main2().await {
+ Ok(_) => (),
+ Err(e) => {
+ eprintln!("There was an error during execution: {}", e);
+ std::process::exit(1);
+ }
+ }
+}
+
+async fn main2() -> Result<(), Box> {
+ let args = Args::parse();
+
+ // find server
+ let server;
+
+ if let Some(srv) = args.server {
+ server = srv; // Environment variable or CLI takes precedence over config file
+ } else {
+ let srv_url_file = dirs::config_dir().unwrap().join("tfcli-server-url.conf");
+ if !srv_url_file.exists() {
+ eprintln!("[error] no server URL available: '-s/--server' not provided, TFCLI_SERVER not set, and {} does not exist", srv_url_file.display());
+ eprintln!("[error] please set a server url via any of these mechanisms and try again");
+ std::process::exit(1);
+ }
+ let url_s = fs::read_to_string(&srv_url_file)?;
+ let url = match Url::parse(&url_s) {
+ Ok(u) => u,
+ Err(e) => {
+ eprintln!("[error] unable to parse the URL in {}", srv_url_file.display());
+ eprintln!("[error] urlparse returned error '{}'", e);
+ eprintln!("[error] please correct the error and try again");
+ std::process::exit(1);
+ }
+ };
+ server = url;
+ }
+
+ match args.command {
+ Commands::Account { command } => account_main(command, server).await,
+ Commands::Network { command } => network_main(command, server).await,
+ Commands::Org { command } => org_main(command, server).await
+ }
+}
\ No newline at end of file
diff --git a/tfcli/src/network.rs b/tfcli/src/network.rs
new file mode 100644
index 0000000..6a59574
--- /dev/null
+++ b/tfcli/src/network.rs
@@ -0,0 +1,114 @@
+use std::error::Error;
+use std::fs;
+use serde::Deserialize;
+use url::Url;
+use crate::api::APIErrorResponse;
+use crate::NetworkCommands;
+
+pub async fn network_main(command: NetworkCommands, server: Url) -> Result<(), Box> {
+ match command {
+ NetworkCommands::List {} => list_networks(server).await,
+ NetworkCommands::Lookup {id} => get_network(id, server).await
+ }
+}
+
+#[derive(Deserialize)]
+pub struct NetworkListResp {
+ pub data: Vec
+}
+
+#[derive(Deserialize)]
+pub struct Network {
+ pub id: String,
+ pub cidr: String,
+ #[serde(rename = "organizationID")]
+ pub organization_id: String,
+ #[serde(rename = "signingCAID")]
+ pub signing_ca_id: String,
+ #[serde(rename = "createdAt")]
+ pub created_at: String,
+ #[serde(rename = "lighthousesAsRelays")]
+ pub lighthouses_as_relays: bool,
+ pub name: String
+}
+
+pub async fn list_networks(server: Url) -> Result<(), Box> {
+ let client = reqwest::Client::new();
+
+ // load session token
+ let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
+ let session_token = fs::read_to_string(&sess_token_store)?;
+ let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
+ let auth_token = fs::read_to_string(&auth_token_store)?;
+
+ let token = format!("{} {}", session_token, auth_token);
+
+ let res = client.get(server.join("/v1/networks")?).bearer_auth(token).send().await?;
+
+ if res.status().is_success() {
+ let resp: NetworkListResp = res.json().await?;
+
+ for network in &resp.data {
+ println!(" Network: {}", network.id);
+ println!(" CIDR: {}", network.cidr);
+ println!(" Organization: {}", network.organization_id);
+ println!(" Signing CA: {}", network.signing_ca_id);
+ println!("Dedicated Relays: {}", !network.lighthouses_as_relays);
+ println!(" Name: {}", network.name);
+ println!(" Created At: {}", network.created_at);
+ println!();
+ }
+
+ if resp.data.is_empty() {
+ println!("No networks found");
+ }
+ } else {
+ let resp: APIErrorResponse = res.json().await?;
+
+ eprintln!("[error] Error listing networks: {} {}", resp.errors[0].code, resp.errors[0].message);
+
+ std::process::exit(1);
+ }
+
+ Ok(())
+}
+
+#[derive(Deserialize)]
+pub struct NetworkGetResponse {
+ pub data: Network
+}
+
+pub async fn get_network(id: String, server: Url) -> Result<(), Box> {
+ let client = reqwest::Client::new();
+
+ // load session token
+ let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
+ let session_token = fs::read_to_string(&sess_token_store)?;
+ let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
+ let auth_token = fs::read_to_string(&auth_token_store)?;
+
+ let token = format!("{} {}", session_token, auth_token);
+
+ let res = client.get(server.join(&format!("/v1/networks/{}", id))?).bearer_auth(token).send().await?;
+
+ if res.status().is_success() {
+ let network: Network = res.json::().await?.data;
+
+ println!(" Network: {}", network.id);
+ println!(" CIDR: {}", network.cidr);
+ println!(" Organization: {}", network.organization_id);
+ println!(" Signing CA: {}", network.signing_ca_id);
+ println!("Dedicated Relays: {}", !network.lighthouses_as_relays);
+ println!(" Name: {}", network.name);
+ println!(" Created At: {}", network.created_at);
+
+ } else {
+ let resp: APIErrorResponse = res.json().await?;
+
+ eprintln!("[error] Error listing networks: {} {}", resp.errors[0].code, resp.errors[0].message);
+
+ std::process::exit(1);
+ }
+
+ Ok(())
+}
diff --git a/tfcli/src/org.rs b/tfcli/src/org.rs
new file mode 100644
index 0000000..6b30dc0
--- /dev/null
+++ b/tfcli/src/org.rs
@@ -0,0 +1,54 @@
+use std::error::Error;
+use std::fs;
+use ipnet::Ipv4Net;
+use serde::{Deserialize, Serialize};
+use url::Url;
+use crate::OrgCommands;
+use crate::api::APIErrorResponse;
+
+pub async fn org_main(command: OrgCommands, server: Url) -> Result<(), Box> {
+ match command {
+ OrgCommands::Create { cidr } => create_org(cidr, server).await,
+ }
+}
+
+#[derive(Serialize)]
+pub struct CreateOrgBody {
+ pub cidr: String
+}
+
+#[derive(Deserialize)]
+pub struct OrgCreateResponse {
+ pub organization: String,
+ pub ca: String,
+ pub network: String
+}
+
+pub async fn create_org(cidr: Ipv4Net, server: Url) -> Result<(), Box> {
+ let client = reqwest::Client::new();
+
+ let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
+ let session_token = fs::read_to_string(&sess_token_store)?;
+ let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
+ let auth_token = fs::read_to_string(&auth_token_store)?;
+
+ let token = format!("{} {}", session_token, auth_token);
+
+ let res = client.post(server.join("/v1/organization")?).json(&CreateOrgBody { cidr: cidr.to_string() }).bearer_auth(token).send().await?;
+
+ if res.status().is_success() {
+ let resp: OrgCreateResponse = res.json().await?;
+ println!("Created organization successfully.");
+ println!("Organization: {}", resp.organization);
+ println!(" Signing CA: {}", resp.ca);
+ println!(" Network: {}", resp.network);
+ } else {
+ let resp: APIErrorResponse = res.json().await?;
+
+ eprintln!("[error] Error creating org: {} {}", resp.errors[0].code, resp.errors[0].message);
+
+ std::process::exit(1);
+ }
+
+ Ok(())
+}