use crate::nip98::inspect_public_key;
use clap::{Args, Subcommand};
use maincopy_shared::auth::{HumanLoginProvider, UserRole, UserStatus};
use uuid::Uuid;
#[derive(Debug, Subcommand)]
pub(crate) enum UserCommand {
Create(CreateUserArguments),
Credentials {
user_id: Uuid,
#[command(subcommand)]
command: UserCredentialCommand,
},
List {
#[arg(long)]
cursor: Option<Uuid>,
},
Inspect { user_id: Uuid },
Status {
#[command(flatten)]
target: UserTarget,
#[arg(long, value_parser = parse_user_status)]
status: UserStatus,
},
Roles {
#[command(flatten)]
target: UserTarget,
#[arg(long, required = true, num_args = 1..=3, value_delimiter = ',', value_parser = parse_user_role)]
roles: Vec<UserRole>,
},
}
#[derive(Debug, Args)]
pub(crate) struct UserTarget {
pub(crate) user_id: Uuid,
#[arg(long, value_parser = clap::value_parser!(u64).range(1..i64::MAX as u64))]
pub(crate) expected_version: u64,
#[arg(long)]
pub(crate) idempotency_key: Option<Uuid>,
}
fn parse_user_status(value: &str) -> Result<UserStatus, clap::Error> {
UserStatus::parse(value).ok_or_else(|| {
clap::Error::raw(
clap::error::ErrorKind::InvalidValue,
"status must be enabled or disabled",
)
})
}
fn parse_user_role(value: &str) -> Result<UserRole, clap::Error> {
UserRole::parse(value).ok_or_else(|| {
clap::Error::raw(
clap::error::ErrorKind::InvalidValue,
"role must be owner, administrator, or publisher",
)
})
}
#[derive(Debug, Args)]
#[command(subcommand_precedence_over_arg = true)]
pub(crate) struct CreateUserArguments {
#[arg(long, default_value = "enabled", value_parser = parse_user_status)]
pub(crate) status: UserStatus,
#[arg(long, required = true, num_args = 1..=3, value_delimiter = ',', value_parser = parse_user_role)]
pub(crate) roles: Vec<UserRole>,
#[arg(long)]
pub(crate) idempotency_key: Option<Uuid>,
#[command(subcommand)]
pub(crate) credentials: InitialCredentials,
}
#[derive(Debug, Subcommand)]
pub(crate) enum InitialCredentials {
Password {
#[arg(long, value_parser = parse_username)]
username: Box<str>,
},
Nostr {
#[arg(long, value_parser = parse_public_key)]
public_key: Box<str>,
},
Both {
#[arg(long, value_parser = parse_username)]
username: Box<str>,
#[arg(long, value_parser = parse_public_key)]
public_key: Box<str>,
},
}
#[derive(Debug, Subcommand)]
pub(crate) enum LoginCredentialArguments {
Password {
#[arg(long, value_parser = parse_username)]
username: Box<str>,
},
Nostr {
#[arg(long, value_parser = parse_public_key)]
public_key: Box<str>,
},
}
#[derive(Debug, Subcommand)]
pub(crate) enum UserCredentialCommand {
Add {
#[arg(long)]
idempotency_key: Option<Uuid>,
#[command(subcommand)]
credential: LoginCredentialArguments,
},
Replace {
#[command(flatten)]
target: CredentialTarget,
#[command(subcommand)]
credential: LoginCredentialArguments,
},
Remove {
#[command(flatten)]
target: CredentialTarget,
#[arg(long, value_parser = parse_provider)]
provider: HumanLoginProvider,
},
}
#[derive(Debug, Args)]
pub(crate) struct CredentialTarget {
#[arg(long, value_parser = clap::value_parser!(u64).range(1..i64::MAX as u64))]
pub(crate) expected_version: u64,
#[arg(long)]
pub(crate) idempotency_key: Option<Uuid>,
}
fn parse_username(value: &str) -> Result<Box<str>, &'static str> {
let bytes = value.as_bytes();
if !(1..=64).contains(&bytes.len())
|| !bytes.first().is_some_and(u8::is_ascii_alphanumeric)
|| !bytes.last().is_some_and(u8::is_ascii_alphanumeric)
|| !bytes
.iter()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"-_.".contains(byte))
{
return Err(
"username must be 1–64 lowercase ASCII letters, digits, '-', '_', or '.', beginning and ending with a letter or digit",
);
}
Ok(value.into())
}
fn parse_public_key(value: &str) -> Result<Box<str>, &'static str> {
inspect_public_key(value)
.map(|identity| identity.public_key)
.map_err(|_| "public key must be a valid x-only secp256k1 key in 64 lowercase hexadecimal characters")
}
fn parse_provider(value: &str) -> Result<HumanLoginProvider, &'static str> {
HumanLoginProvider::parse(value).ok_or("provider must be password or nostr")
}