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",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"rpassword",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
|
|
@ -1874,6 +1875,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "rsa"
|
||||
version = "0.9.9"
|
||||
|
|
@ -1894,6 +1906,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
|
|
@ -3053,6 +3075,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ block-id = "0.2.1"
|
|||
chrono = { version = "0.4", features = ["serde", "clock"] }
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||
rpassword = "7.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
once_cell = "1.19"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
pub mod roasters;
|
||||
pub mod roasts;
|
||||
pub mod tokens;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use roasters::{AddRoasterCommand, DeleteRoasterCommand, GetRoasterCommand, UpdateRoasterCommand};
|
||||
use roasts::{AddRoastCommand, DeleteRoastCommand, GetRoastCommand, ListRoastsCommand};
|
||||
use tokens::{CreateTokenCommand, RevokeTokenCommand};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(author, version, about = "Track coffee roasts, brews, and cups", long_about = None)]
|
||||
|
|
@ -27,6 +29,14 @@ pub enum Commands {
|
|||
#[command(name = "serve")]
|
||||
Serve(ServeCommand),
|
||||
|
||||
// Tokens
|
||||
#[command(name = "create-token")]
|
||||
CreateToken(CreateTokenCommand),
|
||||
#[command(name = "list-tokens")]
|
||||
ListTokens,
|
||||
#[command(name = "revoke-token")]
|
||||
RevokeToken(RevokeTokenCommand),
|
||||
|
||||
// Roasters
|
||||
#[command(name = "add-roaster")]
|
||||
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 roasts;
|
||||
pub mod tokens;
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use reqwest::{Client, Url};
|
||||
|
|
@ -9,6 +10,7 @@ use crate::server::errors::ErrorResponse;
|
|||
pub struct BrewlogClient {
|
||||
base_url: Url,
|
||||
http: Client,
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
impl BrewlogClient {
|
||||
|
|
@ -18,6 +20,8 @@ impl BrewlogClient {
|
|||
normalized.set_path(&format!("{}/", normalized.path().trim_end_matches('/')));
|
||||
}
|
||||
|
||||
let token = std::env::var("BREWLOG_TOKEN").ok();
|
||||
|
||||
let http = Client::builder()
|
||||
.user_agent("brewlog-cli/0.1")
|
||||
.build()
|
||||
|
|
@ -26,6 +30,7 @@ impl BrewlogClient {
|
|||
Ok(Self {
|
||||
base_url: normalized,
|
||||
http,
|
||||
token,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +47,10 @@ impl BrewlogClient {
|
|||
roasts::RoastsClient::new(self)
|
||||
}
|
||||
|
||||
pub fn tokens(&self) -> tokens::TokensClient<'_> {
|
||||
tokens::TokensClient::new(self)
|
||||
}
|
||||
|
||||
pub(crate) fn endpoint(&self, path: &str) -> Result<Url> {
|
||||
self.base_url
|
||||
.join(path)
|
||||
|
|
@ -52,6 +61,15 @@ impl BrewlogClient {
|
|||
&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>
|
||||
where
|
||||
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 brewlog::cli::{Cli, Commands, ServeCommand, roasters, roasts};
|
||||
use brewlog::cli::{Cli, Commands, ServeCommand, roasters, roasts, tokens};
|
||||
use brewlog::client::BrewlogClient;
|
||||
use brewlog::server::{ServerConfig, serve};
|
||||
use clap::Parser;
|
||||
|
|
@ -18,6 +18,11 @@ async fn main() -> Result<()> {
|
|||
command => {
|
||||
let client = BrewlogClient::from_base_url(&cli.api_url)?;
|
||||
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
|
||||
Commands::AddRoaster(cmd) => roasters::add_roaster(&client, cmd).await,
|
||||
Commands::ListRoasters => roasters::list_roasters(&client).await,
|
||||
|
|
|
|||
Loading…
Reference in a new issue