feat(cli): add token management commands
Co-authored-by: jnsgruk <668505+jnsgruk@users.noreply.github.com>
This commit is contained in:
parent
d96f2c27e0
commit
0b9cfefce5
7 changed files with 195 additions and 1 deletions
31
Cargo.lock
generated
31
Cargo.lock
generated
|
|
@ -341,6 +341,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rpassword",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
|
@ -1874,6 +1875,17 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rpassword"
|
||||||
|
version = "7.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rtoolbox",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.9"
|
version = "0.9.9"
|
||||||
|
|
@ -1894,6 +1906,16 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rtoolbox"
|
||||||
|
version = "0.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
|
|
@ -3053,6 +3075,15 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.60.2"
|
version = "0.60.2"
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ block-id = "0.2.1"
|
||||||
chrono = { version = "0.4", features = ["serde", "clock"] }
|
chrono = { version = "0.4", features = ["serde", "clock"] }
|
||||||
clap = { version = "4.5", features = ["derive", "env"] }
|
clap = { version = "4.5", features = ["derive", "env"] }
|
||||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||||
|
rpassword = "7.3"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
pub mod roasters;
|
pub mod roasters;
|
||||||
pub mod roasts;
|
pub mod roasts;
|
||||||
|
pub mod tokens;
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
use roasters::{AddRoasterCommand, DeleteRoasterCommand, GetRoasterCommand, UpdateRoasterCommand};
|
use roasters::{AddRoasterCommand, DeleteRoasterCommand, GetRoasterCommand, UpdateRoasterCommand};
|
||||||
use roasts::{AddRoastCommand, DeleteRoastCommand, GetRoastCommand, ListRoastsCommand};
|
use roasts::{AddRoastCommand, DeleteRoastCommand, GetRoastCommand, ListRoastsCommand};
|
||||||
|
use tokens::{CreateTokenCommand, RevokeTokenCommand};
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(author, version, about = "Track coffee roasts, brews, and cups", long_about = None)]
|
#[command(author, version, about = "Track coffee roasts, brews, and cups", long_about = None)]
|
||||||
|
|
@ -27,6 +29,14 @@ pub enum Commands {
|
||||||
#[command(name = "serve")]
|
#[command(name = "serve")]
|
||||||
Serve(ServeCommand),
|
Serve(ServeCommand),
|
||||||
|
|
||||||
|
// Tokens
|
||||||
|
#[command(name = "create-token")]
|
||||||
|
CreateToken(CreateTokenCommand),
|
||||||
|
#[command(name = "list-tokens")]
|
||||||
|
ListTokens,
|
||||||
|
#[command(name = "revoke-token")]
|
||||||
|
RevokeToken(RevokeTokenCommand),
|
||||||
|
|
||||||
// Roasters
|
// Roasters
|
||||||
#[command(name = "add-roaster")]
|
#[command(name = "add-roaster")]
|
||||||
AddRoaster(AddRoasterCommand),
|
AddRoaster(AddRoasterCommand),
|
||||||
|
|
|
||||||
60
src/cli/tokens.rs
Normal file
60
src/cli/tokens.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use clap::Args;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
use crate::cli::print_json;
|
||||||
|
use crate::client::BrewlogClient;
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct CreateTokenCommand {
|
||||||
|
/// A descriptive name for this token
|
||||||
|
#[arg(long)]
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct RevokeTokenCommand {
|
||||||
|
/// The ID of the token to revoke
|
||||||
|
#[arg(long)]
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_token(client: &BrewlogClient, cmd: CreateTokenCommand) -> Result<()> {
|
||||||
|
// Prompt for username
|
||||||
|
print!("Username: ");
|
||||||
|
io::stdout().flush()?;
|
||||||
|
let mut username = String::new();
|
||||||
|
io::stdin().read_line(&mut username)?;
|
||||||
|
let username = username.trim();
|
||||||
|
|
||||||
|
// Prompt for password (without echo)
|
||||||
|
let password = rpassword::prompt_password("Password: ")
|
||||||
|
.context("failed to read password")?;
|
||||||
|
|
||||||
|
// Create the token
|
||||||
|
let token_response = client
|
||||||
|
.tokens()
|
||||||
|
.create(&username, &password, &cmd.name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("\nToken created successfully!");
|
||||||
|
println!("Token ID: {}", token_response.id);
|
||||||
|
println!("Token Name: {}", token_response.name);
|
||||||
|
println!("\n⚠️ Save this token securely - it will not be shown again:");
|
||||||
|
println!("\n{}", token_response.token);
|
||||||
|
println!("\nExport it in your environment:");
|
||||||
|
println!(" export BREWLOG_TOKEN={}", token_response.token);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_tokens(client: &BrewlogClient) -> Result<()> {
|
||||||
|
let tokens = client.tokens().list().await?;
|
||||||
|
print_json(&tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn revoke_token(client: &BrewlogClient, cmd: RevokeTokenCommand) -> Result<()> {
|
||||||
|
let token = client.tokens().revoke(&cmd.id).await?;
|
||||||
|
println!("Token revoked successfully");
|
||||||
|
print_json(&token)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod roasters;
|
pub mod roasters;
|
||||||
pub mod roasts;
|
pub mod roasts;
|
||||||
|
pub mod tokens;
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use reqwest::{Client, Url};
|
use reqwest::{Client, Url};
|
||||||
|
|
@ -9,6 +10,7 @@ use crate::server::errors::ErrorResponse;
|
||||||
pub struct BrewlogClient {
|
pub struct BrewlogClient {
|
||||||
base_url: Url,
|
base_url: Url,
|
||||||
http: Client,
|
http: Client,
|
||||||
|
token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrewlogClient {
|
impl BrewlogClient {
|
||||||
|
|
@ -18,6 +20,8 @@ impl BrewlogClient {
|
||||||
normalized.set_path(&format!("{}/", normalized.path().trim_end_matches('/')));
|
normalized.set_path(&format!("{}/", normalized.path().trim_end_matches('/')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let token = std::env::var("BREWLOG_TOKEN").ok();
|
||||||
|
|
||||||
let http = Client::builder()
|
let http = Client::builder()
|
||||||
.user_agent("brewlog-cli/0.1")
|
.user_agent("brewlog-cli/0.1")
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -26,6 +30,7 @@ impl BrewlogClient {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
base_url: normalized,
|
base_url: normalized,
|
||||||
http,
|
http,
|
||||||
|
token,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,6 +47,10 @@ impl BrewlogClient {
|
||||||
roasts::RoastsClient::new(self)
|
roasts::RoastsClient::new(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tokens(&self) -> tokens::TokensClient<'_> {
|
||||||
|
tokens::TokensClient::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn endpoint(&self, path: &str) -> Result<Url> {
|
pub(crate) fn endpoint(&self, path: &str) -> Result<Url> {
|
||||||
self.base_url
|
self.base_url
|
||||||
.join(path)
|
.join(path)
|
||||||
|
|
@ -52,6 +61,15 @@ impl BrewlogClient {
|
||||||
&self.http
|
&self.http
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a request with authentication if token is available
|
||||||
|
pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> reqwest::RequestBuilder {
|
||||||
|
let mut request = self.http.request(method, url);
|
||||||
|
if let Some(token) = &self.token {
|
||||||
|
request = request.bearer_auth(token);
|
||||||
|
}
|
||||||
|
request
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle_response<T>(&self, response: reqwest::Response) -> Result<T>
|
pub(crate) async fn handle_response<T>(&self, response: reqwest::Response) -> Result<T>
|
||||||
where
|
where
|
||||||
T: serde::de::DeserializeOwned,
|
T: serde::de::DeserializeOwned,
|
||||||
|
|
|
||||||
69
src/client/tokens.rs
Normal file
69
src/client/tokens.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::client::BrewlogClient;
|
||||||
|
use crate::domain::tokens::Token;
|
||||||
|
|
||||||
|
pub struct TokensClient<'a> {
|
||||||
|
client: &'a BrewlogClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TokensClient<'a> {
|
||||||
|
pub fn new(client: &'a BrewlogClient) -> Self {
|
||||||
|
Self { client }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create(
|
||||||
|
&self,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<TokenResponse> {
|
||||||
|
let url = self.client.endpoint("api/v1/tokens")?;
|
||||||
|
let body = CreateTokenRequest {
|
||||||
|
username: username.to_string(),
|
||||||
|
password: password.to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.http_client()
|
||||||
|
.post(url)
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.client.handle_response(response).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list(&self) -> Result<Vec<Token>> {
|
||||||
|
let url = self.client.endpoint("api/v1/tokens")?;
|
||||||
|
|
||||||
|
let response = self.client.http_client().get(url).send().await?;
|
||||||
|
|
||||||
|
self.client.handle_response(response).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn revoke(&self, id: &str) -> Result<Token> {
|
||||||
|
let url = self.client.endpoint(&format!("api/v1/tokens/{}/revoke", id))?;
|
||||||
|
|
||||||
|
let response = self.client.http_client().post(url).send().await?;
|
||||||
|
|
||||||
|
self.client.handle_response(response).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct CreateTokenRequest {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct TokenResponse {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use brewlog::cli::{Cli, Commands, ServeCommand, roasters, roasts};
|
use brewlog::cli::{Cli, Commands, ServeCommand, roasters, roasts, tokens};
|
||||||
use brewlog::client::BrewlogClient;
|
use brewlog::client::BrewlogClient;
|
||||||
use brewlog::server::{ServerConfig, serve};
|
use brewlog::server::{ServerConfig, serve};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
@ -18,6 +18,11 @@ async fn main() -> Result<()> {
|
||||||
command => {
|
command => {
|
||||||
let client = BrewlogClient::from_base_url(&cli.api_url)?;
|
let client = BrewlogClient::from_base_url(&cli.api_url)?;
|
||||||
match command {
|
match command {
|
||||||
|
// Tokens
|
||||||
|
Commands::CreateToken(cmd) => tokens::create_token(&client, cmd).await,
|
||||||
|
Commands::ListTokens => tokens::list_tokens(&client).await,
|
||||||
|
Commands::RevokeToken(cmd) => tokens::revoke_token(&client, cmd).await,
|
||||||
|
|
||||||
// Roasters
|
// Roasters
|
||||||
Commands::AddRoaster(cmd) => roasters::add_roaster(&client, cmd).await,
|
Commands::AddRoaster(cmd) => roasters::add_roaster(&client, cmd).await,
|
||||||
Commands::ListRoasters => roasters::list_roasters(&client).await,
|
Commands::ListRoasters => roasters::list_roasters(&client).await,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue