diff options
Diffstat (limited to 'server/src/ui/admin')
-rw-r--r-- | server/src/ui/admin/log.rs | 253 | ||||
-rw-r--r-- | server/src/ui/admin/mod.rs | 283 | ||||
-rw-r--r-- | server/src/ui/admin/user.rs | 140 |
3 files changed, 228 insertions, 448 deletions
diff --git a/server/src/ui/admin/log.rs b/server/src/ui/admin/log.rs index dff6d1b..bd6d7af 100644 --- a/server/src/ui/admin/log.rs +++ b/server/src/ui/admin/log.rs @@ -3,256 +3,35 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{ - logic::session::AdminSession, - ui::{ - error::MyResult, - layout::{DynLayoutPage, LayoutPage}, - }, - uri, -}; -use chrono::{DateTime, Utc}; -use log::Level; -use markup::Render; -use rocket::get; +use crate::ui::error::MyResult; +use jellylogic::{admin::log::get_log_stream, session::AdminSession}; +use jellyui::admin::log::render_log_line; +use rocket::{get, response::content::RawHtml}; use rocket_ws::{Message, Stream, WebSocket}; use serde_json::json; -use std::{ - collections::VecDeque, - fmt::Write, - sync::{Arc, LazyLock, RwLock}, -}; -use tokio::sync::broadcast; - -const MAX_LOG_LEN: usize = 4096; - -static LOGGER: LazyLock<Log> = LazyLock::new(Log::default); - -pub fn enable_logging() { - log::set_logger(&*LOGGER).unwrap(); - log::set_max_level(log::LevelFilter::Debug); -} - -type LogBuffer = VecDeque<Arc<LogLine>>; - -pub struct Log { - inner: env_logger::Logger, - stream: ( - broadcast::Sender<Arc<LogLine>>, - broadcast::Sender<Arc<LogLine>>, - ), - log: RwLock<(LogBuffer, LogBuffer)>, -} - -pub struct LogLine { - time: DateTime<Utc>, - module: Option<&'static str>, - level: Level, - message: String, -} #[get("/admin/log?<warnonly>", rank = 2)] -pub fn r_admin_log<'a>(_session: AdminSession, warnonly: bool) -> MyResult<DynLayoutPage<'a>> { - Ok(LayoutPage { - title: "Log".into(), - class: Some("admin_log"), - content: markup::new! { - h1 { "Server Log" } - a[href=uri!(r_admin_log(!warnonly))] { @if warnonly { "Show everything" } else { "Show only warnings" }} - code.log[id="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.time { @e.time.to_rfc3339() } - td.loglevel { @format_level(e.level) } - td.module { @e.module } - td { @markup::raw(vt100_to_html(&e.message)) } - } - }} - } - }, - }) -} +pub fn r_admin_log<'a>(_session: AdminSession, warnonly: bool) -> MyResult<RawHtml<String>> {} -#[get("/admin/log?stream&<warnonly>", rank = 1)] +#[get("/admin/log?stream&<warnonly>&<html>", rank = 1)] pub fn r_admin_log_stream( _session: AdminSession, ws: WebSocket, warnonly: bool, + html: bool, ) -> Stream!['static] { - let mut stream = if warnonly { - LOGGER.stream.1.subscribe() - } else { - LOGGER.stream.0.subscribe() - }; + let mut stream = get_log_stream(warnonly); Stream! { ws => - let _ = ws; - while let Ok(line) = stream.recv().await { - yield Message::Text(json!({ - "time": line.time, - "level_class": format!("level-{:?}", line.level).to_ascii_lowercase(), - "level_html": format_level_string(line.level), - "module": line.module, - "message": vt100_to_html(&line.message), - }).to_string()); - } - } -} - -impl Default for Log { - fn default() -> Self { - Self { - inner: env_logger::builder() - .filter_level(log::LevelFilter::Warn) - .parse_env("LOG") - .build(), - stream: ( - tokio::sync::broadcast::channel(1024).0, - tokio::sync::broadcast::channel(1024).0, - ), - log: Default::default(), - } - } -} -impl Log { - fn should_log(&self, metadata: &log::Metadata) -> bool { - let level = metadata.level(); - level - <= match metadata.target() { - x if x.starts_with("jelly") => Level::Debug, - x if x.starts_with("rocket::") => Level::Info, - _ => Level::Warn, + if html { + let _ = ws; + while let Ok(line) = stream.recv().await { + yield Message::Text(render_log_line(&line)); } - } - fn do_log(&self, record: &log::Record) { - let time = Utc::now(); - let line = Arc::new(LogLine { - time, - module: record.module_path_static(), - level: record.level(), - message: record.args().to_string(), - }); - let mut w = self.log.write().unwrap(); - w.0.push_back(line.clone()); - let _ = self.stream.0.send(line.clone()); - while w.0.len() > MAX_LOG_LEN { - w.0.pop_front(); - } - if record.level() <= Level::Warn { - let _ = self.stream.1.send(line.clone()); - w.1.push_back(line); - while w.1.len() > MAX_LOG_LEN { - w.1.pop_front(); + } else { + let _ = ws; + while let Ok(line) = stream.recv().await { + yield Message::Text(json!(line).to_string()); } } } } - -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(); - st.advance(&mut out, s.as_bytes()); - 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} } -} -fn format_level_string(level: Level) -> String { - let mut w = String::new(); - format_level(level).render(&mut w).unwrap(); - w -} - -#[derive(Default)] -pub struct HtmlOut { - s: String, - color: bool, -} -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) {} - fn csi_dispatch( - &mut self, - params: &vte::Params, - _intermediates: &[u8], - _ignore: bool, - action: char, - ) { - let mut k = params.iter(); - #[allow(clippy::single_match)] - match action { - 'm' => match k.next().unwrap_or(&[0]).first().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/ui/admin/mod.rs b/server/src/ui/admin/mod.rs index d380ae2..b155121 100644 --- a/server/src/ui/admin/mod.rs +++ b/server/src/ui/admin/mod.rs @@ -10,108 +10,74 @@ use super::{ assets::{resolve_asset, AVIF_QUALITY, AVIF_SPEED}, error::MyResult, }; -use crate::{database::Database, logic::session::AdminSession}; +use crate::{database::Database, locale::AcceptLanguage}; use anyhow::{anyhow, Context}; use jellybase::{assetfed::AssetInner, federation::Federation, CONF}; -use jellyimport::{import_wrap, IMPORT_ERRORS}; +use jellycommon::routes::u_admin_dashboard; +use jellyimport::{import_wrap, is_importing, IMPORT_ERRORS}; +use jellylogic::session::AdminSession; +use jellyui::{ + admin::AdminDashboardPage, + render_page, + scaffold::{RenderInfo, SessionInfo}, +}; use rand::Rng; -use rocket::{form::Form, get, post, FromForm, State}; +use rocket::{ + form::Form, + get, post, + response::{content::RawHtml, Redirect}, + FromForm, State, +}; use std::time::Instant; use tokio::{sync::Semaphore, task::spawn_blocking}; #[get("/admin/dashboard")] pub async fn r_admin_dashboard( - _session: AdminSession, + session: AdminSession, database: &State<Database>, -) -> MyResult<DynLayoutPage<'static>> { - admin_dashboard(database, None).await -} - -pub async fn admin_dashboard<'a>( - database: &Database, - flash: Option<MyResult<String>>, -) -> MyResult<DynLayoutPage<'a>> { + lang: AcceptLanguage, +) -> MyResult<RawHtml<String>> { + let AcceptLanguage(lang) = lang; let invites = database.list_invites()?; - let flash = flash.map(|f| f.map_err(|e| format!("{e:?}"))); + let flash = None; let last_import_err = IMPORT_ERRORS.read().await.to_owned(); - let database = database.to_owned(); - Ok(LayoutPage { - title: "Admin Dashboard".to_string(), - content: markup::new! { - h1 { "Admin Panel" } - @FlashDisplay { flash: flash.clone() } - @if !last_import_err.is_empty() { - section.message.error { - details { - summary { p.error { @format!("The last import resulted in {} errors:", last_import_err.len()) } } - ol { @for e in &last_import_err { - li.error { pre.error { @e } } - }} - } - } - } - 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" } - @if is_importing() { - section.message { p.warn { "An import is currently running." } } - } - @if is_transcoding() { - section.message { p.warn { "Currently transcoding posters." } } - } - form[method="POST", action=uri!(r_admin_import(true))] { - input[type="submit", disabled=is_importing(), value="Start incremental import"]; - } - form[method="POST", action=uri!(r_admin_import(false))] { - input[type="submit", disabled=is_importing(), value="Start full import"]; - } - form[method="POST", action=uri!(r_admin_transcode_posters())] { - input[type="submit", disabled=is_transcoding(), value="Transcode all posters with low resolution"]; - } - form[method="POST", action=uri!(r_admin_update_search())] { - input[type="submit", value="Regenerate full-text search index"]; - } - form[method="POST", action=uri!(r_admin_delete_cache())] { - input.danger[type="submit", value="Delete Cache"]; - } - h2 { "Users" } - p { a[href=uri!(r_admin_users())] "Manage Users" } - h2 { "Invitations" } - form[method="POST", action=uri!(r_admin_invite())] { - input[type="submit", value="Generate new invite code"]; - } - ul { @for t in &invites { - li { - form[method="POST", action=uri!(r_admin_remove_invite())] { - span { @t } - input[type="text", name="invite", value=&t, hidden]; - input[type="submit", value="Invalidate"]; - } - } - }} + let busy = if is_importing() { + Some("An import is currently running.") + } else if is_transcoding() { + Some("Currently transcoding posters.") + } else { + None + }; - h2 { "Database" } - @match db_stats(&database) { - Ok(s) => { @s } - Err(e) => { pre.error { @format!("{e:?}") } } - } + Ok(RawHtml(render_page( + &AdminDashboardPage { + busy, + last_import_err: &last_import_err, + invites: &invites, + flash, + lang: &lang, }, - ..Default::default() - }) + RenderInfo { + importing: is_importing(), + session: Some(SessionInfo { + user: session.0.user, + }), + }, + lang, + ))) } #[post("/admin/generate_invite")] pub async fn r_admin_invite( _session: AdminSession, database: &State<Database>, -) -> MyResult<DynLayoutPage<'static>> { +) -> MyResult<Redirect> { let i = format!("{}", rand::rng().random::<u128>()); database.create_invite(&i)?; - admin_dashboard(database, Some(Ok(format!("Invite: {}", i)))).await + // admin_dashboard(database, Some(Ok(format!("Invite: {}", i)))).await + Ok(Redirect::temporary(u_admin_dashboard())) } #[derive(FromForm)] @@ -124,12 +90,13 @@ pub async fn r_admin_remove_invite( session: AdminSession, database: &State<Database>, form: Form<DeleteInvite>, -) -> MyResult<DynLayoutPage<'static>> { +) -> MyResult<Redirect> { drop(session); if !database.delete_invite(&form.invite)? { Err(anyhow!("invite does not exist"))?; }; - admin_dashboard(database, Some(Ok("Invite invalidated".into()))).await + // admin_dashboard(database, Some(Ok("Invite invalidated".into()))).await + Ok(Redirect::temporary(u_admin_dashboard())) } #[post("/admin/import?<incremental>")] @@ -138,55 +105,58 @@ pub async fn r_admin_import( database: &State<Database>, _federation: &State<Federation>, incremental: bool, -) -> MyResult<DynLayoutPage<'static>> { +) -> MyResult<Redirect> { drop(session); let t = Instant::now(); if !incremental { database.clear_nodes()?; } let r = import_wrap((*database).clone(), incremental).await; - let flash = r - .map_err(|e| e.into()) - .map(|_| format!("Import successful; took {:?}", t.elapsed())); - admin_dashboard(database, Some(flash)).await + // let flash = r + // .map_err(|e| e.into()) + // .map(|_| format!("Import successful; took {:?}", t.elapsed())); + // admin_dashboard(database, Some(flash)).await + Ok(Redirect::temporary(u_admin_dashboard())) } #[post("/admin/update_search")] pub async fn r_admin_update_search( _session: AdminSession, database: &State<Database>, -) -> MyResult<DynLayoutPage<'static>> { +) -> MyResult<Redirect> { let db2 = (*database).clone(); let r = spawn_blocking(move || db2.search_create_index()) .await .unwrap(); - admin_dashboard( - database, - Some( - r.map_err(|e| e.into()) - .map(|_| "Search index updated".to_string()), - ), - ) - .await + // admin_dashboard( + // database, + // Some( + // r.map_err(|e| e.into()) + // .map(|_| "Search index updated".to_string()), + // ), + // ) + // .await + Ok(Redirect::temporary(u_admin_dashboard())) } #[post("/admin/delete_cache")] pub async fn r_admin_delete_cache( session: AdminSession, database: &State<Database>, -) -> MyResult<DynLayoutPage<'static>> { +) -> MyResult<Redirect> { drop(session); let t = Instant::now(); let r = tokio::fs::remove_dir_all(&CONF.cache_path).await; tokio::fs::create_dir(&CONF.cache_path).await?; - admin_dashboard( - database, - Some( - r.map_err(|e| e.into()) - .map(|_| format!("Cache deleted; took {:?}", t.elapsed())), - ), - ) - .await + // admin_dashboard( + // database, + // Some( + // r.map_err(|e| e.into()) + // .map(|_| format!("Cache deleted; took {:?}", t.elapsed())), + // ), + // ) + // .await + Ok(Redirect::temporary(u_admin_dashboard())) } static SEM_TRANSCODING: Semaphore = Semaphore::const_new(1); @@ -198,7 +168,7 @@ fn is_transcoding() -> bool { pub async fn r_admin_transcode_posters( session: AdminSession, database: &State<Database>, -) -> MyResult<DynLayoutPage<'static>> { +) -> MyResult<Redirect> { drop(session); let _permit = SEM_TRANSCODING .try_acquire() @@ -223,58 +193,59 @@ pub async fn r_admin_transcode_posters( } drop(_permit); - admin_dashboard( - database, - Some(Ok(format!( - "All posters pre-transcoded; took {:?}", - t.elapsed() - ))), - ) - .await + // admin_dashboard( + // database, + // Some(Ok(format!( + // "All posters pre-transcoded; took {:?}", + // t.elapsed() + // ))), + // ) + // .await + Ok(Redirect::temporary(u_admin_dashboard())) } -fn db_stats(_db: &Database) -> anyhow::Result<DynRender> { - // TODO - // let txn = db.inner.begin_read()?; - // let stats = [ - // ("node", txn.open_table(T_NODE)?.stats()?), - // ("user", txn.open_table(T_USER_NODE)?.stats()?), - // ("user-node", txn.open_table(T_USER_NODE)?.stats()?), - // ("invite", txn.open_table(T_INVITE)?.stats()?), - // ]; +// fn db_stats(_db: &Database) -> anyhow::Result<DynRender> { +// // TODO +// // let txn = db.inner.begin_read()?; +// // let stats = [ +// // ("node", txn.open_table(T_NODE)?.stats()?), +// // ("user", txn.open_table(T_USER_NODE)?.stats()?), +// // ("user-node", txn.open_table(T_USER_NODE)?.stats()?), +// // ("invite", txn.open_table(T_INVITE)?.stats()?), +// // ]; - // let cache_stats = db.node_index.reader.searcher().doc_store_cache_stats(); - // let ft_total_docs = db.node_index.reader.searcher().total_num_docs()?; +// // let cache_stats = db.node_index.reader.searcher().doc_store_cache_stats(); +// // let ft_total_docs = db.node_index.reader.searcher().total_num_docs()?; - Ok(markup::new! { - // h3 { "Key-Value-Store Statistics" } - // table.border { - // tbody { - // tr { - // th { "table name" } - // th { "tree height" } - // th { "stored bytes" } - // th { "metadata bytes" } - // th { "fragmented bytes" } - // th { "branch pages" } - // th { "leaf pages" } - // } - // @for (name, stats) in &stats { tr { - // td { @name } - // td { @stats.tree_height() } - // td { @format_size(stats.stored_bytes(), DECIMAL) } - // td { @format_size(stats.metadata_bytes(), DECIMAL) } - // td { @format_size(stats.fragmented_bytes(), DECIMAL) } - // td { @stats.branch_pages() } - // td { @stats.leaf_pages() } - // }} - // } - // } - // h3 { "Search Engine Statistics" } - // ul { - // li { "Total documents: " @ft_total_docs } - // li { "Cache misses: " @cache_stats.cache_misses } - // li { "Cache hits: " @cache_stats.cache_hits } - // } - }) -} +// Ok(markup::new! { +// // h3 { "Key-Value-Store Statistics" } +// // table.border { +// // tbody { +// // tr { +// // th { "table name" } +// // th { "tree height" } +// // th { "stored bytes" } +// // th { "metadata bytes" } +// // th { "fragmented bytes" } +// // th { "branch pages" } +// // th { "leaf pages" } +// // } +// // @for (name, stats) in &stats { tr { +// // td { @name } +// // td { @stats.tree_height() } +// // td { @format_size(stats.stored_bytes(), DECIMAL) } +// // td { @format_size(stats.metadata_bytes(), DECIMAL) } +// // td { @format_size(stats.fragmented_bytes(), DECIMAL) } +// // td { @stats.branch_pages() } +// // td { @stats.leaf_pages() } +// // }} +// // } +// // } +// // h3 { "Search Engine Statistics" } +// // ul { +// // li { "Total documents: " @ft_total_docs } +// // li { "Cache misses: " @cache_stats.cache_misses } +// // li { "Cache hits: " @cache_stats.cache_hits } +// // } +// }) +// } diff --git a/server/src/ui/admin/user.rs b/server/src/ui/admin/user.rs index 1af83d4..77bcc71 100644 --- a/server/src/ui/admin/user.rs +++ b/server/src/ui/admin/user.rs @@ -3,68 +3,68 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{database::Database, ui::error::MyResult}; +use crate::{database::Database, locale::AcceptLanguage, ui::error::MyResult}; use anyhow::{anyhow, Context}; use jellycommon::user::UserPermission; -use jellylogic::session::AdminSession; -use rocket::{form::Form, get, post, FromForm, FromFormField, State}; +use jellyimport::is_importing; +use jellylogic::{admin::user::admin_users, session::AdminSession}; +use jellyui::{ + admin::user::{AdminUserPage, AdminUsersPage}, + render_page, + scaffold::{RenderInfo, SessionInfo}, +}; +use rocket::{form::Form, get, post, response::content::RawHtml, FromForm, FromFormField, State}; #[get("/admin/users")] pub fn r_admin_users( - _session: AdminSession, + session: AdminSession, database: &State<Database>, -) -> MyResult<DynLayoutPage<'static>> { - user_management(database, None) -} - -fn user_management<'a>( - database: &Database, - flash: Option<MyResult<String>>, -) -> MyResult<DynLayoutPage<'a>> { - // TODO this doesnt scale, pagination! - let users = database.list_users()?; - let flash = flash.map(|f| f.map_err(|e| format!("{e:?}"))); - - Ok(LayoutPage { - title: "User management".to_string(), - content: markup::new! { - h1 { "User Management" } - @FlashDisplay { flash: flash.clone() } - h2 { "All Users" } - ul { @for u in &users { - li { - a[href=uri!(r_admin_user(&u.name))] { @format!("{:?}", u.display_name) " (" @u.name ")" } - } - }} + lang: AcceptLanguage, +) -> MyResult<RawHtml<String>> { + let AcceptLanguage(lang) = lang; + let r = admin_users(database, &session)?; + Ok(RawHtml(render_page( + &AdminUsersPage { + flash: None, + lang: &lang, + users: &r.users, + }, + RenderInfo { + importing: is_importing(), + session: Some(SessionInfo { + user: session.0.user, + }), }, - ..Default::default() - }) + lang, + ))) } #[get("/admin/user/<name>")] pub fn r_admin_user<'a>( - _session: AdminSession, + session: AdminSession, database: &State<Database>, name: &'a str, -) -> MyResult<DynLayoutPage<'a>> { - manage_single_user(database, None, name.to_string()) -} - -fn manage_single_user<'a>( - database: &Database, - flash: Option<MyResult<String>>, - name: String, -) -> MyResult<DynLayoutPage<'a>> { + lang: AcceptLanguage, +) -> MyResult<RawHtml<String>> { + let AcceptLanguage(lang) = lang; let user = database .get_user(&name)? .ok_or(anyhow!("user does not exist"))?; - let flash = flash.map(|f| f.map_err(|e| format!("{e:?}"))); - Ok(LayoutPage { - title: "User management".to_string(), - content: markup::new! {}, - ..Default::default() - }) + Ok(RawHtml(render_page( + &AdminUserPage { + flash: None, + lang: &lang, + user: &user, + }, + RenderInfo { + importing: is_importing(), + session: Some(SessionInfo { + user: session.0.user, + }), + }, + lang, + ))) } #[derive(FromForm)] @@ -86,8 +86,9 @@ pub fn r_admin_user_permission( database: &State<Database>, form: Form<UserPermissionForm>, name: &str, -) -> MyResult<DynLayoutPage<'static>> { - drop(session); + lang: AcceptLanguage, +) -> MyResult<RawHtml<String>> { + let AcceptLanguage(lang) = lang; let perm = serde_json::from_str::<UserPermission>(&form.permission) .context("parsing provided permission")?; @@ -100,11 +101,24 @@ pub fn r_admin_user_permission( Ok(()) })?; - manage_single_user( - database, - Some(Ok("Permissions update".into())), - form.name.clone(), - ) + let user = database + .get_user(&name)? + .ok_or(anyhow!("user does not exist"))?; + + Ok(RawHtml(render_page( + &AdminUserPage { + flash: Some(Ok("Permissions updated".to_string())), + lang: &lang, + user: &user, + }, + RenderInfo { + importing: is_importing(), + session: Some(SessionInfo { + user: session.0.user, + }), + }, + lang, + ))) } #[post("/admin/<name>/remove")] @@ -112,10 +126,26 @@ pub fn r_admin_remove_user( session: AdminSession, database: &State<Database>, name: &str, -) -> MyResult<DynLayoutPage<'static>> { - drop(session); + lang: AcceptLanguage, +) -> MyResult<RawHtml<String>> { + let AcceptLanguage(lang) = lang; if !database.delete_user(&name)? { Err(anyhow!("user did not exist"))?; } - user_management(database, Some(Ok("User removed".into()))) + let r = admin_users(database, &session)?; + + Ok(RawHtml(render_page( + &AdminUsersPage { + flash: Some(Ok("User removed".to_string())), + lang: &lang, + users: &r.users, + }, + RenderInfo { + importing: is_importing(), + session: Some(SessionInfo { + user: session.0.user, + }), + }, + lang, + ))) } |