diff options
Diffstat (limited to 'server/src/routes/ui/admin')
-rw-r--r-- | server/src/routes/ui/admin/log.rs | 216 | ||||
-rw-r--r-- | server/src/routes/ui/admin/mod.rs | 38 |
2 files changed, 232 insertions, 22 deletions
diff --git a/server/src/routes/ui/admin/log.rs b/server/src/routes/ui/admin/log.rs new file mode 100644 index 0000000..e264e8f --- /dev/null +++ b/server/src/routes/ui/admin/log.rs @@ -0,0 +1,216 @@ +use crate::{ + routes::ui::{ + account::session::AdminSession, + error::MyResult, + layout::{DynLayoutPage, LayoutPage}, + }, + uri, +}; +use log::Level; +use rocket::get; +use std::{ + collections::VecDeque, + fmt::Write, + sync::{LazyLock, RwLock}, +}; + +const MAX_LOG_LEN: usize = 4000; + +static LOGGER: LazyLock<Log> = LazyLock::new(Log::new); + +pub fn enable_logging() { + log::set_logger(&*LOGGER).unwrap(); + log::set_max_level(log::LevelFilter::Debug); +} + +pub struct Log { + inner: env_logger::Logger, + log: RwLock<(VecDeque<LogLine>, VecDeque<LogLine>)>, +} + +pub struct LogLine { + module: Option<&'static str>, + level: Level, + message: String, +} + +#[get("/admin/log?<warnonly>")] +pub fn r_admin_log<'a>(_session: AdminSession, warnonly: bool) -> MyResult<DynLayoutPage<'a>> { + Ok(LayoutPage { + title: "Log".into(), + content: markup::new! { + h1 { "Server Log" } + a[href=uri!(r_admin_log(!warnonly))] { @if warnonly { "Show everything" } else { "Show only warnings" }} + code.log { + @let g = LOGGER.log.read().unwrap(); + table { @for e in if warnonly { g.1.iter() } else { g.0.iter() } { + tr[class=format!("level-{:?}", e.level).to_ascii_lowercase()] { + td { @format_level(e.level) } + td { @e.module } + td { @markup::raw(vt100_to_html(&e.message)) } + } + }} + } + }, + ..Default::default() + }) +} + +impl Log { + pub fn new() -> Self { + Self { + inner: env_logger::builder() + .filter_level(log::LevelFilter::Warn) + .parse_env("LOG") + .build(), + log: Default::default(), + } + } + fn should_log(&self, metadata: &log::Metadata) -> bool { + let level = metadata.level(); + level + <= match metadata.target() { + x if x.starts_with("jellything::") => Level::Debug, + x if x.starts_with("rocket::") => Level::Info, + _ => Level::Warn, + } + } + fn do_log(&self, record: &log::Record) { + let mut w = self.log.write().unwrap(); + w.0.push_back(LogLine { + module: record.module_path_static(), + level: record.level(), + message: record.args().to_string(), + }); + while w.0.len() > MAX_LOG_LEN { + w.0.pop_front(); + } + if record.level() <= Level::Warn { + w.1.push_back(LogLine { + module: record.module_path_static(), + level: record.level(), + message: record.args().to_string(), + }); + while w.1.len() > MAX_LOG_LEN { + w.1.pop_front(); + } + } + } +} + +impl log::Log for Log { + fn enabled(&self, metadata: &log::Metadata) -> bool { + self.inner.enabled(metadata) || self.should_log(metadata) + } + fn log(&self, record: &log::Record) { + match (record.module_path_static(), record.line()) { + // TODO is there a better way to ignore those? + (Some("rocket::rocket"), Some(670)) => return, + (Some("rocket::server"), Some(401)) => return, + _ => {} + } + if self.inner.enabled(record.metadata()) { + self.inner.log(record); + } + if self.should_log(record.metadata()) { + self.do_log(record) + } + } + fn flush(&self) { + self.inner.flush(); + } +} + +fn vt100_to_html(s: &str) -> String { + let mut out = HtmlOut::default(); + let mut st = vte::Parser::new(); + for b in s.bytes() { + st.advance(&mut out, b); + } + out.s +} + +fn format_level(level: Level) -> impl markup::Render { + let (s, c) = match level { + Level::Debug => ("DEBUG", "blue"), + Level::Error => ("ERROR", "red"), + Level::Warn => ("WARN", "yellow"), + Level::Info => ("INFO", "green"), + Level::Trace => ("TRACE", "lightblue"), + }; + markup::new! { span[style=format!("color:{c}")] {@s} } +} + +pub struct HtmlOut { + s: String, + color: bool, +} +impl Default for HtmlOut { + fn default() -> Self { + Self { + color: false, + s: String::new(), + } + } +} +impl HtmlOut { + pub fn set_color(&mut self, [r, g, b]: [u8; 3]) { + self.reset_color(); + self.color = true; + write!(self.s, "<span style=color:#{:02x}{:02x}{:02x}>", r, g, b).unwrap() + } + pub fn reset_color(&mut self) { + if self.color { + write!(self.s, "</span>").unwrap(); + self.color = false; + } + } +} +impl vte::Perform for HtmlOut { + fn print(&mut self, c: char) { + match c { + 'a'..='z' | 'A'..='Z' | '0'..='9' | ' ' => self.s.push(c), + x => write!(self.s, "&#{};", x as u32).unwrap(), + } + } + + fn execute(&mut self, _byte: u8) {} + fn hook(&mut self, _params: &vte::Params, _i: &[u8], _ignore: bool, _a: char) {} + fn put(&mut self, _byte: u8) {} + fn unhook(&mut self) {} + fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {} + fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) { + // self.s += &format!(" {:?} ", (_intermediates, _byte)) + } + + fn csi_dispatch( + &mut self, + params: &vte::Params, + _intermediates: &[u8], + _ignore: bool, + action: char, + ) { + let mut k = params.iter(); + // self.s += &format!(" {:?} ", (params, action)); + match action { + 'm' => match k.next().unwrap_or(&[0]).get(0).unwrap_or(&0) { + c @ (30..=37 | 40..=47) => { + let c = if *c >= 40 { *c - 10 } else { *c }; + self.set_color(match c { + 30 => [0, 0, 0], + 31 => [255, 0, 0], + 32 => [0, 255, 0], + 33 => [255, 255, 0], + 34 => [0, 0, 255], + 35 => [255, 0, 255], + 36 => [0, 255, 255], + 37 => [255, 255, 255], + _ => unreachable!(), + }); + } + _ => (), + }, + _ => (), + } + } +} diff --git a/server/src/routes/ui/admin/mod.rs b/server/src/routes/ui/admin/mod.rs index 0775423..f779700 100644 --- a/server/src/routes/ui/admin/mod.rs +++ b/server/src/routes/ui/admin/mod.rs @@ -3,12 +3,15 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin <metamuffin.org> */ +pub mod log; + +use super::account::session::AdminSession; use crate::{ database::Database, federation::Federation, import::import, routes::ui::{ - account::session::Session, + admin::log::rocket_uri_macro_r_admin_log, error::MyResult, layout::{DynLayoutPage, FlashDisplay, LayoutPage}, }, @@ -21,12 +24,9 @@ use std::time::Instant; #[get("/admin/dashboard")] pub fn r_admin_dashboard( - session: Session, + _session: AdminSession, database: &State<Database>, ) -> MyResult<DynLayoutPage<'static>> { - if !session.user.admin { - Err(anyhow!("you not admin"))? - } admin_dashboard(database, None) } @@ -44,6 +44,10 @@ pub fn admin_dashboard<'a>( content: markup::new! { h1 { "Admin Panel" } @FlashDisplay { flash: flash.clone() } + ul { + li{a[href=uri!(r_admin_log(true))] { "Server Log (Warnings only)" }} + li{a[href=uri!(r_admin_log(false))] { "Server Log (Full) " }} + } h2 { "Library" } form[method="POST", action=uri!(r_admin_import())] { input[type="submit", value="(Re-)Import Library"]; @@ -76,13 +80,9 @@ pub fn admin_dashboard<'a>( #[post("/admin/generate_invite")] pub fn r_admin_invite( - session: Session, + _session: AdminSession, database: &State<Database>, ) -> MyResult<DynLayoutPage<'static>> { - if !session.user.admin { - Err(anyhow!("you not admin"))? - } - let i = format!("{}", rand::thread_rng().gen::<u128>()); database.invite.insert(&i, &())?; @@ -96,13 +96,11 @@ pub struct DeleteUser { #[post("/admin/remove_user", data = "<form>")] pub fn r_admin_remove_user( - session: Session, + session: AdminSession, database: &State<Database>, form: Form<DeleteUser>, ) -> MyResult<DynLayoutPage<'static>> { - if !session.user.admin { - Err(anyhow!("you not admin"))? - } + drop(session); database .user .remove(&form.name)? @@ -118,13 +116,11 @@ pub struct DeleteInvite { #[post("/admin/remove_invite", data = "<form>")] pub fn r_admin_remove_invite( - session: Session, + session: AdminSession, database: &State<Database>, form: Form<DeleteInvite>, ) -> MyResult<DynLayoutPage<'static>> { - if !session.user.admin { - Err(anyhow!("you not admin"))? - } + drop(session); database .invite .remove(&form.invite)? @@ -135,13 +131,11 @@ pub fn r_admin_remove_invite( #[post("/admin/import")] pub async fn r_admin_import( - session: Session, + session: AdminSession, database: &State<Database>, federation: &State<Federation>, ) -> MyResult<DynLayoutPage<'static>> { - if !session.user.admin { - Err(anyhow!("you not admin"))? - } + drop(session); let t = Instant::now(); let r = import(&database, &federation).await; admin_dashboard( |