use indexmap::IndexMap;
use colored::Colorize;
use comfy_table::{modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, Cell, Color, ContentArrangement, Table};
use crate::config::{FirebaseConfig, FirebaseProjectInfo, TestResult};
pub fn print_config_info(config: &FirebaseConfig, app_name: Option<&str>) {
let masked_key = if config.api_key.len() > 16 {
format!("{}...{}", &config.api_key[..12], &config.api_key[config.api_key.len() - 4..])
} else {
config.api_key.clone()
};
println!();
println!("╭─ {} ─────────────────────────────────────╮", "Firebase Configuration".bright_cyan().bold());
if let Some(name) = app_name {
println!("│ App: {:<40}│", name);
}
println!("│ API Key: {:<40}│", masked_key.yellow());
println!("│ App ID: {:<40}│", config.app_id.as_deref().unwrap_or("Not set").dimmed());
println!("│ Project: {:<40}│", config.project_id.as_deref().unwrap_or("Not set").green());
println!("│ Sender ID: {:<40}│", config.gcm_sender_id.as_deref().unwrap_or("Not set").dimmed());
println!("╰──────────────────────────────────────────────────────────╯");
}
pub fn print_report(
_config: &FirebaseConfig,
firebase_results: &IndexMap<String, TestResult>,
google_results: &IndexMap<String, TestResult>,
project_info: &FirebaseProjectInfo,
) {
println!("\n{}", "Firebase Services".bright_magenta().bold());
let mut fb_table = Table::new();
fb_table
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS)
.set_content_arrangement(ContentArrangement::Dynamic);
fb_table.set_header(vec![
Cell::new("Service").fg(Color::Magenta),
Cell::new("Status"),
Cell::new("Code"),
Cell::new("Details"),
]);
for (name, res) in firebase_results {
let (status_text, status_color) = if res.success {
("WORKS", Color::Green)
} else {
("DENIED", Color::Red)
};
let code: String = res
.status_code
.map(|c: u16| c.to_string())
.unwrap_or_else(|| "—".to_string());
let detail = res
.detail
.as_deref()
.or(res.error.as_deref())
.unwrap_or("");
let detail_truncated: String = detail.chars().take(55).collect::<String>();
let detail_color = if detail.contains("CRITICAL") {
Color::Red
} else if detail.contains("WARNING") {
Color::Yellow
} else {
Color::White
};
fb_table.add_row(vec![
Cell::new(name).fg(Color::Cyan),
Cell::new(status_text).fg(status_color),
Cell::new(&code),
Cell::new(&detail_truncated).fg(detail_color),
]);
}
println!("{fb_table}");
println!("\n{}", "Google Cloud APIs".bright_magenta().bold());
let mut g_table = Table::new();
g_table
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS)
.set_content_arrangement(ContentArrangement::Dynamic);
g_table.set_header(vec![
Cell::new("Service").fg(Color::Magenta),
Cell::new("Status"),
Cell::new("Code"),
Cell::new("Details"),
]);
for (name, res) in google_results {
let (status_text, status_color, detail) = if res.success {
("VALID", Color::Green, "Works".to_string())
} else {
let mut d = res.error.clone().unwrap_or_else(|| "Failed".to_string());
if let Some(ref hint) = res.detail {
d = format!("{} → {}", d, hint);
}
("FAIL", Color::Red, d)
};
let code: String = res
.status_code
.map(|c: u16| c.to_string())
.unwrap_or_else(|| "—".to_string());
let detail_truncated: String = detail.chars().take(45).collect::<String>();
g_table.add_row(vec![
Cell::new(name).fg(Color::Cyan),
Cell::new(status_text).fg(status_color),
Cell::new(&code),
Cell::new(&detail_truncated),
]);
}
println!("{g_table}");
print_firebase_summary(project_info);
println!("\n{}", "Summary:".bold());
let fb_success = firebase_results.values().any(|r| r.success);
let g_success = google_results.values().any(|r| r.success);
if fb_success || g_success {
println!(
"{}",
"✓ Key is VALID (accepted by at least one service)"
.green()
.bold()
);
} else {
println!(
"{}",
"✗ Key appears INVALID or fully restricted".red().bold()
);
}
println!("\n{}", "Key Insights:".yellow());
println!(" • Firebase API keys IDENTIFY projects, not AUTHORIZE access");
println!(" • App ID is needed for Installations API but not Identity Toolkit");
println!(" • FCM server operations may require a different (server) key");
println!(" • 403 = restriction, 401 = invalid key");
}
fn print_firebase_summary(info: &FirebaseProjectInfo) {
println!();
println!("{}", "Firebase Project Analysis".bright_cyan().bold());
println!(" ├── {}", "Project Identity".bold());
println!(
" │ ├── Project ID: {}",
info.project_id
.as_deref()
.unwrap_or("Unknown")
.yellow()
);
if let Some(ref pn) = info.project_number {
println!(" │ ├── Project Number: {}", pn.dimmed());
}
if let Some(ref sb) = info.storage_bucket {
println!(" │ └── Storage Bucket: {}", sb.dimmed());
} else {
println!(" │ └── (no additional identity info)");
}
println!(" ├── {}", "Authentication Configuration".bold());
if let Some(anon) = info.anonymous_auth_enabled {
let status = if anon {
"ENABLED".green().bold().to_string()
} else {
"Disabled".dimmed().to_string()
};
println!(" │ ├── Anonymous Auth: {}", status);
}
if let Some(email) = info.email_auth_enabled {
let status = if email {
"ENABLED".green().bold().to_string()
} else {
"Disabled".dimmed().to_string()
};
println!(" │ ├── Email/Password: {}", status);
}
if !info.enabled_providers.is_empty() {
println!(" │ └── OAuth Providers:");
let unique: Vec<&String> = {
let mut seen = std::collections::HashSet::new();
info.enabled_providers
.iter()
.filter(|p| seen.insert(p.as_str()))
.collect()
};
for (i, p) in unique.iter().enumerate() {
let prefix = if i == unique.len() - 1 {
"└──"
} else {
"├──"
};
println!(" │ {} {}", prefix, p.cyan());
}
} else {
println!(" │ └── OAuth Providers: (none detected)");
}
println!(" ├── {}", "Firebase Cloud Messaging".bold());
if let Some(fcm) = info.fcm_enabled {
let status = if fcm {
"ENABLED".green().bold().to_string()
} else {
"Disabled/Restricted".dimmed().to_string()
};
println!(" │ ├── Legacy API: {}", status);
}
if let Some(tok) = info.fcm_token_obtained {
let status = if tok {
"Works".green().to_string()
} else {
"Failed".dimmed().to_string()
};
println!(" │ ├── Token Registration: {}", status);
}
if let Some(topics) = info.fcm_topics_accessible {
let status = if topics {
"Accessible".green().to_string()
} else {
"Restricted".dimmed().to_string()
};
println!(" │ └── Topic Management: {}", status);
} else {
println!(" │ └── (no additional FCM info)");
}
println!(" └── {}", "Security Findings".bold());
let mut critical: Vec<String> = Vec::new();
let mut warnings: Vec<String> = Vec::new();
if info.realtime_db_public_read == Some(true) {
critical.push("Realtime Database PUBLICLY READABLE".to_string());
}
if info.storage_public == Some(true) {
critical.push("Storage Bucket PUBLICLY LISTABLE".to_string());
}
if info.fcm_enabled == Some(true) {
warnings.push("FCM enabled — push notifications possible".to_string());
}
if info.anonymous_auth_enabled == Some(true) {
warnings.push("Anonymous auth — unrestricted account creation".to_string());
}
if !critical.is_empty() {
println!(" ├── {}", "CRITICAL ISSUES".red().bold());
for (i, issue) in critical.iter().enumerate() {
let prefix = if i == critical.len() - 1 && warnings.is_empty() {
"└──"
} else {
"├──"
};
println!(" │ {} {}", prefix, issue.red());
}
}
if !warnings.is_empty() {
println!(" ├── {}", "Warnings".yellow().bold());
for (i, w) in warnings.iter().enumerate() {
let prefix = if i == warnings.len() - 1 {
"└──"
} else {
"├──"
};
println!(" │ {} {}", prefix, w.yellow());
}
}
if critical.is_empty() && warnings.is_empty() {
println!(" └── {}", "No critical issues detected".green());
}
}
pub fn print_saved_configs() {
use crate::config::{config_file_path, load_saved_configs};
let store = load_saved_configs();
if store.configs.is_empty() {
println!("\n{}", "No saved configurations found.".yellow().bold());
println!();
println!(
" Run {} to discover and save configs from an APK.",
"flintbase scan <package> --save".bold()
);
println!(
" Configs are stored in: {}",
config_file_path().display().to_string().dimmed()
);
return;
}
println!("\n{}", "Saved Configurations".bright_cyan().bold());
println!(
"{}\n",
format!("({})", config_file_path().display()).dimmed()
);
for (key, cfg) in &store.configs {
let bar_len = 58usize.saturating_sub(key.len());
println!("╭─ --config {} {}", key.bold(), "─".repeat(bar_len));
println!("│ {}: {}", "Name".dimmed(), cfg.name);
if let Some(ref pkg) = cfg.package {
println!("│ {}: {}", "Pkg".dimmed(), pkg);
}
println!(
"│ {}: {}...{}",
"Key".dimmed(),
&cfg.api_key[..12.min(cfg.api_key.len())],
&cfg.api_key[cfg.api_key.len().saturating_sub(4)..]
);
if let Some(ref pid) = cfg.project_id {
println!("│ {}: {}", "Project".dimmed(), pid.green());
}
if let Some(ref aid) = cfg.app_id {
println!("│ {}: {}", "App ID".dimmed(), aid);
}
if let Some(ref sid) = cfg.gcm_sender_id {
println!("│ {}: {}", "Sender".dimmed(), sid);
}
if let Some(ref db) = cfg.database_url {
println!("│ {}: {}", "DB".dimmed(), db);
}
if let Some(ref sb) = cfg.storage_bucket {
println!("│ {}: {}", "Bucket".dimmed(), sb);
}
if let Some(ref ts) = cfg.saved_at {
println!("│ {}: {}", "Saved".dimmed(), ts.dimmed());
}
println!("╰{}", "─".repeat(60));
println!();
}
println!(
"{} {} to test a saved config.",
"Tip:".bold(),
"flintbase key --config <name>".bold()
);
println!(
" Edit {} to tweak values.",
config_file_path().display().to_string().dimmed()
);
}