use clap::Subcommand;
use crate::api::ControlPlaneClient;
use crate::config::{default_api_base, SessionConfig};
#[derive(Debug, Subcommand)]
#[command(
about = "Control plane authentication and API tokens",
long_about = "Manage human login (email/password sessions) and automation API tokens.\n\
Requires flatland-control-plane running (default http://127.0.0.1:7380)."
)]
pub enum AuthCommand {
Register {
#[arg(long)]
email: String,
#[arg(long)]
password: String,
#[arg(long)]
name: String,
#[arg(long, default_value_t = default_api_base())]
api: String,
#[arg(long)]
no_login: bool,
},
Login {
#[arg(long)]
email: String,
#[arg(long)]
password: String,
#[arg(long, default_value_t = default_api_base())]
api: String,
},
#[command(subcommand)]
Token(TokenCommand),
}
#[derive(Debug, Subcommand)]
pub enum TokenCommand {
Create {
#[arg(long)]
name: String,
#[arg(long, value_delimiter = ',')]
scope: Vec<String>,
#[arg(long)]
test: bool,
},
List,
Revoke {
id: uuid::Uuid,
},
}
pub async fn run(command: AuthCommand) -> anyhow::Result<()> {
match command {
AuthCommand::Register {
email,
password,
name,
api,
no_login,
} => register(&email, &password, &name, &api, !no_login).await,
AuthCommand::Login { email, password, api } => login(&email, &password, &api).await,
AuthCommand::Token(command) => run_token(command).await,
}
}
async fn register(
email: &str,
password: &str,
display_name: &str,
api: &str,
login_after: bool,
) -> anyhow::Result<()> {
let client = ControlPlaneClient::new(api);
#[derive(serde::Deserialize)]
struct RegisterData {
account: AccountSummary,
}
#[derive(serde::Deserialize)]
struct AccountSummary {
email: String,
display_name: String,
}
let data: RegisterData = client
.post(
"/v1/auth/register",
&serde_json::json!({
"email": email,
"password": password,
"display_name": display_name,
}),
None,
)
.await?;
println!(
"Registered {} ({})",
data.account.email, data.account.display_name
);
if login_after {
login(email, password, api).await?;
} else {
println!("Run `flatland3 auth login --email {email} --password '…'` to sign in.");
}
Ok(())
}
async fn login(email: &str, password: &str, api: &str) -> anyhow::Result<()> {
let client = ControlPlaneClient::new(api);
#[derive(serde::Deserialize)]
struct LoginData {
session_token: String,
}
let data: LoginData = client
.post(
"/v1/auth/login",
&serde_json::json!({ "email": email, "password": password }),
None,
)
.await?;
let config = SessionConfig {
api_base: api.trim_end_matches('/').to_string(),
session_token: data.session_token,
last_character_id: None,
};
config.save()?;
println!(
"Logged in. Session saved to {}",
SessionConfig::path()?.display()
);
Ok(())
}
async fn run_token(command: TokenCommand) -> anyhow::Result<()> {
let session = SessionConfig::load()?;
let client = ControlPlaneClient::new(&session.api_base);
match command {
TokenCommand::Create { name, scope, test } => {
#[derive(serde::Deserialize)]
struct CreateData {
token: String,
record: serde_json::Value,
}
let data: CreateData = client
.post(
"/v1/me/tokens",
&serde_json::json!({
"label": name,
"scopes": scope,
"test": test,
}),
Some(&session.session_token),
)
.await?;
println!("API token (save now — shown once):\n{}", data.token);
println!("\nRecord: {}", serde_json::to_string_pretty(&data.record)?);
}
TokenCommand::List => {
#[derive(serde::Deserialize)]
struct ListData {
tokens: Vec<serde_json::Value>,
}
let data: ListData = client
.get("/v1/me/tokens", &session.session_token)
.await?;
println!("{}", serde_json::to_string_pretty(&data.tokens)?);
}
TokenCommand::Revoke { id } => {
client
.delete::<serde_json::Value>(
&format!("/v1/me/tokens/{id}"),
&session.session_token,
)
.await?;
println!("Revoked token {id}");
}
}
Ok(())
}