aboutsummaryrefslogtreecommitdiff
path: root/server/src/routes/admin
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/routes/admin')
-rw-r--r--server/src/routes/admin/import.rs78
-rw-r--r--server/src/routes/admin/log.rs55
-rw-r--r--server/src/routes/admin/mod.rs29
-rw-r--r--server/src/routes/admin/users.rs119
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"),
+ ))
+}