use crate::{
client::{WekanClientFactory, users::tokens::DeleteUserTokenRequest},
command_result::{
CancellationSuccess, CommandSuccess, DestructiveOperation, UserTokenDeleteScope,
UserTokenDeleteSuccess,
},
commands::{
authenticated::AuthenticatedContext,
client_error::map_client_error,
credential_ops::{lock_credential_mutation, map_credential_load_error},
},
credentials::{CredentialStore, SecretInputProvider},
error::{AppError, ErrorCode, ErrorDetails},
exit_code::StableExitCode,
input::{
ConfirmationArgs, ConfirmationDecision, ConfirmationProvider, ConfirmationRequest,
confirm_or_skip, escape_terminal_text,
},
redaction::Redactor,
};
use clap::Args;
use secrecy::{ExposeSecret, SecretString};
use time::OffsetDateTime;
#[derive(Debug, Args)]
#[command(
after_help = "Wekan v12.57 acknowledges raw-token deletion without revoking that session (W11-058). Tokens are sent unchanged; local credentials are retained."
)]
pub struct DeleteArgs {
#[arg(value_parser = super::super::non_empty)]
user_id: String,
#[arg(long, required_unless_present = "all", conflicts_with = "all")]
token_stdin: bool,
#[arg(long)]
all: bool,
#[command(flatten)]
confirmation: ConfirmationArgs,
}
pub(super) async fn execute(
args: DeleteArgs,
factory: &WekanClientFactory,
store: &dyn CredentialStore,
secrets: &dyn SecretInputProvider,
confirmation: &dyn ConfirmationProvider,
) -> Result<CommandSuccess, AppError> {
let context = AuthenticatedContext::load(factory, store)?;
let guard = if args.all && args.user_id == context.record().user_id() {
Some(lock_credential_mutation(
store,
context.credential_target(),
)?)
} else {
None
};
let prompt = ConfirmationRequest::new(
DestructiveOperation::UserTokenDelete,
format!(
"Request deletion of {} for Wekan user `{}`?",
if args.all {
"all tokens"
} else {
"the supplied token"
},
escape_terminal_text(&args.user_id)
),
);
if confirm_or_skip(&args.confirmation, confirmation, &prompt)?
== ConfirmationDecision::Cancelled
{
return Ok(CommandSuccess::Cancelled(CancellationSuccess::new(
DestructiveOperation::UserTokenDelete,
)));
}
let token = if args.token_stdin {
let raw = secrets.read_raw_secret()?;
let value = raw.expose_secret();
let value = value
.strip_suffix("\r\n")
.or_else(|| value.strip_suffix('\n'))
.unwrap_or(value);
if value.is_empty() {
return Err(AppError::invalid_input(
"token stdin must not be empty; use --all to delete all tokens; no request was sent",
));
}
Some(SecretString::from(value.to_owned()))
} else {
None
};
if let Some(guard) = &guard {
let record = guard
.load()
.map_err(map_credential_load_error)?
.ok_or_else(|| {
AppError::new(
ErrorCode::CredentialNotFound,
"the credential used to prepare token deletion is no longer stored; no HTTP request was sent",
StableExitCode::Credential,
)
})?;
if !record.matches(context.record()) {
return Err(AppError::new(
ErrorCode::CredentialStoreFailed,
"the stored credential changed while preparing token deletion; no HTTP request was sent",
StableExitCode::Credential,
)
.with_credential_stored(true));
}
if record.token_expires() <= OffsetDateTime::now_utc() {
return Err(AppError::new(
ErrorCode::CredentialExpired,
format!(
"the stored credential expired at {}; no HTTP request was sent",
context.token_expires()
),
StableExitCode::Server,
)
.with_details(ErrorDetails {
token_expires: Some(context.token_expires().to_owned()),
..ErrorDetails::default()
}));
}
}
let request = DeleteUserTokenRequest {
user_id: args.user_id,
token,
};
let mut redactor = Redactor::with_secret(context.record().token());
if let Some(token) = &request.token {
redactor = redactor.and_secret(token);
}
let response = context
.client()
.delete_user_token(&request, context.record().token())
.await;
drop(guard);
let response = response.map_err(|e| map_client_error(e, &redactor, "token deletion"))?;
Ok(CommandSuccess::UserTokenDeleted(UserTokenDeleteSuccess {
user_id: request.user_id.clone(),
scope: if args.all {
UserTokenDeleteScope::All
} else {
UserTokenDeleteScope::Single
},
message: redactor.redact(response.message.expose_secret()),
}))
}