Skip to main content

adminx_audit/
resource.rs

1// adminx-audit/src/resource.rs
2//
3// The in-panel viewer for the audit log. Registered like any other resource, so
4// it inherits the existing list/view routes and templates from every web adapter
5// — no route or template changes in adminx-axum / adminx-actix.
6//
7// It is deliberately **append-only**: the log's value is that it cannot be
8// quietly rewritten by the same panel that produces it, so create / update /
9// delete are refused here even for an admin. Rows are written only by the
10// `Auditor` seam, never through this resource.
11
12use adminx_core::authz::Action;
13use adminx_core::error::CoreError;
14use adminx_core::filters::{FilterField, FilterOption};
15use adminx_core::request::ReqCtx;
16use adminx_core::resource::Resource;
17use adminx_core::response::ApiResponse;
18use async_trait::async_trait;
19use serde_json::Value;
20
21/// Read-only view over `adminx_audit_versions`.
22#[derive(Clone)]
23pub struct AuditVersionResource;
24
25/// The refusal returned by every mutating entry point. 405: the route exists,
26/// the verb does not apply to it.
27fn append_only() -> ApiResponse {
28    ApiResponse::json(
29        405,
30        serde_json::json!({
31            "success": false,
32            "message": "The audit log is append-only; entries cannot be created, \
33                        edited or deleted from the panel.",
34        }),
35    )
36}
37
38#[async_trait]
39impl Resource for AuditVersionResource {
40    fn resource_name(&self) -> &'static str {
41        "Audit Log"
42    }
43    fn base_path(&self) -> &'static str {
44        "adminx-audit-versions"
45    }
46    fn table_name(&self) -> &'static str {
47        crate::store::TABLE
48    }
49    fn clone_box(&self) -> Box<dyn Resource> {
50        Box::new(self.clone())
51    }
52    fn menu(&self) -> &'static str {
53        "Audit Log"
54    }
55
56    /// Nothing is writable. This also means `filter_writable` can never admit a
57    /// column, so even if a mutating override were removed the defaults would
58    /// reject the body rather than insert a forged entry.
59    fn permit_keys(&self) -> Vec<&'static str> {
60        vec![]
61    }
62
63    fn filterable_fields(&self) -> Vec<FilterField> {
64        vec![
65            FilterField::text("item_type", "Resource"),
66            FilterField::text("item_id", "Record ID"),
67            FilterField::select(
68                "event",
69                "Event",
70                vec![
71                    FilterOption::new("create", "Created"),
72                    FilterOption::new("update", "Updated"),
73                    FilterOption::new("delete", "Deleted"),
74                ],
75            ),
76            FilterField::text("whodunnit_email", "Changed by"),
77            FilterField::date_range("created_at", "When"),
78        ]
79    }
80
81    // ===== APPEND-ONLY =====
82    //
83    // Both the JSON API and the HTML form handlers funnel through these, so the
84    // panel offers no path to a write. `new_page` / `edit_page` are refused too,
85    // so the UI never renders a form that could not be submitted.
86
87    async fn create(&self, _ctx: &ReqCtx, _body: Value) -> ApiResponse {
88        append_only()
89    }
90
91    async fn update(&self, _ctx: &ReqCtx, _id: &str, _body: Value) -> ApiResponse {
92        append_only()
93    }
94
95    async fn delete(&self, _ctx: &ReqCtx, _id: &str) -> ApiResponse {
96        append_only()
97    }
98
99    async fn new_page(&self, ctx: &ReqCtx) -> ApiResponse {
100        if !self.authorize(ctx, Action::Read) {
101            return CoreError::Unauthorized.into();
102        }
103        append_only()
104    }
105
106    async fn edit_page(&self, ctx: &ReqCtx, _id: &str) -> ApiResponse {
107        if !self.authorize(ctx, Action::Read) {
108            return CoreError::Unauthorized.into();
109        }
110        append_only()
111    }
112}