aboutsummaryrefslogtreecommitdiff
path: root/server/src/routes
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
parent246fcc704621d7c9626c990ded29b82abab47c8b (diff)
downloadjellything-7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed.tar
jellything-7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed.tar.bz2
jellything-7b0cdc8edec53f2b084ef28a8d6a537f1ebdd9ed.tar.zst
in-browser server log
Diffstat (limited to 'server/src/routes')
-rw-r--r--server/src/routes/mod.rs3
-rw-r--r--server/src/routes/ui/account/session/guard.rs56
-rw-r--r--server/src/routes/ui/account/session/mod.rs2
-rw-r--r--server/src/routes/ui/admin/log.rs216
-rw-r--r--server/src/routes/ui/admin/mod.rs38
-rw-r--r--server/src/routes/ui/style/layout.css11
6 files changed, 279 insertions, 47 deletions
diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs
index 1490290..38d0390 100644
--- a/server/src/routes/mod.rs
+++ b/server/src/routes/mod.rs
@@ -22,7 +22,7 @@ use ui::{
settings::{r_account_settings, r_account_settings_post},
},
admin::{
- r_admin_dashboard, r_admin_import, r_admin_invite, r_admin_remove_invite,
+ log::r_admin_log, r_admin_dashboard, r_admin_import, r_admin_invite, r_admin_remove_invite,
r_admin_remove_user,
},
assets::r_item_assets,
@@ -104,6 +104,7 @@ pub fn build_rocket(
r_admin_remove_user,
r_admin_remove_invite,
r_admin_import,
+ r_admin_log,
r_account_settings,
r_account_settings_post,
r_api_version,
diff --git a/server/src/routes/ui/account/session/guard.rs b/server/src/routes/ui/account/session/guard.rs
index e2bc093..19d68ad 100644
--- a/server/src/routes/ui/account/session/guard.rs
+++ b/server/src/routes/ui/account/session/guard.rs
@@ -3,11 +3,13 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
-use super::Session;
+use super::{AdminSession, Session};
use crate::{database::Database, routes::ui::error::MyError};
use anyhow::anyhow;
use log::warn;
use rocket::{
+ async_trait,
+ http::Status,
outcome::Outcome,
request::{self, FromRequest},
Request, State,
@@ -40,31 +42,43 @@ impl Session {
}
}
+#[async_trait]
impl<'r> FromRequest<'r> for Session {
type Error = MyError;
+ async fn from_request<'life0>(
+ request: &'r Request<'life0>,
+ ) -> request::Outcome<Self, Self::Error> {
+ match Session::from_request_ut(request).await {
+ Ok(x) => Outcome::Success(x),
+ Err(e) => {
+ warn!("authentificated route rejected: {e:?}");
+ Outcome::Forward(())
+ }
+ }
+ }
+}
- fn from_request<'life0, 'async_trait>(
+#[async_trait]
+impl<'r> FromRequest<'r> for AdminSession {
+ type Error = MyError;
+ async fn from_request<'life0>(
request: &'r Request<'life0>,
- ) -> core::pin::Pin<
- Box<
- dyn core::future::Future<Output = request::Outcome<Self, Self::Error>>
- + core::marker::Send
- + 'async_trait,
- >,
- >
- where
- 'r: 'async_trait,
- 'life0: 'async_trait,
- Self: 'async_trait,
- {
- Box::pin(async move {
- match Self::from_request_ut(request).await {
- Ok(x) => Outcome::Success(x),
- Err(e) => {
- warn!("authentificated route rejected: {e:?}");
- Outcome::Forward(())
+ ) -> request::Outcome<Self, Self::Error> {
+ match Session::from_request_ut(request).await {
+ Ok(x) => {
+ if x.user.admin {
+ Outcome::Success(AdminSession(x))
+ } else {
+ Outcome::Failure((
+ Status::Unauthorized,
+ MyError(anyhow!("you are not an admin")),
+ ))
}
}
- })
+ Err(e) => {
+ warn!("authentificated route rejected: {e:?}");
+ Outcome::Forward(())
+ }
+ }
}
}
diff --git a/server/src/routes/ui/account/session/mod.rs b/server/src/routes/ui/account/session/mod.rs
index 2a7908f..89592c3 100644
--- a/server/src/routes/ui/account/session/mod.rs
+++ b/server/src/routes/ui/account/session/mod.rs
@@ -15,6 +15,8 @@ pub struct Session {
pub user: User,
}
+pub struct AdminSession(pub Session);
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionData {
username: String,
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(
diff --git a/server/src/routes/ui/style/layout.css b/server/src/routes/ui/style/layout.css
index 09da718..e62e59a 100644
--- a/server/src/routes/ui/style/layout.css
+++ b/server/src/routes/ui/style/layout.css
@@ -33,12 +33,13 @@
* {
color: var(--font);
- font-family: "Cantarell", sans-serif;
- font-weight: 500;
-
scrollbar-width: thin;
scrollbar-color: var(--background-light) #0000;
}
+:root {
+ font-family: "Cantarell", sans-serif;
+ font-weight: 500;
+}
body {
background-color: var(--background-dark);
@@ -68,6 +69,10 @@ nav {
align-items: center;
}
+code {
+ font-family: monospace !important;
+}
+
nav a {
border: 0px solid transparent;
border-radius: 5px;