umbral-admin 0.0.12

Auto-generated CRUD admin UI for umbral models.
Documentation
//! `GET /admin/{table}/{id}/history` — audit timeline for one object.

use minijinja::context;
use umbral::web::{HeaderMap, IntoResponse, Path, Response, StatusCode};

use crate::AdminState;
use crate::auth::require_staff;
use crate::discovery::{find_model, user_theme};
use crate::engine::render;
use crate::error::AdminError;
use crate::models;
use crate::view::sidebar_apps;
use axum::extract::State;

/// `GET /admin/{table}/{id}/history` — render the timeline page with
/// the 50 most recent audit entries for `(table, id)`.
pub(crate) async fn history_handler(
    State(state): State<AdminState>,
    headers: HeaderMap,
    Path((table, id)): Path<(String, String)>,
) -> Response {
    let path = format!(
        "{}/{table}/{id}/history",
        crate::branding::current().base_path
    );
    let user = match require_staff(&headers, &path).await {
        Ok(u) => u,
        Err(r) => return r,
    };
    let Some((plugin_name, model)) = find_model(&table) else {
        return AdminError::NotFound(format!("no model `{table}`")).into_response();
    };
    // WEB-7: the audit timeline exposes an object's change history ("updated
    // Product #5", "changed password on …"). Gate it on the same per-model
    // View permission the rest of the admin enforces, so a staff user without
    // `view_<model>` can't read the trail across the permission boundary.
    if let Err(r) =
        crate::permcheck::require(&user, &plugin_name, &table, crate::permcheck::Action::View).await
    {
        return r;
    }
    // gaps3 #59: no parse. This used to demand an `i64` and return a hard 400
    // ("invalid id") for EVERY row of a Uuid/String-keyed model — the object-history page
    // was unreachable, not merely empty.
    let entries = match models::audit_for_object(&table, &id, 50).await {
        Ok(e) => e,
        Err(e) => {
            tracing::error!(error = %e, "admin: audit_for_object failed");
            return (StatusCode::INTERNAL_SERVER_ERROR, "audit error").into_response();
        }
    };

    let apps = sidebar_apps(&state, &user).await;
    let initial_theme = user_theme(&user).await;
    match render(
        "admin/history.html",
        context!(
            model_name    => model.name.clone(),
            object_id     => id.clone(),
            entries       => entries,
            apps          => apps,
            active_table  => table,
            breadcrumbs   => Vec::<serde_json::Value>::new(),
            initial_theme => initial_theme,
        ),
    ) {
        Ok(html) => html.into_response(),
        Err(e) => e.into_response(),
    }
}