rustlavel_audit/plugin.rs
1//! Attaching the package to an application, and reaching it from a handler.
2//!
3//! ```ignore
4//! App::new().plugin(Audit::new(db.clone()))
5//! ```
6//!
7//! The migrations are not registered from here: they belong in the registry
8//! the CLI generates, next to the application's own. Add them with
9//! `registry.extend(rustlavel_audit::migrations());`
10
11use crate::entry::{Builder, Entry};
12use crate::store::Trail;
13use rustlavel_auth::guard::AuthExt;
14use rustlavel_db::Database;
15use rustlavel_http::Request;
16use rustlavel_http::plugin::{Plugin, Setup};
17
18/// Registers the [`Trail`] into application state.
19pub struct Audit {
20 trail: Trail,
21}
22
23impl Audit {
24 pub fn new(db: Database) -> Audit {
25 Audit { trail: Trail::new(db) }
26 }
27
28 pub fn with_trail(trail: Trail) -> Audit {
29 Audit { trail }
30 }
31
32 pub fn trail(&self) -> &Trail {
33 &self.trail
34 }
35}
36
37impl Plugin for Audit {
38 fn name(&self) -> &'static str {
39 "audit"
40 }
41
42 fn register(self: Box<Self>, setup: &mut Setup<'_>) {
43 setup.state(self.trail.clone());
44 }
45}
46
47/// `req.audit(...)` — an entry with the request's context already on it.
48pub trait AuditExt {
49 /// The trail, or `None` when the plugin is not registered.
50 fn trail(&self) -> Option<&Trail>;
51
52 /// Begin an entry, with the actor's id, the address and the user agent
53 /// filled in from this request.
54 ///
55 /// The actor's *name* is not, and cannot be: the framework's `Identity`
56 /// holds an id and nothing else, on purpose. The caller adds it with
57 /// [`Builder::by`], from whatever it is already holding.
58 ///
59 /// Returns `None` rather than a silent no-op builder when there is no
60 /// trail: a caller that meant to record something should find out that it
61 /// did not, and `let Some(audit) = req.audit(..)` is where they find out.
62 fn audit(&self, event: &str) -> Option<Builder>;
63}
64
65impl AuditExt for Request {
66 fn trail(&self) -> Option<&Trail> {
67 self.state::<Trail>()
68 }
69
70 fn audit(&self, event: &str) -> Option<Builder> {
71 let trail = self.state::<Trail>()?;
72
73 let mut entry = Entry::new(event);
74 // Read off the request rather than passed in. An entry that says
75 // "someone updated the settings" answers nothing, and a caller in a
76 // hurry is exactly who writes that entry.
77 if let Some(identity) = self.identity() {
78 entry.user_id = identity.id_as::<i64>();
79 }
80 entry.ip_address = self.ip();
81 entry.user_agent = self.header("user-agent").map(|agent| agent.to_string());
82
83 Some(Builder { trail: trail.clone(), entry })
84 }
85}