diff --git a/src/logs.rs b/src/logs.rs index aaf92d0..9f395f2 100644 --- a/src/logs.rs +++ b/src/logs.rs @@ -1,9 +1,9 @@ use serde::{Deserialize, Serialize}; use sqlx::{PgConnection, Row}; -use strum::IntoStaticStr; +use strum::{EnumDiscriminants, IntoStaticStr, VariantNames}; use uuid::Uuid; -use crate::{database::DatabaseError, users::User}; +use crate::{database::DatabaseError, users::User, web::icons}; #[derive(Debug)] pub struct LogEntry { @@ -46,15 +46,26 @@ impl LogEntry { pub async fn get_chronological_offset( conn: &mut PgConnection, + action_type: Option, offset: i64, limit: i64, ) -> Result, DatabaseError> { - let rows = - sqlx::query("SELECT id, actor, payload FROM logs ORDER BY id DESC LIMIT $1 OFFSET $2") - .bind(limit) - .bind(offset) - .fetch_all(&mut *conn) - .await?; + let mut qstr = String::from("SELECT id, actor, payload FROM logs "); + if action_type.is_some() { + qstr += "WHERE actiontype = $3 " + } + qstr += "ORDER BY id DESC LIMIT $1 OFFSET $2"; + + let q = sqlx::query(&qstr).bind(limit).bind(offset); + + let rows = match action_type { + Some(at) => { + let atstr: &'static str = at.into(); + q.bind(atstr).fetch_all(&mut *conn).await? + } + None => q.fetch_all(&mut *conn).await?, + }; + let mut entries = Vec::new(); for row in rows { let payload: serde_json::Value = row.try_get("payload")?; @@ -69,10 +80,9 @@ impl LogEntry { } } -// #[derive(Debug, thiserror::Error)] -// pub enum LogError {} - -#[derive(Debug, IntoStaticStr, Serialize, Deserialize)] +#[derive(Debug, IntoStaticStr, Serialize, Deserialize, VariantNames, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr, Serialize, Deserialize))] +#[strum_discriminants(name(LogActionDiscriminant))] pub enum LogAction { Initialize, RegenInfradmin, @@ -191,3 +201,45 @@ impl LogAction { } } } + +impl LogActionDiscriminant { + pub fn human_readable(&self) -> &'static str { + use LogActionDiscriminant as LAD; + match self { + LAD::Initialize => "Mnemosyne Initialization", + LAD::RegenInfradmin => "Infradmin Regeneration", + LAD::CreateUser => "User Creation", + LAD::ManuallyChangeUsersPassword => "Another User's Password Change", + LAD::CreateTag => "Tag Creation", + LAD::RenameTag => "Tag Rename", + LAD::DeleteTag => "Tag Deletion", + LAD::CreatePerson => "Person Creation", + LAD::ChangeUserHandle => "User Handle Change", + LAD::AddPersonName => "Person Name Addition", + LAD::DeletePersonName => "Person Name Deletion", + LAD::SetPersonPrimaryName => "Person Primary Name Set", + LAD::CreateQuote => "Quote Creation", + LAD::ManuallyRevokeSession => "Manual Session Revocation", + } + } + pub fn icon(&self) -> &'static str { + use LogActionDiscriminant as LAD; + match self { + LAD::Initialize => icons::LINE_DOT_RIGHT_HORIZONTAL, + // LAD::RegenInfradmin => + // LAD::CreateUser => + // LAD::ManuallyChangeUsersPassword => + // LAD::CreateTag => + // LAD::RenameTag => + // LAD::DeleteTag => + // LAD::CreatePerson => + // LAD::ChangeUserHandle => + // LAD::AddPersonName => + // LAD::DeletePersonName => + // LAD::SetPersonPrimaryName => + // LAD::CreateQuote => + // LAD::ManuallyRevokeSession => + _ => icons::GIT_COMMIT_VERTICAL, + } + } +} diff --git a/src/web/icons/git-commit-vertical.svg b/src/web/icons/git-commit-vertical.svg new file mode 100644 index 0000000..f944d60 --- /dev/null +++ b/src/web/icons/git-commit-vertical.svg @@ -0,0 +1 @@ + diff --git a/src/web/icons/line-dot-right-horizontal.svg b/src/web/icons/line-dot-right-horizontal.svg new file mode 100644 index 0000000..bc5e3c6 --- /dev/null +++ b/src/web/icons/line-dot-right-horizontal.svg @@ -0,0 +1 @@ + diff --git a/src/web/icons/mod.rs b/src/web/icons/mod.rs index b7c47f3..a708e20 100644 --- a/src/web/icons/mod.rs +++ b/src/web/icons/mod.rs @@ -8,8 +8,10 @@ pub const CLOCK: &str = include_str!("clock.svg"); pub const CONTACT: &str = include_str!("contact.svg"); pub const EYE: &str = include_str!("eye.svg"); pub const FILE_IMAGE: &str = include_str!("file-image.svg"); +pub const GIT_COMMIT_VERTICAL: &str = include_str!("git-commit-vertical.svg"); pub const INFO: &str = include_str!("info.svg"); pub const LAYOUT_DASHBOARD: &str = include_str!("layout-dashboard.svg"); +pub const LINE_DOT_RIGHT_HORIZONTAL: &str = include_str!("line-dot-right-horizontal.svg"); pub const LOG_OUT: &str = include_str!("log-out.svg"); pub const MAP_PIN: &str = include_str!("map-pin.svg"); pub const PEN: &str = include_str!("pen.svg"); diff --git a/src/web/mod.rs b/src/web/mod.rs index 20931f1..9daa121 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -1,9 +1,9 @@ -use axum::{ Router, http::header, routing::get}; +use axum::{Router, http::header, routing::get}; use crate::MnemoState; mod components; -mod icons; +pub mod icons; mod pages; pub const CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/styles.css")); diff --git a/src/web/pages/logs.rs b/src/web/pages/logs.rs index 802ebd6..26d76ec 100644 --- a/src/web/pages/logs.rs +++ b/src/web/pages/logs.rs @@ -8,19 +8,20 @@ use serde::Deserialize; use crate::{ MnemoState, error::CompositeError, - logs::LogEntry, + logs::{LogActionDiscriminant, LogEntry}, users::{User, auth::UserAuthenticate}, web::{components::nav::nav, icons, pages::base}, }; #[derive(Deserialize)] -pub struct PageQuery { +pub struct LogsPageQuery { page: Option, + action: Option, } pub async fn page( State(state): State, - Query(query): Query, + Query(query): Query, req: Request, ) -> Result { let mut tx = state.pool.acquire().await?; @@ -33,7 +34,7 @@ pub async fn page( let per_page = 20; let offset = (page - 1) * per_page; - let logs = LogEntry::get_chronological_offset(&mut *tx, offset, per_page).await?; + let logs = LogEntry::get_chronological_offset(&mut *tx, query.action, offset, per_page).await?; let total_logs = LogEntry::total_count(&mut *tx).await?; let total_pages = (total_logs as f64 / per_page as f64).ceil() as i64; @@ -50,6 +51,7 @@ pub async fn page( span class="text-2xl font-semibold font-lora" {"Logs"} } } + div class="w-full border border-neutral-200/25 rounded grid grid-cols-[auto_auto_1fr]" { @for (txt, ico) in [("Timestamp", icons::CLOCK), ("Actor", icons::USER), ("Action", icons::PEN)] { div class="p-2 flex gap-1 font-semibold border-b border-neutral-200/25" {