use crate::serve::changes::{
self, ChangeKind, ChangeListFilter, ChangeRequest, ChangeStatus, NewChange,
};
use crate::serve::error::ServeError;
use crate::serve::rbac::AuthContext;
use crate::serve::state::ServerState;
use axum::Json;
use axum::extract::{Extension, Path, Query, State};
use axum::http::StatusCode;
use serde::Deserialize;
const DEFAULT_LIMIT: usize = 200;
const MAX_LIMIT: usize = 2000;
pub async fn create_change(
State(state): State<ServerState>,
Extension(actor): Extension<AuthContext>,
Json(body): Json<NewChange>,
) -> Result<(StatusCode, Json<ChangeRequest>), ServeError> {
let change = changes::create(&state, &actor, body).await?;
Ok((StatusCode::CREATED, Json(change)))
}
#[derive(Debug, Deserialize)]
pub struct ListQuery {
pub status: Option<String>,
pub kind: Option<String>,
pub requester: Option<String>,
pub tenant: Option<String>,
pub limit: Option<usize>,
}
pub async fn list_changes(
State(state): State<ServerState>,
Extension(actor): Extension<AuthContext>,
Query(q): Query<ListQuery>,
) -> Result<Json<Vec<ChangeRequest>>, ServeError> {
let status = q
.status
.as_deref()
.map(str::parse::<ChangeStatus>)
.transpose()
.map_err(ServeError::BadConfig)?;
let kind = q
.kind
.as_deref()
.map(str::parse::<ChangeKind>)
.transpose()
.map_err(ServeError::BadConfig)?;
let filter = ChangeListFilter {
status,
kind,
requester: q.requester,
tenant: actor.tenant_filter(q.tenant)?,
limit: q.limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT),
};
Ok(Json(changes::list(&state, &filter).await?))
}
async fn visible_change(
state: &ServerState,
actor: &AuthContext,
id: &str,
) -> Result<ChangeRequest, ServeError> {
let change = changes::get(state, id).await?;
if !actor.sees_tenant(change.tenant.as_deref()) {
return Err(ServeError::NotFound);
}
Ok(change)
}
pub async fn get_change(
State(state): State<ServerState>,
Extension(actor): Extension<AuthContext>,
Path(id): Path<String>,
) -> Result<Json<ChangeRequest>, ServeError> {
Ok(Json(visible_change(&state, &actor, &id).await?))
}
#[derive(Debug, Default, Deserialize)]
pub struct ApproveBody {
#[serde(default)]
pub comment: Option<String>,
}
pub async fn approve_change(
State(state): State<ServerState>,
Extension(actor): Extension<AuthContext>,
Path(id): Path<String>,
body: Option<Json<ApproveBody>>,
) -> Result<Json<ChangeRequest>, ServeError> {
let comment = body.and_then(|b| b.0.comment);
visible_change(&state, &actor, &id).await?;
Ok(Json(changes::approve(&state, &actor, &id, comment).await?))
}
#[derive(Debug, Deserialize)]
pub struct RejectBody {
pub reason: String,
}
pub async fn reject_change(
State(state): State<ServerState>,
Extension(actor): Extension<AuthContext>,
Path(id): Path<String>,
Json(body): Json<RejectBody>,
) -> Result<Json<ChangeRequest>, ServeError> {
if body.reason.trim().is_empty() {
return Err(ServeError::BadConfig("a rejection needs a reason".into()));
}
visible_change(&state, &actor, &id).await?;
Ok(Json(
changes::reject(&state, &actor, &id, body.reason).await?,
))
}