Skip to content

Commit bece1bf

Browse files
committed
feat: add rich version, verbose logging, colored output, error context, single-user delete
- Rich --version via vergen-gitcl showing git hash and build date - Wire up --verbose with tracing-subscriber (stderr, env-filter) - Colored output: bold/dim key labels, bold table headers, green success messages, red/green disabled status - Add .context() on all SDK calls for user-friendly error messages - Remove fb_anyhow! macro in claims.rs, use IntoAnyhow consistently - Add users remove --email for single-user delete with confirmation - Add tracing debug/info logs in firebase, config, errors, commands - Remove unused toml dependency
1 parent 0502803 commit bece1bf

15 files changed

Lines changed: 575 additions & 134 deletions

Cargo.lock

Lines changed: 278 additions & 61 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ clap = { version = "4", features = ["derive", "env"] }
1111
comfy-table = "7"
1212
confy = "2"
1313
console = "0.16"
14+
const_format = "0.2"
1415
csv = "1"
1516
dialoguer = "0.12"
1617
error-stack = "0.6"
@@ -24,7 +25,11 @@ serde_json = "1"
2425
shellexpand = "3"
2526
time = { version = "0.3", features = ["formatting", "parsing", "macros"] }
2627
tokio = { version = "1", features = ["full"] }
27-
toml = "0.8"
28+
tracing = "0.1"
29+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
30+
31+
[build-dependencies]
32+
vergen-gitcl = { version = "9", features = ["build"] }
2833

2934
[profile.release]
3035
strip = true

README.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,18 @@ fbadmin info # Shows resolved profile, project, credentials, and verifies con
107107

108108
## Global flags
109109

110-
| Flag | Short | Env var | Description |
111-
|---|---|---|---|
112-
| `--profile` | `-p` | `FBADMIN_PROFILE` | Named profile from config |
113-
| `--project` | | `FBADMIN_PROJECT` | Firebase project ID |
114-
| `--credentials` | `-c` | `FBADMIN_CREDENTIALS` | Path to service account JSON |
115-
| `--emulator-host` | `-e` | `FBADMIN_EMULATOR_HOST` | Emulator host:port |
116-
| `--format` | `-f` | | Output format: `table`, `json`, `csv` |
117-
| `--dry-run` | | | Preview destructive operations |
118-
| `--yes` | `-y` | | Skip confirmation prompts |
119-
| `--verbose` | `-v` | | Increase verbosity (`-vv`, `-vvv`) |
110+
111+
| Flag | Short | Env var | Description |
112+
| ----------------- | ----- | ----------------------- | ------------------------------------- |
113+
| `--profile` | `-p` | `FBADMIN_PROFILE` | Named profile from config |
114+
| `--project` | | `FBADMIN_PROJECT` | Firebase project ID |
115+
| `--credentials` | `-c` | `FBADMIN_CREDENTIALS` | Path to service account JSON |
116+
| `--emulator-host` | `-e` | `FBADMIN_EMULATOR_HOST` | Emulator host:port |
117+
| `--format` | `-f` | | Output format: `table`, `json`, `csv` |
118+
| `--dry-run` | | | Preview destructive operations |
119+
| `--yes` | `-y` | | Skip confirmation prompts |
120+
| `--verbose` | `-v` | | Increase verbosity (`-vv`, `-vvv`) |
121+
120122

121123
## Output formats
122124

@@ -129,4 +131,4 @@ fbadmin claims get --email user@example.com -f json # Single record as JSON
129131

130132
## License
131133

132-
MIT
134+
MIT

build.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use vergen_gitcl::{BuildBuilder, Emitter, GitclBuilder};
2+
3+
fn main() -> Result<(), Box<dyn std::error::Error>> {
4+
let build = BuildBuilder::default().build_date(true).build()?;
5+
let git = GitclBuilder::default()
6+
.sha(true)
7+
.commit_date(true)
8+
.build()?;
9+
Emitter::default()
10+
.add_instructions(&build)?
11+
.add_instructions(&git)?
12+
.emit()?;
13+
Ok(())
14+
}

src/commands/claims.rs

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use std::collections::BTreeMap;
22

3-
use anyhow::{Result, bail};
3+
use anyhow::{Context, Result, bail};
44
use indicatif::ProgressBar;
55
use rs_firebase_admin_sdk::auth::{Claims, FirebaseAuthService, UserIdentifiers, UserUpdate};
66
use serde_json::Value;
77

88
use crate::config::resolve_connection;
9+
use crate::errors::IntoAnyhow;
910
use crate::firebase::{AuthBackend, init_firebase};
10-
use crate::output::{render_json_value, render_message, render_table};
11+
use crate::output::{render_json_value, render_message, render_success, render_table};
1112
use crate::prompt::{confirm, resolve_email, resolve_string};
1213
use crate::{ClaimsCommand, Cli};
1314

@@ -65,12 +66,6 @@ fn map_to_claims(map: BTreeMap<String, Value>) -> Claims {
6566
Claims::from(map)
6667
}
6768

68-
macro_rules! fb_anyhow {
69-
($expr:expr) => {
70-
$expr.map_err(|e| anyhow::anyhow!("{e}"))
71-
};
72-
}
73-
7469
async fn get(cli: &Cli, email: Option<String>) -> Result<()> {
7570
let email = resolve_email(email)?;
7671
let conn = resolve_connection(
@@ -81,8 +76,13 @@ async fn get(cli: &Cli, email: Option<String>) -> Result<()> {
8176
)?;
8277
let auth = init_firebase(AuthBackend::from_resolved(&conn)).await?;
8378

79+
tracing::debug!("Fetching claims for {email}");
8480
let identifiers = UserIdentifiers::builder().with_email(email.clone()).build();
85-
let user = fb_anyhow!(auth.get_user(identifiers).await)?
81+
let user = auth
82+
.get_user(identifiers)
83+
.await
84+
.into_anyhow()
85+
.context(format!("Failed to fetch user {email}"))?
8686
.ok_or_else(|| anyhow::anyhow!("User not found: {email}"))?;
8787

8888
let claims_map = claims_to_map(&user.custom_claims);
@@ -114,8 +114,13 @@ async fn merge(
114114
)?;
115115
let auth = init_firebase(AuthBackend::from_resolved(&conn)).await?;
116116

117+
tracing::debug!("Fetching user {email} for claim merge");
117118
let identifiers = UserIdentifiers::builder().with_email(email.clone()).build();
118-
let user = fb_anyhow!(auth.get_user(identifiers).await)?
119+
let user = auth
120+
.get_user(identifiers)
121+
.await
122+
.into_anyhow()
123+
.context(format!("Failed to fetch user {email}"))?
119124
.ok_or_else(|| anyhow::anyhow!("User not found: {email}"))?;
120125

121126
let mut claims_map = claims_to_map(&user.custom_claims);
@@ -127,10 +132,15 @@ async fn merge(
127132
return Ok(());
128133
}
129134

135+
tracing::debug!("Updating claims for {email}");
130136
let update = UserUpdate::builder(user.uid)
131137
.custom_claims(map_to_claims(claims_map))
132138
.build();
133-
let updated_user = fb_anyhow!(auth.update_user(update).await)?;
139+
let updated_user = auth
140+
.update_user(update)
141+
.await
142+
.into_anyhow()
143+
.context(format!("Failed to update claims for {email}"))?;
134144

135145
let updated_map = claims_to_map(&updated_user.custom_claims);
136146
render_json_value(&cli.format, &map_to_json(&updated_map));
@@ -150,8 +160,13 @@ async fn remove(cli: &Cli, key: Option<String>, email: Option<String>) -> Result
150160
)?;
151161
let auth = init_firebase(AuthBackend::from_resolved(&conn)).await?;
152162

163+
tracing::debug!("Fetching user {email} for claim removal");
153164
let identifiers = UserIdentifiers::builder().with_email(email.clone()).build();
154-
let user = fb_anyhow!(auth.get_user(identifiers).await)?
165+
let user = auth
166+
.get_user(identifiers)
167+
.await
168+
.into_anyhow()
169+
.context(format!("Failed to fetch user {email}"))?
155170
.ok_or_else(|| anyhow::anyhow!("User not found: {email}"))?;
156171

157172
let mut claims_map = claims_to_map(&user.custom_claims);
@@ -167,10 +182,15 @@ async fn remove(cli: &Cli, key: Option<String>, email: Option<String>) -> Result
167182
return Ok(());
168183
}
169184

185+
tracing::debug!("Removing claim '{key}' for {email}");
170186
let update = UserUpdate::builder(user.uid)
171187
.custom_claims(map_to_claims(claims_map))
172188
.build();
173-
let updated_user = fb_anyhow!(auth.update_user(update).await)?;
189+
let updated_user = auth
190+
.update_user(update)
191+
.await
192+
.into_anyhow()
193+
.context(format!("Failed to update claims for {email}"))?;
174194

175195
let updated_map = claims_to_map(&updated_user.custom_claims);
176196
render_json_value(&cli.format, &map_to_json(&updated_map));
@@ -200,16 +220,24 @@ async fn clear(cli: &Cli, email: Option<String>) -> Result<()> {
200220
)?;
201221
let auth = init_firebase(AuthBackend::from_resolved(&conn)).await?;
202222

223+
tracing::debug!("Clearing all claims for {email}");
203224
let identifiers = UserIdentifiers::builder().with_email(email.clone()).build();
204-
let user = fb_anyhow!(auth.get_user(identifiers).await)?
225+
let user = auth
226+
.get_user(identifiers)
227+
.await
228+
.into_anyhow()
229+
.context(format!("Failed to fetch user {email}"))?
205230
.ok_or_else(|| anyhow::anyhow!("User not found: {email}"))?;
206231

207232
let update = UserUpdate::builder(user.uid)
208233
.custom_claims(map_to_claims(BTreeMap::new()))
209234
.build();
210-
fb_anyhow!(auth.update_user(update).await)?;
235+
auth.update_user(update)
236+
.await
237+
.into_anyhow()
238+
.context(format!("Failed to clear claims for {email}"))?;
211239

212-
render_message(&format!("Cleared all custom claims for {email}"));
240+
render_success(&format!("Cleared all custom claims for {email}"));
213241

214242
Ok(())
215243
}
@@ -233,7 +261,11 @@ async fn find(cli: &Cli, key: String, value: Option<String>, exclusive: bool) ->
233261
loop {
234262
spinner.tick();
235263

236-
let page = fb_anyhow!(auth.list_users(1000, prev_page).await)?;
264+
let page = auth
265+
.list_users(1000, prev_page)
266+
.await
267+
.into_anyhow()
268+
.context("Failed to list users during claim search")?;
237269

238270
let user_list = match page {
239271
Some(list) => list,

src/commands/config_cmd.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::config::{
44
Profile, add_profile, config_dir, load_config, remove_profile, resolve_connection, save_config,
55
set_default,
66
};
7-
use crate::output::{render_message, render_single_record, render_table};
7+
use crate::output::{render_message, render_single_record, render_success, render_table};
88
use crate::prompt::{resolve_select, resolve_string};
99
use crate::{Cli, ConfigCommand};
1010

@@ -80,7 +80,7 @@ async fn init(_cli: &Cli) -> Result<()> {
8080
}
8181

8282
save_config(&config)?;
83-
render_message(&format!("Profile '{name}' created."));
83+
render_success(&format!("Profile '{name}' created."));
8484

8585
Ok(())
8686
}
@@ -114,9 +114,9 @@ async fn add(
114114
save_config(&config)?;
115115

116116
if overwriting {
117-
render_message(&format!("Profile '{name}' updated."));
117+
render_success(&format!("Profile '{name}' updated."));
118118
} else {
119-
render_message(&format!("Profile '{name}' added."));
119+
render_success(&format!("Profile '{name}' added."));
120120
}
121121

122122
Ok(())
@@ -131,7 +131,7 @@ async fn remove(_cli: &Cli, name: Option<String>) -> Result<()> {
131131
remove_profile(&mut config, &name)?;
132132
save_config(&config)?;
133133

134-
render_message(&format!("Profile '{name}' removed."));
134+
render_success(&format!("Profile '{name}' removed."));
135135
Ok(())
136136
}
137137

@@ -144,7 +144,7 @@ async fn default(_cli: &Cli, name: Option<String>) -> Result<()> {
144144
set_default(&mut config, &name)?;
145145
save_config(&config)?;
146146

147-
render_message(&format!("Default profile set to '{name}'."));
147+
render_success(&format!("Default profile set to '{name}'."));
148148
Ok(())
149149
}
150150

src/commands/emulator.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use anyhow::Result;
1+
use anyhow::{Context, Result};
22
use rs_firebase_admin_sdk::auth::FirebaseEmulatorAuthService;
33

44
use crate::config::resolve_connection;
55
use crate::errors::IntoAnyhow;
66
use crate::firebase::{AuthBackend, init_firebase, require_emulator};
7-
use crate::output::{render_json_value, render_message};
7+
use crate::output::{render_json_value, render_success};
88
use crate::{Cli, EmulatorCommand};
99

1010
pub async fn run(cli: &Cli, command: &EmulatorCommand) -> Result<()> {
@@ -24,9 +24,13 @@ async fn clear_users(cli: &Cli) -> Result<()> {
2424
require_emulator(&conn)?;
2525
let auth = init_firebase(AuthBackend::from_resolved(&conn)).await?;
2626

27-
auth.clear_all_users().await.into_anyhow()?;
27+
tracing::debug!("Clearing all emulator users");
28+
auth.clear_all_users()
29+
.await
30+
.into_anyhow()
31+
.context("Failed to clear emulator users")?;
2832

29-
render_message("All users cleared.");
33+
render_success("All users cleared.");
3034

3135
Ok(())
3236
}
@@ -41,7 +45,12 @@ async fn config(cli: &Cli) -> Result<()> {
4145
require_emulator(&conn)?;
4246
let auth = init_firebase(AuthBackend::from_resolved(&conn)).await?;
4347

44-
let config = auth.get_emulator_configuration().await.into_anyhow()?;
48+
tracing::debug!("Fetching emulator configuration");
49+
let config = auth
50+
.get_emulator_configuration()
51+
.await
52+
.into_anyhow()
53+
.context("Failed to get emulator configuration")?;
4554
let value = serde_json::to_value(&config)?;
4655

4756
render_json_value(&cli.format, &value);

src/commands/info.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
use anyhow::Result;
1+
use anyhow::{Context, Result};
22
use rs_firebase_admin_sdk::auth::FirebaseAuthService;
33

44
use crate::Cli;
55
use crate::config::resolve_connection;
66
use crate::errors::IntoAnyhow;
77
use crate::firebase::{AuthBackend, init_firebase};
8-
use crate::output::{render_message, render_single_record};
8+
use crate::output::{render_message, render_single_record, render_success};
99

1010
pub async fn run(cli: &Cli) -> Result<()> {
1111
let conn = resolve_connection(
@@ -50,9 +50,15 @@ pub async fn run(cli: &Cli) -> Result<()> {
5050

5151
render_single_record(&cli.format, &fields);
5252

53+
tracing::debug!("Verifying connectivity");
5354
match init_firebase(AuthBackend::from_resolved(&conn)).await {
54-
Ok(auth) => match auth.list_users(1, None).await.into_anyhow() {
55-
Ok(_) => render_message("status: connected ✓"),
55+
Ok(auth) => match auth
56+
.list_users(1, None)
57+
.await
58+
.into_anyhow()
59+
.context("Connectivity check failed")
60+
{
61+
Ok(_) => render_success("status: connected"),
5662
Err(err) => render_message(&format!("status: error - {err}")),
5763
},
5864
Err(err) => render_message(&format!("status: error - {err}")),

src/commands/links.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use anyhow::Result;
1+
use anyhow::{Context, Result};
22
use rs_firebase_admin_sdk::auth::FirebaseAuthService;
33
use rs_firebase_admin_sdk::auth::oob_code::{OobCodeAction, OobCodeActionType};
44

@@ -37,11 +37,13 @@ async fn generate_link(
3737
)?;
3838
let auth = init_firebase(AuthBackend::from_resolved(&conn)).await?;
3939

40-
let action = OobCodeAction::builder(action_type, email).build();
40+
tracing::debug!("Generating {action_type:?} link for {email}");
41+
let action = OobCodeAction::builder(action_type, email.clone()).build();
4142
let link = auth
4243
.generate_email_action_link(action)
4344
.await
44-
.into_anyhow()?;
45+
.into_anyhow()
46+
.context(format!("Failed to generate link for {email}"))?;
4547

4648
render_message(&link);
4749

0 commit comments

Comments
 (0)