aboutsummaryrefslogtreecommitdiff
path: root/server/src
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-04-29 15:19:36 +0200
committermetamuffin <metamuffin@disroot.org>2025-04-29 15:19:36 +0200
commitf73aa32549743b2967160d38c1622199c41524a4 (patch)
tree0fa290fbf9b14d7bfd3803f8cc4618c6c9829330 /server/src
parentf62c7f2a8cc143454779dc99334ca9fc80ddabd5 (diff)
downloadjellything-f73aa32549743b2967160d38c1622199c41524a4.tar
jellything-f73aa32549743b2967160d38c1622199c41524a4.tar.bz2
jellything-f73aa32549743b2967160d38c1622199c41524a4.tar.zst
aaaaaaa
Diffstat (limited to 'server/src')
-rw-r--r--server/src/compat/jellyfin/mod.rs85
-rw-r--r--server/src/compat/youtube.rs34
-rw-r--r--server/src/helper/mod.rs6
-rw-r--r--server/src/helper/node_id.rs17
-rw-r--r--server/src/helper/session.rs (renamed from server/src/logic/session.rs)6
-rw-r--r--server/src/logic/mod.rs2
-rw-r--r--server/src/logic/stream.rs6
-rw-r--r--server/src/logic/userdata.rs34
-rw-r--r--server/src/main.rs3
-rw-r--r--server/src/ui/account/mod.rs19
-rw-r--r--server/src/ui/account/settings.rs42
-rw-r--r--server/src/ui/admin/log.rs253
-rw-r--r--server/src/ui/admin/mod.rs283
-rw-r--r--server/src/ui/admin/user.rs140
14 files changed, 342 insertions, 588 deletions
diff --git a/server/src/compat/jellyfin/mod.rs b/server/src/compat/jellyfin/mod.rs
index 9d5c93e..20f8c7e 100644
--- a/server/src/compat/jellyfin/mod.rs
+++ b/server/src/compat/jellyfin/mod.rs
@@ -6,25 +6,22 @@
pub mod models;
use crate::{
- logic::session::Session,
- ui::{
- account::login_logic,
- assets::{
- rocket_uri_macro_r_asset, rocket_uri_macro_r_item_backdrop,
- rocket_uri_macro_r_item_poster,
- },
- error::MyResult,
- node::{aspect_class, DatabaseNodeUserDataExt},
- sort::{filter_and_sort_nodes, FilterProperty, NodeFilterSort, SortOrder, SortProperty},
- },
+ helper::A,
+ ui::{account::login_logic, error::MyResult},
};
use anyhow::{anyhow, Context};
use jellybase::{database::Database, CONF};
use jellycommon::{
+ api::{FilterProperty, NodeFilterSort, SortOrder, SortProperty},
+ routes::{u_asset, u_node_slug_backdrop, u_node_slug_poster},
stream::{StreamContainer, StreamSpec},
user::{NodeUserData, WatchedState},
MediaInfo, Node, NodeID, NodeKind, SourceTrack, SourceTrackKind, Visibility,
};
+use jellylogic::{
+ filter_sort::filter_and_sort_nodes, node::DatabaseNodeUserDataExt, session::Session,
+};
+use jellyui::node_page::aspect_class;
use models::*;
use rocket::{
get,
@@ -86,7 +83,7 @@ pub fn r_jellyfin_quickconnect_enabled() -> Json<Value> {
}
#[get("/System/Endpoint")]
-pub fn r_jellyfin_system_endpoint(_session: Session) -> Json<Value> {
+pub fn r_jellyfin_system_endpoint(_session: A<Session>) -> Json<Value> {
Json(json!({
"IsLocal": false,
"IsInNetwork": false,
@@ -95,7 +92,7 @@ pub fn r_jellyfin_system_endpoint(_session: Session) -> Json<Value> {
use rocket_ws::{Message, Stream, WebSocket};
#[get("/socket")]
-pub fn r_jellyfin_socket(_session: Session, ws: WebSocket) -> Stream!['static] {
+pub fn r_jellyfin_socket(_session: A<Session>, ws: WebSocket) -> Stream!['static] {
Stream! { ws =>
for await message in ws {
eprintln!("{message:?}");
@@ -105,7 +102,7 @@ pub fn r_jellyfin_socket(_session: Session, ws: WebSocket) -> Stream!['static] {
}
#[get("/System/Info")]
-pub fn r_jellyfin_system_info(_session: Session) -> Json<Value> {
+pub fn r_jellyfin_system_info(_session: A<Session>) -> Json<Value> {
Json(json!({
"OperatingSystemDisplayName": "",
"HasPendingRestart": false,
@@ -135,7 +132,7 @@ pub fn r_jellyfin_system_info(_session: Session) -> Json<Value> {
}
#[get("/DisplayPreferences/usersettings")]
-pub fn r_jellyfin_displaypreferences_usersettings(_session: Session) -> Json<Value> {
+pub fn r_jellyfin_displaypreferences_usersettings(_session: A<Session>) -> Json<Value> {
Json(json!({
"Id": "3ce5b65d-e116-d731-65d1-efc4a30ec35c",
"SortBy": "SortName",
@@ -153,53 +150,53 @@ pub fn r_jellyfin_displaypreferences_usersettings(_session: Session) -> Json<Val
}
#[post("/DisplayPreferences/usersettings")]
-pub fn r_jellyfin_displaypreferences_usersettings_post(_session: Session) {}
+pub fn r_jellyfin_displaypreferences_usersettings_post(_session: A<Session>) {}
#[get("/Users/<id>")]
-pub fn r_jellyfin_users_id(session: Session, id: &str) -> Json<Value> {
+pub fn r_jellyfin_users_id(session: A<Session>, id: &str) -> Json<Value> {
let _ = id;
- Json(user_object(session.user.name))
+ Json(user_object(session.0.user.name))
}
#[get("/Items/<id>/Images/Primary?<fillWidth>&<tag>")]
#[allow(non_snake_case)]
pub fn r_jellyfin_items_image_primary(
- _session: Session,
+ _session: A<Session>,
id: &str,
fillWidth: Option<usize>,
tag: String,
) -> Redirect {
if tag == "poster" {
- Redirect::permanent(rocket::uri!(r_item_poster(id, fillWidth)))
+ Redirect::permanent(u_node_slug_poster(id, fillWidth.unwrap_or(1024)))
} else {
- Redirect::permanent(rocket::uri!(r_asset(tag, fillWidth)))
+ Redirect::permanent(u_asset(&tag, fillWidth.unwrap_or(1024)))
}
}
#[get("/Items/<id>/Images/Backdrop/0?<maxWidth>")]
#[allow(non_snake_case)]
pub fn r_jellyfin_items_images_backdrop(
- _session: Session,
+ _session: A<Session>,
id: &str,
maxWidth: Option<usize>,
) -> Redirect {
- Redirect::permanent(rocket::uri!(r_item_backdrop(id, maxWidth)))
+ Redirect::permanent(u_node_slug_backdrop(id, maxWidth.unwrap_or(1024)))
}
#[get("/Items/<id>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_items_item(
- session: Session,
+ session: A<Session>,
database: &State<Database>,
id: &str,
) -> MyResult<Json<JellyfinItem>> {
- let (n, ud) = database.get_node_with_userdata(NodeID::from_slug(id), &session)?;
+ let (n, ud) = database.get_node_with_userdata(NodeID::from_slug(id), &session.0)?;
Ok(Json(item_object(&n, &ud)))
}
#[get("/Users/<uid>/Items/<id>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_users_items_item(
- session: Session,
+ session: A<Session>,
database: &State<Database>,
uid: &str,
id: &str,
@@ -228,7 +225,7 @@ struct JellyfinItemQuery {
#[get("/Users/<uid>/Items?<query..>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_users_items(
- session: Session,
+ session: A<Session>,
database: &State<Database>,
uid: &str,
query: JellyfinItemQuery,
@@ -240,7 +237,7 @@ pub fn r_jellyfin_users_items(
#[get("/Artists?<query..>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_artists(
- session: Session,
+ session: A<Session>,
database: &State<Database>,
mut query: JellyfinItemQuery,
) -> MyResult<Json<Value>> {
@@ -256,7 +253,7 @@ pub fn r_jellyfin_artists(
#[get("/Persons?<query..>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_persons(
- session: Session,
+ session: A<Session>,
database: &State<Database>,
mut query: JellyfinItemQuery,
) -> MyResult<Json<Value>> {
@@ -272,7 +269,7 @@ pub fn r_jellyfin_persons(
#[get("/Items?<query..>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_items(
- session: Session,
+ session: A<Session>,
database: &State<Database>,
query: JellyfinItemQuery,
) -> MyResult<Json<Value>> {
@@ -320,7 +317,7 @@ pub fn r_jellyfin_items(
let mut nodes = nodes
.into_iter()
- .map(|nid| database.get_node_with_userdata(nid, &session))
+ .map(|nid| database.get_node_with_userdata(nid, &session.0))
.collect::<Result<Vec<_>, anyhow::Error>>()?;
filter_and_sort_nodes(
@@ -352,7 +349,7 @@ pub fn r_jellyfin_items(
#[get("/UserViews?<userId>")]
#[allow(non_snake_case)]
pub fn r_jellyfin_users_views(
- session: Session,
+ session: A<Session>,
database: &State<Database>,
userId: &str,
) -> MyResult<Json<Value>> {
@@ -362,7 +359,7 @@ pub fn r_jellyfin_users_views(
.get_node_children(NodeID::from_slug("library"))
.context("root node missing")?
.into_iter()
- .map(|nid| database.get_node_with_userdata(nid, &session))
+ .map(|nid| database.get_node_with_userdata(nid, &session.0))
.collect::<Result<Vec<_>, anyhow::Error>>()?;
toplevel.sort_by_key(|(n, _)| n.index.unwrap_or(usize::MAX));
@@ -382,7 +379,7 @@ pub fn r_jellyfin_users_views(
}
#[get("/Items/<id>/Similar")]
-pub fn r_jellyfin_items_similar(_session: Session, id: &str) -> Json<Value> {
+pub fn r_jellyfin_items_similar(_session: A<Session>, id: &str) -> Json<Value> {
let _ = id;
Json(json!({
"Items": [],
@@ -392,7 +389,7 @@ pub fn r_jellyfin_items_similar(_session: Session, id: &str) -> Json<Value> {
}
#[get("/LiveTv/Programs/Recommended")]
-pub fn r_jellyfin_livetv_programs_recommended(_session: Session) -> Json<Value> {
+pub fn r_jellyfin_livetv_programs_recommended(_session: A<Session>) -> Json<Value> {
Json(json!({
"Items": [],
"TotalRecordCount": 0,
@@ -401,7 +398,7 @@ pub fn r_jellyfin_livetv_programs_recommended(_session: Session) -> Json<Value>
}
#[get("/Users/<uid>/Items/<id>/Intros")]
-pub fn r_jellyfin_items_intros(_session: Session, uid: &str, id: &str) -> Json<Value> {
+pub fn r_jellyfin_items_intros(_session: A<Session>, uid: &str, id: &str) -> Json<Value> {
let _ = (uid, id);
Json(json!({
"Items": [],
@@ -411,7 +408,7 @@ pub fn r_jellyfin_items_intros(_session: Session, uid: &str, id: &str) -> Json<V
}
#[get("/Shows/NextUp")]
-pub fn r_jellyfin_shows_nextup(_session: Session) -> Json<Value> {
+pub fn r_jellyfin_shows_nextup(_session: A<Session>) -> Json<Value> {
Json(json!({
"Items": [],
"TotalRecordCount": 0,
@@ -421,7 +418,7 @@ pub fn r_jellyfin_shows_nextup(_session: Session) -> Json<Value> {
#[post("/Items/<id>/PlaybackInfo")]
pub fn r_jellyfin_items_playbackinfo(
- _session: Session,
+ _session: A<Session>,
database: &State<Database>,
id: &str,
) -> MyResult<Json<Value>> {
@@ -438,7 +435,7 @@ pub fn r_jellyfin_items_playbackinfo(
#[get("/Videos/<id>/stream.webm")]
pub fn r_jellyfin_video_stream(
- _session: Session,
+ _session: A<Session>,
database: &State<Database>,
id: &str,
) -> MyResult<Redirect> {
@@ -463,14 +460,14 @@ struct JellyfinProgressData {
#[post("/Sessions/Playing/Progress", data = "<data>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_sessions_playing_progress(
- session: Session,
+ session: A<Session>,
database: &State<Database>,
data: Json<JellyfinProgressData>,
) -> MyResult<()> {
let position = data.position_ticks / 10_000_000.;
database.update_node_udata(
NodeID::from_slug(&data.item_id),
- &session.user.name,
+ &session.0.user.name,
|udata| {
udata.watched = match udata.watched {
WatchedState::None | WatchedState::Pending | WatchedState::Progress(_) => {
@@ -485,16 +482,16 @@ pub fn r_jellyfin_sessions_playing_progress(
}
#[post("/Sessions/Playing")]
-pub fn r_jellyfin_sessions_playing(_session: Session) {}
+pub fn r_jellyfin_sessions_playing(_session: A<Session>) {}
#[get("/Playback/BitrateTest?<Size>")]
#[allow(non_snake_case)]
-pub fn r_jellyfin_playback_bitratetest(_session: Session, Size: usize) -> Vec<u8> {
+pub fn r_jellyfin_playback_bitratetest(_session: A<Session>, Size: usize) -> Vec<u8> {
vec![0; Size.min(1_000_000)]
}
#[post("/Sessions/Capabilities/Full")]
-pub fn r_jellyfin_sessions_capabilities_full(_session: Session) {}
+pub fn r_jellyfin_sessions_capabilities_full(_session: A<Session>) {}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
diff --git a/server/src/compat/youtube.rs b/server/src/compat/youtube.rs
index 1df2751..67a34fc 100644
--- a/server/src/compat/youtube.rs
+++ b/server/src/compat/youtube.rs
@@ -3,21 +3,15 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
-use crate::{
- logic::session::Session,
- ui::{
- error::MyResult,
- node::rocket_uri_macro_r_library_node,
- player::{rocket_uri_macro_r_player, PlayerConfig},
- },
-};
+use crate::{helper::A, ui::error::MyResult};
use anyhow::anyhow;
use jellybase::database::Database;
-use jellycommon::NodeID;
+use jellycommon::routes::{u_node_slug, u_node_slug_player};
+use jellylogic::session::Session;
use rocket::{get, response::Redirect, State};
#[get("/watch?<v>")]
-pub fn r_youtube_watch(_session: Session, db: &State<Database>, v: &str) -> MyResult<Redirect> {
+pub fn r_youtube_watch(_session: A<Session>, db: &State<Database>, v: &str) -> MyResult<Redirect> {
if v.len() != 11 {
Err(anyhow!("video id length incorrect"))?
}
@@ -25,14 +19,15 @@ pub fn r_youtube_watch(_session: Session, db: &State<Database>, v: &str) -> MyRe
Err(anyhow!("element not found"))?
};
let node = db.get_node(id)?.ok_or(anyhow!("node missing"))?;
- Ok(Redirect::to(rocket::uri!(r_player(
- &node.slug,
- PlayerConfig::default()
- ))))
+ Ok(Redirect::to(u_node_slug_player(&node.slug)))
}
#[get("/channel/<id>")]
-pub fn r_youtube_channel(_session: Session, db: &State<Database>, id: &str) -> MyResult<Redirect> {
+pub fn r_youtube_channel(
+ _session: A<Session>,
+ db: &State<Database>,
+ id: &str,
+) -> MyResult<Redirect> {
let Some(id) = (if id.starts_with("UC") {
db.get_node_external_id("youtube:channel", id)?
} else if id.starts_with("@") {
@@ -43,11 +38,11 @@ pub fn r_youtube_channel(_session: Session, db: &State<Database>, id: &str) -> M
Err(anyhow!("channel not found"))?
};
let node = db.get_node(id)?.ok_or(anyhow!("node missing"))?;
- Ok(Redirect::to(rocket::uri!(r_library_node(&node.slug))))
+ Ok(Redirect::to(u_node_slug(&node.slug)))
}
#[get("/embed/<v>")]
-pub fn r_youtube_embed(_session: Session, db: &State<Database>, v: &str) -> MyResult<Redirect> {
+pub fn r_youtube_embed(_session: A<Session>, db: &State<Database>, v: &str) -> MyResult<Redirect> {
if v.len() != 11 {
Err(anyhow!("video id length incorrect"))?
}
@@ -55,8 +50,5 @@ pub fn r_youtube_embed(_session: Session, db: &State<Database>, v: &str) -> MyRe
Err(anyhow!("element not found"))?
};
let node = db.get_node(id)?.ok_or(anyhow!("node missing"))?;
- Ok(Redirect::to(rocket::uri!(r_player(
- &node.slug,
- PlayerConfig::default()
- ))))
+ Ok(Redirect::to(u_node_slug_player(&node.slug)))
}
diff --git a/server/src/helper/mod.rs b/server/src/helper/mod.rs
index 856f6b7..125b159 100644
--- a/server/src/helper/mod.rs
+++ b/server/src/helper/mod.rs
@@ -3,5 +3,9 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
-pub mod cors;
pub mod cache;
+pub mod cors;
+pub mod session;
+pub mod node_id;
+
+pub struct A<T>(pub T);
diff --git a/server/src/helper/node_id.rs b/server/src/helper/node_id.rs
new file mode 100644
index 0000000..f891d62
--- /dev/null
+++ b/server/src/helper/node_id.rs
@@ -0,0 +1,17 @@
+/*
+ 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) 2025 metamuffin <metamuffin.org>
+*/
+
+use super::A;
+use jellycommon::NodeID;
+use rocket::request::FromParam;
+use std::str::FromStr;
+
+impl<'a> FromParam<'a> for A<NodeID> {
+ type Error = ();
+ fn from_param(param: &'a str) -> Result<Self, Self::Error> {
+ NodeID::from_str(param).map_err(|_| ()).map(A)
+ }
+}
diff --git a/server/src/logic/session.rs b/server/src/helper/session.rs
index 105aa10..b77f9fa 100644
--- a/server/src/logic/session.rs
+++ b/server/src/helper/session.rs
@@ -16,7 +16,7 @@ use rocket::{
Request, State,
};
-pub struct A<T>(pub T);
+use super::A;
impl A<Session> {
async fn from_request_ut(req: &Request<'_>) -> Result<Self, MyError> {
@@ -73,7 +73,7 @@ impl<'r> FromRequest<'r> for A<Session> {
async fn from_request<'life0>(
request: &'r Request<'life0>,
) -> request::Outcome<Self, Self::Error> {
- match Session::from_request_ut(request).await {
+ match A::<Session>::from_request_ut(request).await {
Ok(x) => Outcome::Success(x),
Err(e) => {
warn!("authentificated route rejected: {e:?}");
@@ -91,7 +91,7 @@ impl<'r> FromRequest<'r> for A<AdminSession> {
) -> request::Outcome<Self, Self::Error> {
match A::<Session>::from_request_ut(request).await {
Ok(x) => {
- if x.user.admin {
+ if x.0.user.admin {
Outcome::Success(A(AdminSession(x.0)))
} else {
Outcome::Error((
diff --git a/server/src/logic/mod.rs b/server/src/logic/mod.rs
index 745d11b..26f45de 100644
--- a/server/src/logic/mod.rs
+++ b/server/src/logic/mod.rs
@@ -4,6 +4,6 @@
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
pub mod playersync;
-pub mod session;
pub mod stream;
pub mod userdata;
+
diff --git a/server/src/logic/stream.rs b/server/src/logic/stream.rs
index f9cdb41..9d4db6d 100644
--- a/server/src/logic/stream.rs
+++ b/server/src/logic/stream.rs
@@ -3,7 +3,7 @@
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::MyError};
+use crate::{database::Database, helper::A, ui::error::MyError};
use anyhow::{anyhow, Result};
use jellybase::{assetfed::AssetInner, federation::Federation};
use jellycommon::{stream::StreamSpec, TrackSource};
@@ -26,7 +26,7 @@ use tokio::io::{duplex, DuplexStream};
#[head("/n/<_id>/stream?<spec..>")]
pub async fn r_stream_head(
- _sess: Session,
+ _sess: A<Session>,
_id: &str,
spec: BTreeMap<String, String>,
) -> Result<Either<StreamResponse, Redirect>, MyError> {
@@ -42,7 +42,7 @@ pub async fn r_stream_head(
#[get("/n/<id>/stream?<spec..>")]
pub async fn r_stream(
- _session: Session,
+ _session: A<Session>,
_federation: &State<Federation>,
db: &State<Database>,
id: &str,
diff --git a/server/src/logic/userdata.rs b/server/src/logic/userdata.rs
index 8da6be9..25d3893 100644
--- a/server/src/logic/userdata.rs
+++ b/server/src/logic/userdata.rs
@@ -3,10 +3,12 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
-use crate::ui::error::MyResult;
+use crate::{helper::A, ui::error::MyResult};
use jellybase::database::Database;
use jellycommon::{
- routes::u_node_id, user::{NodeUserData, WatchedState}, NodeID
+ routes::u_node_id,
+ user::{NodeUserData, WatchedState},
+ NodeID,
};
use jellylogic::session::Session;
use rocket::{
@@ -23,25 +25,25 @@ pub enum UrlWatchedState {
#[get("/n/<id>/userdata")]
pub fn r_node_userdata(
- session: Session,
+ session: A<Session>,
db: &State<Database>,
- id: NodeID,
+ id: A<NodeID>,
) -> MyResult<Json<NodeUserData>> {
let u = db
- .get_node_udata(id, &session.user.name)?
+ .get_node_udata(id.0, &session.0.user.name)?
.unwrap_or_default();
Ok(Json(u))
}
#[post("/n/<id>/watched?<state>")]
pub async fn r_node_userdata_watched(
- session: Session,
+ session: A<Session>,
db: &State<Database>,
- id: NodeID,
+ id: A<NodeID>,
state: UrlWatchedState,
) -> MyResult<Redirect> {
// TODO perm
- db.update_node_udata(id, &session.user.name, |udata| {
+ db.update_node_udata(id.0, &session.0.user.name, |udata| {
udata.watched = match state {
UrlWatchedState::None => WatchedState::None,
UrlWatchedState::Watched => WatchedState::Watched,
@@ -49,7 +51,7 @@ pub async fn r_node_userdata_watched(
};
Ok(())
})?;
- Ok(Redirect::found(u_node_id(id)))
+ Ok(Redirect::found(u_node_id(id.0)))
}
#[derive(FromForm)]
@@ -60,28 +62,28 @@ pub struct UpdateRating {
#[post("/n/<id>/update_rating", data = "<form>")]
pub async fn r_node_userdata_rating(
- session: Session,
+ session: A<Session>,
db: &State<Database>,
- id: NodeID,
+ id: A<NodeID>,
form: Form<UpdateRating>,
) -> MyResult<Redirect> {
// TODO perm
- db.update_node_udata(id, &session.user.name, |udata| {
+ db.update_node_udata(id.0, &session.0.user.name, |udata| {
udata.rating = form.rating;
Ok(())
})?;
- Ok(Redirect::found(u_node_id(id)))
+ Ok(Redirect::found(u_node_id(id.0)))
}
#[post("/n/<id>/progress?<t>")]
pub async fn r_node_userdata_progress(
- session: Session,
+ session: A<Session>,
db: &State<Database>,
- id: NodeID,
+ id: A<NodeID>,
t: f64,
) -> MyResult<()> {
// TODO perm
- db.update_node_udata(id, &session.user.name, |udata| {
+ db.update_node_udata(id.0, &session.0.user.name, |udata| {
udata.watched = match udata.watched {
WatchedState::None | WatchedState::Pending | WatchedState::Progress(_) => {
WatchedState::Progress(t)
diff --git a/server/src/main.rs b/server/src/main.rs
index b583823..6634143 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -10,10 +10,11 @@
use anyhow::Context;
use database::Database;
use jellybase::{federation::Federation, CONF, SECRETS};
+use jellylogic::admin::log::enable_logging;
use log::{error, info, warn};
use routes::build_rocket;
use tokio::fs::create_dir_all;
-use ui::{account::hash_password, admin::log::enable_logging};
+use ui::account::hash_password;
pub use jellybase::database;
pub mod api;
diff --git a/server/src/ui/account/mod.rs b/server/src/ui/account/mod.rs
index c1e5479..54fa4d0 100644
--- a/server/src/ui/account/mod.rs
+++ b/server/src/ui/account/mod.rs
@@ -9,12 +9,9 @@ use super::error::MyError;
use crate::{
database::Database,
locale::AcceptLanguage,
- logic::session::{self, Session},
ui::{error::MyResult, home::rocket_uri_macro_r_home},
- uri,
};
use anyhow::anyhow;
-use argon2::{password_hash::Salt, Argon2, PasswordHasher};
use chrono::Duration;
use jellycommon::user::{User, UserPermission};
use rocket::{
@@ -44,21 +41,7 @@ pub async fn r_account_register(lang: AcceptLanguage) -> DynLayoutPage<'static>
LayoutPage {
title: tr(lang, "account.register").to_string(),
content: markup::new! {
- form.account[method="POST", action=""] {
- h1 { @trs(&lang, "account.register") }
-
- label[for="inp-invitation"] { @trs(&lang, "account.register.invitation") }
- input[type="text", id="inp-invitation", name="invitation"]; br;
-
- label[for="inp-username"] { @trs(&lang, "account.username") }
- input[type="text", id="inp-username", name="username"]; br;
- label[for="inp-password"] { @trs(&lang, "account.password") }
- input[type="password", id="inp-password", name="password"]; br;
-
- input[type="submit", value=&*tr(lang, "account.register.submit")];
-
- p { @trs(&lang, "account.register.login") " " a[href=uri!(r_account_login())] { @trs(&lang, "account.register.login_here") } }
- }
+
},
..Default::default()
}
diff --git a/server/src/ui/account/settings.rs b/server/src/ui/account/settings.rs
index 3b53bc4..e6d096f 100644
--- a/server/src/ui/account/settings.rs
+++ b/server/src/ui/account/settings.rs
@@ -4,27 +4,18 @@
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
use super::{format_form_error, hash_password};
-use crate::{
- database::Database,
- locale::AcceptLanguage,
- ui::{
- account::{rocket_uri_macro_r_account_login, session::Session},
- error::MyResult,
- layout::{trs, DynLayoutPage, LayoutPage},
- },
- uri,
-};
-use jellybase::{
- locale::{tr, Language},
- permission::PermissionSetExt,
-};
+use crate::{database::Database, locale::AcceptLanguage, ui::error::MyResult};
+use jellybase::permission::PermissionSetExt;
use jellycommon::user::{PlayerKind, Theme, UserPermission};
-use markup::{Render, RenderAttributeValue};
+use jellylogic::session::Session;
+use jellyui::locale::Language;
use rocket::{
form::{self, validate::len, Contextual, Form},
get,
http::uri::fmt::{Query, UriDisplay},
- post, FromForm, State,
+ post,
+ response::content::RawHtml,
+ FromForm, State,
};
use std::ops::Range;
@@ -47,24 +38,11 @@ fn settings_page(
session: Session,
flash: Option<MyResult<String>>,
lang: Language,
-) -> DynLayoutPage<'static> {
- LayoutPage {
- title: "Settings".to_string(),
- class: Some("settings"),
- content:
- }
-}
-
-struct A<T>(pub T);
-impl<T: UriDisplay<Query>> RenderAttributeValue for A<T> {}
-impl<T: UriDisplay<Query>> Render for A<T> {
- fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
- writer.write_fmt(format_args!("{}", &self.0 as &dyn UriDisplay<Query>))
- }
+) -> RawHtml<String> {
}
#[get("/account/settings")]
-pub fn r_account_settings(session: Session, lang: AcceptLanguage) -> DynLayoutPage<'static> {
+pub fn r_account_settings(session: Session, lang: AcceptLanguage) -> RawHtml<String> {
let AcceptLanguage(lang) = lang;
settings_page(session, None, lang)
}
@@ -75,7 +53,7 @@ pub fn r_account_settings_post(
database: &State<Database>,
form: Form<Contextual<SettingsForm>>,
lang: AcceptLanguage,
-) -> MyResult<DynLayoutPage<'static>> {
+) -> MyResult<RawHtml<String>> {
let AcceptLanguage(lang) = lang;
session
.user
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,
+ )))
}