diff options
Diffstat (limited to 'server/src/routes/admin')
| -rw-r--r-- | server/src/routes/admin/import.rs | 78 | ||||
| -rw-r--r-- | server/src/routes/admin/log.rs | 55 | ||||
| -rw-r--r-- | server/src/routes/admin/mod.rs | 29 | ||||
| -rw-r--r-- | server/src/routes/admin/users.rs | 119 |
4 files changed, 281 insertions, 0 deletions
diff --git a/server/src/routes/admin/import.rs b/server/src/routes/admin/import.rs new file mode 100644 index 0000000..31e7d70 --- /dev/null +++ b/server/src/routes/admin/import.rs @@ -0,0 +1,78 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin <metamuffin.org> +*/ + +use crate::{request_info::RequestInfo, routes::error::MyResult}; +use jellycommon::routes::u_admin_import; +use jellyimport::{ + ImportConfig, import_wrap, is_importing, + reporting::{IMPORT_ERRORS, IMPORT_PROGRESS}, +}; +use jellyui::{components::admin::AdminImport, tr}; +use rocket::{ + get, post, + response::{Flash, Redirect, content::RawHtml}, +}; +use rocket_ws::{Message, Stream, WebSocket}; +use std::time::Duration; +use tokio::{spawn, time::sleep}; + +#[get("/admin/import", rank = 2)] +pub async fn r_admin_import(ri: RequestInfo<'_>) -> MyResult<RawHtml<String>> { + ri.require_admin()?; + + let last_import_err = IMPORT_ERRORS.read().await.clone(); + let last_import_err = last_import_err + .iter() + .map(|e| e.as_str()) + .collect::<Vec<_>>(); + + Ok(ri.respond_ui(&AdminImport { + busy: is_importing(), + errors: &last_import_err, + ri: &ri.render_info(), + })) +} + +#[post("/admin/import?<incremental>")] +pub async fn r_admin_import_post( + ri: RequestInfo<'_>, + incremental: bool, +) -> MyResult<Flash<Redirect>> { + ri.require_admin()?; + spawn(async move { + let _ = import_wrap( + ImportConfig { + config: ri.state.config.import.clone(), + cache: ri.state.cache.clone(), + db: ri.state.database.clone(), + }, + incremental, + ) + .await; + }); + Ok(Flash::success( + Redirect::to(u_admin_import()), + tr(ri.lang, "admin.import_success"), + )) +} + +#[get("/admin/import", rank = 1)] +pub fn r_admin_import_stream(ri: RequestInfo<'_>, ws: WebSocket) -> MyResult<Stream!['static]> { + ri.require_admin()?; + Ok({ + Stream! { ws => + loop { + let Some(p) = IMPORT_PROGRESS.read().await.clone() else { + break; + }; + yield Message::Text(serde_json::to_string(&p).unwrap()); + sleep(Duration::from_secs_f32(0.05)).await; + } + yield Message::Text("done".to_string()); + let _ = ws; + } + }) +} diff --git a/server/src/routes/admin/log.rs b/server/src/routes/admin/log.rs new file mode 100644 index 0000000..bf8126a --- /dev/null +++ b/server/src/routes/admin/log.rs @@ -0,0 +1,55 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin <metamuffin.org> +*/ +use crate::{ + logger::{get_log_buffer, get_log_stream}, + request_info::RequestInfo, + routes::error::MyResult, +}; +use jellyui::components::admin_log::{ServerLogPage, render_log_line}; +use rocket::{get, response::content::RawHtml}; +use rocket_ws::{Message, Stream, WebSocket}; +use serde_json::json; + +#[get("/admin/log?<warnonly>", rank = 2)] +pub fn r_admin_log(ri: RequestInfo, warnonly: bool) -> MyResult<RawHtml<String>> { + ri.require_admin()?; + let messages = get_log_buffer(warnonly) + .into_iter() + .map(|l| render_log_line(&l)) + .collect::<Vec<_>>(); + + Ok(ri.respond_ui(&ServerLogPage { + ri: &ri.render_info(), + messages: &messages, + warnonly, + })) +} + +#[get("/admin/log?stream&<warnonly>&<html>", rank = 1)] +pub fn r_admin_log_stream( + ri: RequestInfo, + ws: WebSocket, + warnonly: bool, + html: bool, +) -> MyResult<Stream!['static]> { + ri.require_admin()?; + let mut stream = get_log_stream(warnonly); + Ok({ + Stream! { ws => + if html { + let _ = ws; + while let Ok(line) = stream.recv().await { + yield Message::Text(render_log_line(&line)); + } + } else { + let _ = ws; + while let Ok(line) = stream.recv().await { + yield Message::Text(json!(line).to_string()); + } + } + } + }) +} diff --git a/server/src/routes/admin/mod.rs b/server/src/routes/admin/mod.rs new file mode 100644 index 0000000..6119b74 --- /dev/null +++ b/server/src/routes/admin/mod.rs @@ -0,0 +1,29 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin <metamuffin.org> +*/ + +pub mod import; +pub mod log; +pub mod users; + +use super::error::MyResult; +use crate::request_info::RequestInfo; +use jellyui::components::admin::AdminDashboard; +use rocket::{get, response::content::RawHtml}; + +#[get("/admin/dashboard")] +pub async fn r_admin_dashboard(ri: RequestInfo<'_>) -> MyResult<RawHtml<String>> { + ri.require_admin()?; + + // let mut db_debug = String::new(); + // ri.state.database.transaction(&mut |txn| { + // db_debug = txn.debug_info()?; + // Ok(()) + // })?; + + Ok(ri.respond_ui(&AdminDashboard { + ri: &ri.render_info(), + })) +} diff --git a/server/src/routes/admin/users.rs b/server/src/routes/admin/users.rs new file mode 100644 index 0000000..01a6403 --- /dev/null +++ b/server/src/routes/admin/users.rs @@ -0,0 +1,119 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin <metamuffin.org> +*/ + +use std::str::FromStr; + +use crate::{auth::hash_password, request_info::RequestInfo, routes::error::MyResult}; +use anyhow::anyhow; +use base64::{Engine, prelude::BASE64_URL_SAFE}; +use jellycommon::{ + jellyobject::{ObjectBufferBuilder, Path}, + routes::u_admin_users, + *, +}; +use jellydb::{Filter, Query}; +use jellyui::{ + components::admin::{AdminUser, AdminUserList}, + tr, +}; +use rand::random; +use rocket::{ + FromForm, + form::Form, + get, post, + response::{Flash, Redirect, content::RawHtml}, +}; + +#[get("/admin/users")] +pub fn r_admin_users(ri: RequestInfo) -> MyResult<RawHtml<String>> { + ri.require_admin()?; + + let mut users = Vec::new(); + ri.state.database.transaction(&mut |txn| { + users.clear(); + let rows = txn + .query(Query::from_str("FILTER Ulgn")?)? + .collect::<Vec<_>>(); + for row in rows { + let (row, _) = row?; + users.push(txn.get(row)?.unwrap()); + } + Ok(()) + })?; + + Ok(ri.respond_ui(&AdminUserList { + ri: &ri.render_info(), + users: &users.iter().map(|u| &**u).collect::<Vec<_>>(), + })) +} + +#[derive(FromForm)] +pub struct NewUser { + login: String, +} + +#[post("/admin/new_user", data = "<form>")] +pub fn r_admin_new_user(ri: RequestInfo, form: Form<NewUser>) -> MyResult<Flash<Redirect>> { + ri.require_admin()?; + + let password = BASE64_URL_SAFE.encode([(); 12].map(|()| random())); + let password_hashed = hash_password(&form.login, &password); + + ri.state.database.transaction(&mut |txn| { + let mut user = ObjectBufferBuilder::default(); + user.push(USER_LOGIN, &form.login); + user.push(USER_PASSWORD, &password_hashed); + user.push(USER_PASSWORD_REQUIRE_CHANGE, ()); + txn.insert(user.finish())?; + Ok(()) + })?; + + Ok(Flash::success( + Redirect::to(u_admin_users()), + format!("User created; password: {password}"), + )) +} + +#[get("/admin/user/<name>")] +pub fn r_admin_user(ri: RequestInfo<'_>, name: &str) -> MyResult<RawHtml<String>> { + ri.require_admin()?; + let mut user = None; + ri.state.database.transaction(&mut |txn| { + if let Some(row) = txn.query_single(Query { + filter: Filter::Match(Path(vec![USER_LOGIN.0]), name.into()), + ..Default::default() + })? { + user = Some(txn.get(row)?.unwrap()); + } + Ok(()) + })?; + let Some(user) = user else { + Err(anyhow!("no such user"))? + }; + + Ok(ri.respond_ui(&AdminUser { + ri: &ri.render_info(), + user: &user, + })) +} + +#[post("/admin/user/<name>/remove")] +pub fn r_admin_user_remove(ri: RequestInfo<'_>, name: &str) -> MyResult<Flash<Redirect>> { + ri.require_admin()?; + ri.state.database.transaction(&mut |txn| { + if let Some(row) = txn.query_single(Query { + filter: Filter::Match(Path(vec![USER_LOGIN.0]), name.into()), + ..Default::default() + })? { + txn.remove(row)?; + } + Ok(()) + })?; + Ok(Flash::success( + Redirect::to(u_admin_users()), + tr(ri.lang, "admin.users.remove_success"), + )) +} |