use axum::{ extract::{Query, Request, State}, response::{IntoResponse, Redirect, Response}, }; use maud::{PreEscaped, html}; use serde::Deserialize; use crate::{ MnemoState, error::CompositeError, logs::LogEntry, users::{User, auth::UserAuthenticate}, web::{components::nav::nav, icons, pages::base}, }; #[derive(Deserialize)] pub struct PageQuery { page: Option, } pub async fn page( State(state): State, Query(query): Query, req: Request, ) -> Result { let mut tx = state.pool.acquire().await?; let u = match User::authenticate(&mut *tx, req.headers()).await? { Some(u) => u, None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()), }; let page = query.page.unwrap_or(1).max(1); let per_page = 20; let offset = (page - 1) * per_page; let logs = LogEntry::get_chronological_offset(&mut *tx, 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; Ok(base( "Logs | Mnemosyne", html!( (nav(Some(&u), req.uri().path())) @if true {//let Ok(true) = u.has_permission(&mut *tx, Permission::BrowseServerLogs) { div class="max-w-4xl mx-auto px-2" { div class="my-4" { p class="flex items-center gap-2" { span class="text-neutral-500" {(PreEscaped(icons::CLIPBOARD_CLOCK))} 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" { span class="text-neutral-500 scale-[.8]" {(PreEscaped(ico))} (txt) } } @for (idx, log) in logs.iter().enumerate() { @let s = if idx % 2 == 0 {"background-color: #e5e5e50b"} else {""}; div class="p-2 font-light" style=(s) { (log.id.get_timestamp() .map(|ts| { let (secs, nanos) = ts.to_unix(); chrono::DateTime::from_timestamp(secs as i64, nanos) .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()) .unwrap_or_else(|| "invalid date".to_string()) }) .unwrap_or_else(|| "no timestamp".to_string())) } div class="p-2 font-light" style=(s) {(log.actor.handle)} div class="p-2 font-light" style=(s) {(log.data.get_humanreadable_payload())} } } div class="flex justify-between items-center my-4 text-neutral-400" { @if page > 1 { a href=(format!("/logs?page={}", (page - 1).max(1))) class="px-4 py-2 border border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-200/5 hover:bg-neutral-200/15 rounded" { "Previous" } } @else { div {} } span { "Page " (page) " of " (total_pages) } @if page < total_pages { a href=(format!("/logs?page={}", page + 1)) class="px-4 py-2 border border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-200/5 hover:bg-neutral-200/15 rounded" { "Next" } } @else { div {} } } } } @else { p class="text-center p-2" {"You must have permission to view logs."} } ), ) .into_response()) }