use serde::{Deserialize, Serialize};
use vti_common::error::AppError;
use crate::acl::admin_consent::Operation;
use crate::acl::get_acl_entry;
use crate::auth::AuthClaims;
use crate::ceremony::{LeaveOutcome, remove_inner};
use crate::error::TaskError;
use crate::members::Disposition;
use crate::routes::acl::caller_covers_target;
use crate::server::AppState;
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[derive(utoipa::ToSchema)]
pub struct RemoveBody {
#[serde(default)]
pub disposition: Option<Disposition>,
#[serde(default)]
pub reason: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(utoipa::ToSchema)]
pub struct RemoveResponse {
pub did: String,
pub disposition: String,
pub removed: bool,
}
impl From<LeaveOutcome> for RemoveResponse {
fn from(o: LeaveOutcome) -> Self {
Self {
did: o.did,
disposition: o.disposition,
removed: o.removed,
}
}
}
const REASON_MAX: usize = 1024;
pub(crate) async fn admin_remove_inner(
state: &AppState,
actor: &AuthClaims,
target_did: &str,
body: RemoveBody,
op: Operation<'_>,
) -> Result<LeaveOutcome, TaskError> {
let actor_did = actor.did.as_str();
vti_common::identifier::validate_did("did", target_did)?;
if actor_did == target_did {
return Err(AppError::Validation(
"use vtc/members/self-remove to remove yourself — \
vtc/members/admin-remove is for admins removing other members"
.to_string(),
)
.into());
}
let reason = body.reason.unwrap_or_default();
if reason.len() > REASON_MAX {
return Err(AppError::Validation(format!(
"reason exceeds {REASON_MAX} chars (got {})",
reason.len(),
))
.into());
}
if let Some(entry) = get_acl_entry(&state.acl_ks, target_did).await?
&& entry.is_administrator()
&& !caller_covers_target(state, actor_did, &entry).await?
{
return Err(AppError::Forbidden(format!(
"{target_did} is an administrator holding authority outside yours — only an \
administrator holding vtc.roles.assign over everything it holds can remove it \
(VTI-ACL-050)"
))
.into());
}
remove_inner(
state,
actor_did,
target_did,
body.disposition,
reason,
Some(op),
)
.await
}