Files
bichon/crates/cli/src/auth.rs
T

218 lines
6.2 KiB
Rust

//
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use std::process;
use console::style;
use dialoguer::{theme::ColorfulTheme, Select};
use reqwest::Client;
use bichon_core::{
account::payload::MinimalAccount,
users::{permissions::Permission, view::UserView},
};
use crate::BichonCliConfig;
async fn fetch_json<T: serde::de::DeserializeOwned>(
client: &Client,
url: &str,
token: &str,
label: &str,
) -> T {
let response = match client
.get(url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
{
Ok(res) => res,
Err(e) => {
eprintln!(
"\n{} {}",
style("✘ Network Error:").red().bold(),
"Could not connect to Bichon service."
);
eprintln!("{} {}", style("Details:").dim(), e);
eprintln!(
"\n{} Please check if the Base URL is correct and the server is running.",
style("Tip:").cyan()
);
process::exit(1);
}
};
let status = response.status();
let body = response.text().await.unwrap_or_else(|_| String::new());
if !status.is_success() {
eprintln!(
"\n{} Server returned an error (Status: {})",
style("✘ API Error:").red().bold(),
style(status).yellow()
);
if status == 401 {
eprintln!(
"{} Your API Token seems to be invalid or expired.",
style("Context:").dim()
);
} else if status == 404 {
eprintln!(
"{} The endpoint was not found. Please check your Base URL.",
style("Context:").dim()
);
}
eprintln!("{} {}", style("Response:").dim(), body);
process::exit(1);
}
if body.is_empty() {
eprintln!(
"\n{} Server returned an empty response for [{}] (Status: {})",
style("✘ Empty Response:").red().bold(),
label,
status
);
eprintln!(
"{} This may be caused by a reverse proxy or middleware issue.",
style("Tip:").cyan()
);
process::exit(1);
}
match serde_json::from_str::<T>(&body) {
Ok(data) => data,
Err(e) => {
eprintln!(
"\n{} Failed to parse response for [{}]: {}",
style("✘ Parse Error:").red().bold(),
label,
e
);
eprintln!("{} Raw body: {}", style("Debug:").dim(), body);
process::exit(1);
}
}
}
pub async fn verify_user_and_get_account(
config: &BichonCliConfig,
theme: &ColorfulTheme,
only_nosync: bool,
) -> MinimalAccount {
let client = Client::new();
let user: UserView = fetch_json(
&client,
&format!("{}/api/v1/current-user", config.base_url),
&config.api_token,
"current-user",
)
.await;
println!("Welcome, {}!", style(&user.username).cyan());
let accounts: Vec<MinimalAccount> = fetch_json(
&client,
&format!(
"{}/api/v1/minimal-account-list?only_nosync={only_nosync}",
config.base_url
),
&config.api_token,
"minimal-account-list",
)
.await;
if accounts.is_empty() {
println!(
"\n{}",
style("Error: No 'nosync' accounts found.").red().bold()
);
println!(
"{}",
style("Mail import is only supported for 'nosync' type accounts.").dim()
);
println!(
"Please create a new {} account in the Bichon web interface first.",
style("Nosync").bold().yellow()
);
process::exit(1);
}
let required_permission = Permission::DATA_IMPORT_BATCH;
let mut selectable_accounts = Vec::new();
let mut options = Vec::new();
for acc in accounts {
let has_permission = if let Some(perms) = user.account_permissions.get(&acc.id) {
perms.iter().any(|p| p == required_permission)
} else {
user.global_permissions
.iter()
.any(|p| p == Permission::DATA_MANAGE_ALL || p == Permission::ROOT)
};
let status_prefix = if has_permission {
style(" [READY] ").green()
} else {
style(" [NO PERMISSION] ").red()
};
options.push(format!(
"{}{} - {}",
status_prefix,
style(&acc.email).bold(),
style(format!("ID: {}", acc.id)).dim()
));
selectable_accounts.push((acc, has_permission));
}
let selection = Select::with_theme(theme)
.with_prompt("Select the target account for import")
.items(&options)
.default(0)
.max_length(10)
.interact()
.unwrap();
let (selected_acc, can_import) = &selectable_accounts[selection];
if !*can_import {
eprintln!(
"\n{} You do not have '{}' permission for account {}.",
style("✘ Permission Denied:").red().bold(),
style(required_permission).yellow(),
style(&selected_acc.email).cyan()
);
eprintln!(
"{} Please contact your administrator to upgrade your role for this account.",
style("Tip:").dim()
);
process::exit(1);
}
println!(
"{} Targeting account: {}",
style("✔").green(),
style(&selected_acc.email).cyan().bold()
);
selected_acc.clone()
}