use axum::{extract::State, http::StatusCode, Extension, Json};
use gradatum_acl_policy::{AclDecision, AclOp};
use gradatum_core::error::GradatumError;
use gradatum_core::trust::TrustContext;
use crate::state::AppState;
pub use gradatum_dto::{
VaultDiffRequest, VaultDiffResponse, VaultHistoryGetRequest, VaultHistoryGetResponse,
VaultHistoryRequest, VaultHistoryResponse, VaultRestoreRequest, VaultRestoreResponse,
};
fn locus_for_tenant(tenant_id: &str) -> String {
format!("{}/main", tenant_id)
}
fn map_err_to_status(e: &GradatumError) -> StatusCode {
match e {
GradatumError::NoteNotFound(_) => StatusCode::NOT_FOUND,
GradatumError::Storage(msg) if msg.contains("introuvable") || msg.contains("Not found") => {
StatusCode::NOT_FOUND
}
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub async fn vault_history(
State(state): State<AppState>,
Extension(trust): Extension<TrustContext>,
Json(req): Json<VaultHistoryRequest>,
) -> Result<Json<VaultHistoryResponse>, StatusCode> {
if !trust.is_authenticated() {
return Err(StatusCode::UNAUTHORIZED);
}
let locus = locus_for_tenant(&req.tenant_id);
if state.acl.evaluate(&trust, AclOp::Read, &locus) != AclDecision::Allow {
return Err(StatusCode::FORBIDDEN);
}
let versions = state
.vault
.history_versions(&req.note_id)
.await
.map_err(|e| {
tracing::error!(
err = %e,
note_id = %req.note_id,
"vault_history: history_versions failed"
);
map_err_to_status(&e)
})?;
let count = versions.len();
Ok(Json(VaultHistoryResponse { versions, count }))
}
pub async fn vault_history_get(
State(state): State<AppState>,
Extension(trust): Extension<TrustContext>,
Json(req): Json<VaultHistoryGetRequest>,
) -> Result<Json<VaultHistoryGetResponse>, StatusCode> {
if !trust.is_authenticated() {
return Err(StatusCode::UNAUTHORIZED);
}
let locus = locus_for_tenant(&req.tenant_id);
if state.acl.evaluate(&trust, AclOp::Read, &locus) != AclDecision::Allow {
return Err(StatusCode::FORBIDDEN);
}
let snapshot = state
.vault
.history_get(&req.note_id, req.ts_ms)
.await
.map_err(|e| {
if !matches!(e, GradatumError::NoteNotFound(_)) {
tracing::error!(
err = %e,
note_id = %req.note_id,
ts_ms = req.ts_ms,
"vault_history_get: history_get failed"
);
}
map_err_to_status(&e)
})?;
Ok(Json(VaultHistoryGetResponse {
note_id: req.note_id,
ts_ms: req.ts_ms,
body: snapshot.body.markdown,
section: snapshot.frontmatter.section.to_string(),
}))
}
pub async fn vault_restore(
State(state): State<AppState>,
Extension(trust): Extension<TrustContext>,
Json(req): Json<VaultRestoreRequest>,
) -> Result<Json<VaultRestoreResponse>, StatusCode> {
if !trust.is_authenticated() {
return Err(StatusCode::UNAUTHORIZED);
}
let locus = locus_for_tenant(&req.tenant_id);
if state.acl.evaluate(&trust, AclOp::Write, &locus) != AclDecision::Allow {
return Err(StatusCode::FORBIDDEN);
}
let content_hash = state
.vault
.history_restore(&req.note_id, req.ts_ms)
.await
.map_err(|e| {
if !matches!(e, GradatumError::NoteNotFound(_)) {
tracing::error!(
err = %e,
note_id = %req.note_id,
ts_ms = req.ts_ms,
"vault_restore: history_restore failed"
);
}
map_err_to_status(&e)
})?;
Ok(Json(VaultRestoreResponse {
note_id: req.note_id,
ts_ms: req.ts_ms,
content_hash,
}))
}
pub async fn vault_diff(
State(state): State<AppState>,
Extension(trust): Extension<TrustContext>,
Json(req): Json<VaultDiffRequest>,
) -> Result<Json<VaultDiffResponse>, StatusCode> {
if !trust.is_authenticated() {
return Err(StatusCode::UNAUTHORIZED);
}
let locus = locus_for_tenant(&req.tenant_id);
if state.acl.evaluate(&trust, AclOp::Read, &locus) != AclDecision::Allow {
return Err(StatusCode::FORBIDDEN);
}
let is_valid_selector = |s: &str| -> bool { s == "current" || s.parse::<i64>().is_ok() };
if !is_valid_selector(&req.a) || !is_valid_selector(&req.b) {
tracing::warn!(
a = %req.a,
b = %req.b,
note_id = %req.note_id,
"vault_diff: sélecteur invalide (attendu 'current' ou timestamp ms)"
);
return Err(StatusCode::BAD_REQUEST);
}
let lines = state
.vault
.history_diff(&req.note_id, &req.a, &req.b)
.await
.map_err(|e| {
if !matches!(e, GradatumError::NoteNotFound(_)) {
let status = map_err_to_status(&e);
if status == StatusCode::INTERNAL_SERVER_ERROR {
tracing::error!(
err = %e,
note_id = %req.note_id,
a = %req.a,
b = %req.b,
"vault_diff: history_diff failed"
);
}
}
map_err_to_status(&e)
})?;
let count = lines.len();
Ok(Json(VaultDiffResponse { lines, count }))
}