aboutsummaryrefslogtreecommitdiff
path: root/server/src/routes/ui/admin
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2023-08-05 22:25:25 +0200
committermetamuffin <metamuffin@disroot.org>2023-08-05 22:45:30 +0200
commit7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed (patch)
tree7dd547ece6eaa2d5e4c6f117b20db3ad5730b594 /server/src/routes/ui/admin
parent246fcc704621d7c9626c990ded29b82abab47c8b (diff)
downloadjellything-7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed.tar
jellything-7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed.tar.bz2
jellything-7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed.tar.zst
in-browser server log
Diffstat (limited to 'server/src/routes/ui/admin')
-rw-r--r--server/src/routes/ui/admin/log.rs216
-rw-r--r--server/src/routes/ui/admin/mod.rs38
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(