diff options
author | metamuffin <metamuffin@disroot.org> | 2025-04-29 17:06:23 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-04-29 17:06:23 +0200 |
commit | 212a0f23bc894faf88e159560c113f504349cc05 (patch) | |
tree | ee2ff0ff3b185d1874eb0e8fc4b75f1badf659b8 | |
parent | f73aa32549743b2967160d38c1622199c41524a4 (diff) | |
download | jellything-212a0f23bc894faf88e159560c113f504349cc05.tar jellything-212a0f23bc894faf88e159560c113f504349cc05.tar.bz2 jellything-212a0f23bc894faf88e159560c113f504349cc05.tar.zst |
comiles again but still many logic holes
-rw-r--r-- | logic/src/lib.rs | 1 | ||||
-rw-r--r-- | logic/src/login.rs | 61 | ||||
-rw-r--r-- | server/src/api.rs | 13 | ||||
-rw-r--r-- | server/src/compat/jellyfin/mod.rs | 8 | ||||
-rw-r--r-- | server/src/helper/filter_sort.rs | 45 | ||||
-rw-r--r-- | server/src/helper/mod.rs | 4 | ||||
-rw-r--r-- | server/src/main.rs | 5 | ||||
-rw-r--r-- | server/src/routes.rs | 6 | ||||
-rw-r--r-- | server/src/ui/account/mod.rs | 171 | ||||
-rw-r--r-- | server/src/ui/account/settings.rs | 47 | ||||
-rw-r--r-- | server/src/ui/admin/log.rs | 41 | ||||
-rw-r--r-- | server/src/ui/admin/mod.rs | 18 | ||||
-rw-r--r-- | server/src/ui/admin/user.rs | 24 | ||||
-rw-r--r-- | server/src/ui/assets.rs | 32 | ||||
-rw-r--r-- | server/src/ui/home.rs | 14 | ||||
-rw-r--r-- | server/src/ui/items.rs | 14 | ||||
-rw-r--r-- | server/src/ui/mod.rs | 4 | ||||
-rw-r--r-- | server/src/ui/node.rs | 20 | ||||
-rw-r--r-- | server/src/ui/player.rs | 30 | ||||
-rw-r--r-- | server/src/ui/search.rs | 10 | ||||
-rw-r--r-- | server/src/ui/stats.rs | 10 | ||||
-rw-r--r-- | ui/src/account/mod.rs | 83 | ||||
-rw-r--r-- | ui/src/account/settings.rs (renamed from ui/src/settings.rs) | 12 | ||||
-rw-r--r-- | ui/src/admin/log.rs | 10 | ||||
-rw-r--r-- | ui/src/lib.rs | 1 | ||||
-rw-r--r-- | ui/src/stats.rs | 16 |
26 files changed, 461 insertions, 239 deletions
diff --git a/logic/src/lib.rs b/logic/src/lib.rs index 1dba95d..54c9c40 100644 --- a/logic/src/lib.rs +++ b/logic/src/lib.rs @@ -13,3 +13,4 @@ pub mod session; pub mod stats; pub mod items; pub mod admin; +pub mod login; diff --git a/logic/src/login.rs b/logic/src/login.rs new file mode 100644 index 0000000..e9c2f93 --- /dev/null +++ b/logic/src/login.rs @@ -0,0 +1,61 @@ +/* + 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 crate::session::create; +use anyhow::{Result, anyhow}; +use argon2::{Argon2, PasswordHasher, password_hash::Salt}; +use jellybase::{CONF, database::Database}; +use jellycommon::user::UserPermission; +use std::{collections::HashSet, time::Duration}; + +pub fn login_logic( + database: &Database, + username: &str, + password: &str, + expire: Option<i64>, + drop_permissions: Option<HashSet<UserPermission>>, +) -> Result<String> { + // hashing the password regardless if the accounts exists to better resist timing attacks + let password = hash_password(username, password); + + let mut user = database + .get_user(username)? + .ok_or(anyhow!("invalid password"))?; + + if user.password != password { + Err(anyhow!("invalid password"))? + } + + if let Some(ep) = drop_permissions { + // remove all grant perms that are in `ep` + user.permissions + .0 + .retain(|p, val| if *val { !ep.contains(p) } else { true }) + } + + Ok(create( + user.name, + user.permissions, + Duration::from_days( + CONF.login_expire + .min(expire.unwrap_or(i64::MAX)) + .try_into() + .unwrap(), + ), + )) +} + +pub fn hash_password(username: &str, password: &str) -> Vec<u8> { + Argon2::default() + .hash_password( + format!("{username}\0{password}").as_bytes(), + <&str as TryInto<Salt>>::try_into("IYMa13osbNeLJKnQ1T8LlA").unwrap(), + ) + .unwrap() + .hash + .unwrap() + .as_bytes() + .to_vec() +} diff --git a/server/src/api.rs b/server/src/api.rs index a9df1bd..fb5ee88 100644 --- a/server/src/api.rs +++ b/server/src/api.rs @@ -3,11 +3,14 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use super::ui::{account::login_logic, error::MyResult}; -use crate::database::Database; +use super::ui::error::MyResult; +use crate::{database::Database, helper::A}; use jellybase::assetfed::AssetInner; use jellycommon::{user::CreateSessionParams, NodeID, Visibility}; -use jellylogic::session::{AdminSession, Session}; +use jellylogic::{ + login::login_logic, + session::{AdminSession, Session}, +}; use rocket::{ get, http::MediaType, @@ -47,13 +50,13 @@ pub fn r_api_account_login( } #[get("/api/asset_token_raw/<token>")] -pub fn r_api_asset_token_raw(_admin: AdminSession, token: &str) -> MyResult<Json<AssetInner>> { +pub fn r_api_asset_token_raw(_admin: A<AdminSession>, token: &str) -> MyResult<Json<AssetInner>> { Ok(Json(AssetInner::deser(token)?)) } #[get("/api/nodes_modified?<since>")] pub fn r_api_nodes_modified_since( - _session: Session, + _session: A<Session>, database: &State<Database>, since: u64, ) -> MyResult<Json<Vec<NodeID>>> { diff --git a/server/src/compat/jellyfin/mod.rs b/server/src/compat/jellyfin/mod.rs index 20f8c7e..f999060 100644 --- a/server/src/compat/jellyfin/mod.rs +++ b/server/src/compat/jellyfin/mod.rs @@ -5,10 +5,7 @@ */ pub mod models; -use crate::{ - helper::A, - ui::{account::login_logic, error::MyResult}, -}; +use crate::{helper::A, ui::error::MyResult}; use anyhow::{anyhow, Context}; use jellybase::{database::Database, CONF}; use jellycommon::{ @@ -19,7 +16,8 @@ use jellycommon::{ MediaInfo, Node, NodeID, NodeKind, SourceTrack, SourceTrackKind, Visibility, }; use jellylogic::{ - filter_sort::filter_and_sort_nodes, node::DatabaseNodeUserDataExt, session::Session, + filter_sort::filter_and_sort_nodes, login::login_logic, node::DatabaseNodeUserDataExt, + session::Session, }; use jellyui::node_page::aspect_class; use models::*; diff --git a/server/src/helper/filter_sort.rs b/server/src/helper/filter_sort.rs new file mode 100644 index 0000000..186aa86 --- /dev/null +++ b/server/src/helper/filter_sort.rs @@ -0,0 +1,45 @@ +/* + 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::{ + api::NodeFilterSort, + user::{PlayerKind, Theme}, +}; +use rocket::{ + async_trait, + form::{DataField, FromFormField, Result, ValueField}, +}; + +#[async_trait] +impl<'v> FromFormField<'v> for A<NodeFilterSort> { + fn from_value(field: ValueField<'v>) -> Result<'v, Self> { + Err(field.unexpected())? + } + async fn from_data(field: DataField<'v, '_>) -> Result<'v, Self> { + Err(field.unexpected())? + } +} + +#[async_trait] +impl<'v> FromFormField<'v> for A<Theme> { + fn from_value(field: ValueField<'v>) -> Result<'v, Self> { + Err(field.unexpected())? + } + async fn from_data(field: DataField<'v, '_>) -> Result<'v, Self> { + Err(field.unexpected())? + } +} + +#[async_trait] +impl<'v> FromFormField<'v> for A<PlayerKind> { + fn from_value(field: ValueField<'v>) -> Result<'v, Self> { + Err(field.unexpected())? + } + async fn from_data(field: DataField<'v, '_>) -> Result<'v, Self> { + Err(field.unexpected())? + } +} diff --git a/server/src/helper/mod.rs b/server/src/helper/mod.rs index 125b159..7164175 100644 --- a/server/src/helper/mod.rs +++ b/server/src/helper/mod.rs @@ -5,7 +5,9 @@ */ pub mod cache; pub mod cors; -pub mod session; +pub mod filter_sort; pub mod node_id; +pub mod session; +#[derive(Debug, Clone, Copy)] pub struct A<T>(pub T); diff --git a/server/src/main.rs b/server/src/main.rs index 6634143..94a53c7 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -3,18 +3,17 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -#![feature(int_roundings, let_chains, str_as_str)] +#![feature(int_roundings, let_chains, str_as_str, duration_constructors)] #![allow(clippy::needless_borrows_for_generic_args)] #![recursion_limit = "4096"] use anyhow::Context; use database::Database; use jellybase::{federation::Federation, CONF, SECRETS}; -use jellylogic::admin::log::enable_logging; +use jellylogic::{admin::log::enable_logging, login::hash_password}; use log::{error, info, warn}; use routes::build_rocket; use tokio::fs::create_dir_all; -use ui::account::hash_password; pub use jellybase::database; pub mod api; diff --git a/server/src/routes.rs b/server/src/routes.rs index 4b52da0..ef7b067 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -5,10 +5,10 @@ */ use crate::database::Database; use crate::logic::playersync::{r_playersync, PlayersyncChannels}; +use crate::ui::account::{r_account_login, r_account_logout, r_account_register}; use crate::ui::{ account::{ - r_account_login, r_account_login_post, r_account_logout, r_account_logout_post, - r_account_register, r_account_register_post, + r_account_login_post, r_account_logout_post, r_account_register_post, settings::{r_account_settings, r_account_settings_post}, }, admin::{ @@ -18,9 +18,9 @@ use crate::ui::{ user::{r_admin_remove_user, r_admin_user, r_admin_user_permission, r_admin_users}, }, assets::{r_asset, r_item_backdrop, r_item_poster, r_node_thumbnail, r_person_asset}, - items::r_items, error::{r_api_catch, r_catch}, home::r_home, + items::r_items, node::r_node, player::r_player, r_favicon, r_index, diff --git a/server/src/ui/account/mod.rs b/server/src/ui/account/mod.rs index 54fa4d0..a9c28ea 100644 --- a/server/src/ui/account/mod.rs +++ b/server/src/ui/account/mod.rs @@ -8,22 +8,31 @@ pub mod settings; use super::error::MyError; use crate::{ database::Database, + helper::A, locale::AcceptLanguage, ui::{error::MyResult, home::rocket_uri_macro_r_home}, }; use anyhow::anyhow; -use chrono::Duration; -use jellycommon::user::{User, UserPermission}; +use jellycommon::user::User; +use jellyimport::is_importing; +use jellylogic::{ + login::{hash_password, login_logic}, + session::Session, +}; +use jellyui::{ + account::{AccountLogin, AccountLogout, AccountRegister, AccountRegisterSuccess}, + render_page, + scaffold::{RenderInfo, SessionInfo}, +}; use rocket::{ form::{Contextual, Form}, get, http::{Cookie, CookieJar}, post, - response::Redirect, + response::{content::RawHtml, Redirect}, FromForm, State, }; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; #[derive(FromForm)] pub struct RegisterForm { @@ -36,15 +45,16 @@ pub struct RegisterForm { } #[get("/account/register")] -pub async fn r_account_register(lang: AcceptLanguage) -> DynLayoutPage<'static> { +pub async fn r_account_register(lang: AcceptLanguage) -> RawHtml<String> { let AcceptLanguage(lang) = lang; - LayoutPage { - title: tr(lang, "account.register").to_string(), - content: markup::new! { - + RawHtml(render_page( + &AccountRegister { lang: &lang }, + RenderInfo { + importing: false, + session: None, }, - ..Default::default() - } + lang, + )) } #[derive(FromForm, Serialize, Deserialize)] @@ -58,69 +68,47 @@ pub struct LoginForm { } #[get("/account/login")] -pub fn r_account_login(sess: Option<Session>, lang: AcceptLanguage) -> DynLayoutPage<'static> { +pub fn r_account_login(session: Option<A<Session>>, lang: AcceptLanguage) -> RawHtml<String> { let AcceptLanguage(lang) = lang; - let logged_in = sess.is_some(); - let title = tr( - lang, - if logged_in { - "account.login.switch" - } else { - "account.login" + let logged_in = session.is_some(); + RawHtml(render_page( + &AccountLogin { + lang: &lang, + logged_in, }, - ); - LayoutPage { - title: title.to_string(), - content: markup::new! { - form.account[method="POST", action=""] { - h1 { @title.to_string() } - - 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, if logged_in { "account.login.submit.switch" } else { "account.login.submit" })]; - - @if logged_in { - p { @trs(&lang, "account.login.register.switch") " " a[href=uri!(r_account_register())] { @trs(&lang, "account.login.register_here") } } - } else { - p { @trs(&lang, "account.login.cookie_note") } - p { @trs(&lang, "account.login.register") " " a[href=uri!(r_account_register())] { @trs(&lang, "account.login.register_here") } } - } - } + RenderInfo { + session: session.map(|s| SessionInfo { user: s.0.user }), + importing: is_importing(), }, - ..Default::default() - } + lang, + )) } #[get("/account/logout")] -pub fn r_account_logout(lang: AcceptLanguage) -> DynLayoutPage<'static> { +pub fn r_account_logout(session: Option<A<Session>>, lang: AcceptLanguage) -> RawHtml<String> { let AcceptLanguage(lang) = lang; - LayoutPage { - title: tr(lang, "account.logout").to_string(), - content: markup::new! { - form.account[method="POST", action=""] { - h1 { @trs(&lang, "account.logout") } - input[type="submit", value=&*tr(lang, "account.logout.submit")]; - } + RawHtml(render_page( + &AccountLogout { lang: &lang }, + RenderInfo { + session: session.map(|s| SessionInfo { user: s.0.user }), + importing: is_importing(), }, - ..Default::default() - } + lang, + )) } #[post("/account/register", data = "<form>")] pub fn r_account_register_post<'a>( database: &'a State<Database>, - _sess: Option<Session>, + session: Option<A<Session>>, form: Form<Contextual<'a, RegisterForm>>, lang: AcceptLanguage, -) -> MyResult<DynLayoutPage<'a>> { +) -> MyResult<RawHtml<String>> { let AcceptLanguage(lang) = lang; - let logged_in = _sess.is_some(); + let logged_in = session.is_some(); let form = match &form.value { Some(v) => v, - None => return Err(format_form_error(form)), + None => return Err(MyError(anyhow!(format_form_error(form)))), }; database.register_user( @@ -134,17 +122,17 @@ pub fn r_account_register_post<'a>( }, )?; - Ok(LayoutPage { - title: tr(lang, "account.register.success.title").to_string(), - content: markup::new! { - h1 { @trs(&lang, if logged_in { - "account.register.success.switch" - } else { - "account.register.success" - })} + Ok(RawHtml(render_page( + &AccountRegisterSuccess { + lang: &lang, + logged_in, + }, + RenderInfo { + session: session.map(|s| SessionInfo { user: s.0.user }), + importing: is_importing(), }, - ..Default::default() - }) + lang, + ))) } #[post("/account/login", data = "<form>")] @@ -155,7 +143,7 @@ pub fn r_account_login_post( ) -> MyResult<Redirect> { let form = match &form.value { Some(v) => v, - None => return Err(format_form_error(form)), + None => return Err(MyError(anyhow!(format_form_error(form)))), }; jar.add( Cookie::build(( @@ -175,39 +163,7 @@ pub fn r_account_logout_post(jar: &CookieJar) -> MyResult<Redirect> { Ok(Redirect::found(rocket::uri!(r_home()))) } -pub fn login_logic( - database: &Database, - username: &str, - password: &str, - expire: Option<i64>, - drop_permissions: Option<HashSet<UserPermission>>, -) -> MyResult<String> { - // hashing the password regardless if the accounts exists to better resist timing attacks - let password = hash_password(username, password); - - let mut user = database - .get_user(username)? - .ok_or(anyhow!("invalid password"))?; - - if user.password != password { - Err(anyhow!("invalid password"))? - } - - if let Some(ep) = drop_permissions { - // remove all grant perms that are in `ep` - user.permissions - .0 - .retain(|p, val| if *val { !ep.contains(p) } else { true }) - } - - Ok(session::create( - user.name, - user.permissions, - Duration::days(CONF.login_expire.min(expire.unwrap_or(i64::MAX))), - )) -} - -pub fn format_form_error<T>(form: Form<Contextual<T>>) -> MyError { +pub fn format_form_error<T>(form: Form<Contextual<T>>) -> String { let mut k = String::from("form validation failed:"); for e in form.context.errors() { k += &format!( @@ -218,18 +174,5 @@ pub fn format_form_error<T>(form: Form<Contextual<T>>) -> MyError { .unwrap_or("<unknown>".to_string()) ) } - MyError(anyhow!(k)) -} - -pub fn hash_password(username: &str, password: &str) -> Vec<u8> { - Argon2::default() - .hash_password( - format!("{username}\0{password}").as_bytes(), - <&str as TryInto<Salt>>::try_into("IYMa13osbNeLJKnQ1T8LlA").unwrap(), - ) - .unwrap() - .hash - .unwrap() - .as_bytes() - .to_vec() + k } diff --git a/server/src/ui/account/settings.rs b/server/src/ui/account/settings.rs index e6d096f..5355321 100644 --- a/server/src/ui/account/settings.rs +++ b/server/src/ui/account/settings.rs @@ -4,16 +4,20 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::{format_form_error, hash_password}; -use crate::{database::Database, locale::AcceptLanguage, ui::error::MyResult}; +use crate::{database::Database, helper::A, locale::AcceptLanguage, ui::error::MyResult}; use jellybase::permission::PermissionSetExt; use jellycommon::user::{PlayerKind, Theme, UserPermission}; +use jellyimport::is_importing; use jellylogic::session::Session; -use jellyui::locale::Language; +use jellyui::{ + account::settings::SettingsPage, + locale::{tr, Language}, + render_page, + scaffold::{RenderInfo, SessionInfo}, +}; use rocket::{ form::{self, validate::len, Contextual, Form}, - get, - http::uri::fmt::{Query, UriDisplay}, - post, + get, post, response::content::RawHtml, FromForm, State, }; @@ -25,8 +29,8 @@ pub struct SettingsForm { password: Option<String>, #[field(validate = option_len(4..32))] display_name: Option<String>, - theme: Option<Theme>, - player_preference: Option<PlayerKind>, + theme: Option<A<Theme>>, + player_preference: Option<A<PlayerKind>>, native_secret: Option<String>, } @@ -36,25 +40,40 @@ fn option_len<'v>(value: &Option<String>, range: Range<usize>) -> form::Result<' fn settings_page( session: Session, - flash: Option<MyResult<String>>, + flash: Option<Result<String, String>>, lang: Language, ) -> RawHtml<String> { + RawHtml(render_page( + &SettingsPage { + flash, + session: &SessionInfo { + user: session.user.clone(), + }, + lang: &lang, + }, + RenderInfo { + importing: is_importing(), + session: Some(SessionInfo { user: session.user }), + }, + lang, + )) } #[get("/account/settings")] -pub fn r_account_settings(session: Session, lang: AcceptLanguage) -> RawHtml<String> { +pub fn r_account_settings(session: A<Session>, lang: AcceptLanguage) -> RawHtml<String> { let AcceptLanguage(lang) = lang; - settings_page(session, None, lang) + settings_page(session.0, None, lang) } #[post("/account/settings", data = "<form>")] pub fn r_account_settings_post( - session: Session, + session: A<Session>, database: &State<Database>, form: Form<Contextual<SettingsForm>>, lang: AcceptLanguage, ) -> MyResult<RawHtml<String>> { let AcceptLanguage(lang) = lang; + let A(session) = session; session .user .permissions @@ -65,7 +84,7 @@ pub fn r_account_settings_post( None => { return Ok(settings_page( session, - Some(Err(format_form_error(form))), + Some(Err(format_form_error(form).to_string())), lang, )) } @@ -85,12 +104,12 @@ pub fn r_account_settings_post( out += "\n"; } if let Some(theme) = form.theme { - user.theme = theme; + user.theme = theme.0; out += &*tr(lang, "settings.account.theme.changed"); out += "\n"; } if let Some(player_preference) = form.player_preference { - user.player_preference = player_preference; + user.player_preference = player_preference.0; out += &*tr(lang, "settings.player_preference.changed"); out += "\n"; } diff --git a/server/src/ui/admin/log.rs b/server/src/ui/admin/log.rs index bd6d7af..64782ba 100644 --- a/server/src/ui/admin/log.rs +++ b/server/src/ui/admin/log.rs @@ -3,19 +3,50 @@ 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 jellylogic::{admin::log::get_log_stream, session::AdminSession}; -use jellyui::admin::log::render_log_line; +use crate::{helper::A, locale::AcceptLanguage, ui::error::MyResult}; +use jellyimport::is_importing; +use jellylogic::{ + admin::log::{get_log_buffer, get_log_stream}, + session::AdminSession, +}; +use jellyui::{ + admin::log::{render_log_line, ServerLogPage}, + render_page, + scaffold::{RenderInfo, SessionInfo}, +}; 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<'a>(_session: AdminSession, warnonly: bool) -> MyResult<RawHtml<String>> {} +pub fn r_admin_log<'a>( + session: A<AdminSession>, + warnonly: bool, + lang: AcceptLanguage, +) -> MyResult<RawHtml<String>> { + let AcceptLanguage(lang) = lang; + let messages = get_log_buffer(warnonly) + .into_iter() + .map(|l| render_log_line(&l)) + .collect::<Vec<_>>(); + Ok(RawHtml(render_page( + &ServerLogPage { + messages: &messages, + warnonly, + }, + RenderInfo { + importing: is_importing(), + session: Some(SessionInfo { + user: session.0 .0.user, + }), + }, + lang, + ))) +} #[get("/admin/log?stream&<warnonly>&<html>", rank = 1)] pub fn r_admin_log_stream( - _session: AdminSession, + _session: A<AdminSession>, ws: WebSocket, warnonly: bool, html: bool, diff --git a/server/src/ui/admin/mod.rs b/server/src/ui/admin/mod.rs index b155121..3a9e4e2 100644 --- a/server/src/ui/admin/mod.rs +++ b/server/src/ui/admin/mod.rs @@ -10,7 +10,7 @@ use super::{ assets::{resolve_asset, AVIF_QUALITY, AVIF_SPEED}, error::MyResult, }; -use crate::{database::Database, locale::AcceptLanguage}; +use crate::{database::Database, helper::A, locale::AcceptLanguage}; use anyhow::{anyhow, Context}; use jellybase::{assetfed::AssetInner, federation::Federation, CONF}; use jellycommon::routes::u_admin_dashboard; @@ -33,7 +33,7 @@ use tokio::{sync::Semaphore, task::spawn_blocking}; #[get("/admin/dashboard")] pub async fn r_admin_dashboard( - session: AdminSession, + session: A<AdminSession>, database: &State<Database>, lang: AcceptLanguage, ) -> MyResult<RawHtml<String>> { @@ -62,7 +62,7 @@ pub async fn r_admin_dashboard( RenderInfo { importing: is_importing(), session: Some(SessionInfo { - user: session.0.user, + user: session.0 .0.user, }), }, lang, @@ -71,7 +71,7 @@ pub async fn r_admin_dashboard( #[post("/admin/generate_invite")] pub async fn r_admin_invite( - _session: AdminSession, + _session: A<AdminSession>, database: &State<Database>, ) -> MyResult<Redirect> { let i = format!("{}", rand::rng().random::<u128>()); @@ -87,7 +87,7 @@ pub struct DeleteInvite { #[post("/admin/remove_invite", data = "<form>")] pub async fn r_admin_remove_invite( - session: AdminSession, + session: A<AdminSession>, database: &State<Database>, form: Form<DeleteInvite>, ) -> MyResult<Redirect> { @@ -101,7 +101,7 @@ pub async fn r_admin_remove_invite( #[post("/admin/import?<incremental>")] pub async fn r_admin_import( - session: AdminSession, + session: A<AdminSession>, database: &State<Database>, _federation: &State<Federation>, incremental: bool, @@ -121,7 +121,7 @@ pub async fn r_admin_import( #[post("/admin/update_search")] pub async fn r_admin_update_search( - _session: AdminSession, + _session: A<AdminSession>, database: &State<Database>, ) -> MyResult<Redirect> { let db2 = (*database).clone(); @@ -141,7 +141,7 @@ pub async fn r_admin_update_search( #[post("/admin/delete_cache")] pub async fn r_admin_delete_cache( - session: AdminSession, + session: A<AdminSession>, database: &State<Database>, ) -> MyResult<Redirect> { drop(session); @@ -166,7 +166,7 @@ fn is_transcoding() -> bool { #[post("/admin/transcode_posters")] pub async fn r_admin_transcode_posters( - session: AdminSession, + session: A<AdminSession>, database: &State<Database>, ) -> MyResult<Redirect> { drop(session); diff --git a/server/src/ui/admin/user.rs b/server/src/ui/admin/user.rs index 77bcc71..fb646ab 100644 --- a/server/src/ui/admin/user.rs +++ b/server/src/ui/admin/user.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, locale::AcceptLanguage, ui::error::MyResult}; +use crate::{database::Database, helper::A, locale::AcceptLanguage, ui::error::MyResult}; use anyhow::{anyhow, Context}; use jellycommon::user::UserPermission; use jellyimport::is_importing; @@ -17,12 +17,12 @@ use rocket::{form::Form, get, post, response::content::RawHtml, FromForm, FromFo #[get("/admin/users")] pub fn r_admin_users( - session: AdminSession, + session: A<AdminSession>, database: &State<Database>, lang: AcceptLanguage, ) -> MyResult<RawHtml<String>> { let AcceptLanguage(lang) = lang; - let r = admin_users(database, &session)?; + let r = admin_users(database, &session.0)?; Ok(RawHtml(render_page( &AdminUsersPage { flash: None, @@ -32,7 +32,7 @@ pub fn r_admin_users( RenderInfo { importing: is_importing(), session: Some(SessionInfo { - user: session.0.user, + user: session.0 .0.user, }), }, lang, @@ -41,7 +41,7 @@ pub fn r_admin_users( #[get("/admin/user/<name>")] pub fn r_admin_user<'a>( - session: AdminSession, + session: A<AdminSession>, database: &State<Database>, name: &'a str, lang: AcceptLanguage, @@ -60,7 +60,7 @@ pub fn r_admin_user<'a>( RenderInfo { importing: is_importing(), session: Some(SessionInfo { - user: session.0.user, + user: session.0 .0.user, }), }, lang, @@ -82,7 +82,7 @@ pub enum GrantState { #[post("/admin/user/<name>/update_permission", data = "<form>")] pub fn r_admin_user_permission( - session: AdminSession, + session: A<AdminSession>, database: &State<Database>, form: Form<UserPermissionForm>, name: &str, @@ -92,7 +92,7 @@ pub fn r_admin_user_permission( let perm = serde_json::from_str::<UserPermission>(&form.permission) .context("parsing provided permission")?; - database.update_user(&form.name, |user| { + database.update_user(name, |user| { match form.action { GrantState::Grant => drop(user.permissions.0.insert(perm.clone(), true)), GrantState::Revoke => drop(user.permissions.0.insert(perm.clone(), false)), @@ -114,7 +114,7 @@ pub fn r_admin_user_permission( RenderInfo { importing: is_importing(), session: Some(SessionInfo { - user: session.0.user, + user: session.0 .0.user, }), }, lang, @@ -123,7 +123,7 @@ pub fn r_admin_user_permission( #[post("/admin/<name>/remove")] pub fn r_admin_remove_user( - session: AdminSession, + session: A<AdminSession>, database: &State<Database>, name: &str, lang: AcceptLanguage, @@ -132,7 +132,7 @@ pub fn r_admin_remove_user( if !database.delete_user(&name)? { Err(anyhow!("user did not exist"))?; } - let r = admin_users(database, &session)?; + let r = admin_users(database, &session.0)?; Ok(RawHtml(render_page( &AdminUsersPage { @@ -143,7 +143,7 @@ pub fn r_admin_remove_user( RenderInfo { importing: is_importing(), session: Some(SessionInfo { - user: session.0.user, + user: session.0 .0.user, }), }, lang, diff --git a/server/src/ui/assets.rs b/server/src/ui/assets.rs index ecab3d3..3b9319e 100644 --- a/server/src/ui/assets.rs +++ b/server/src/ui/assets.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::error::MyResult; -use crate::helper::cache::CacheControlFile; +use crate::helper::{cache::CacheControlFile, A}; use anyhow::{anyhow, bail, Context}; use base64::Engine; use jellybase::{assetfed::AssetInner, database::Database, federation::Federation, CONF}; @@ -20,7 +20,7 @@ pub const AVIF_SPEED: u8 = 5; #[get("/asset/<token>?<width>")] pub async fn r_asset( - _session: Session, + _session: A<Session>, fed: &State<Federation>, token: &str, width: Option<usize>, @@ -63,13 +63,13 @@ pub async fn resolve_asset(asset: AssetInner) -> anyhow::Result<PathBuf> { #[get("/n/<id>/poster?<width>")] pub async fn r_item_poster( - _session: Session, + _session: A<Session>, db: &State<Database>, - id: NodeID, + id: A<NodeID>, width: Option<usize>, ) -> MyResult<Redirect> { // TODO perm - let node = db.get_node(id)?.ok_or(anyhow!("node does not exist"))?; + let node = db.get_node(id.0)?.ok_or(anyhow!("node does not exist"))?; let mut asset = node.poster.clone(); if asset.is_none() { @@ -86,13 +86,13 @@ pub async fn r_item_poster( #[get("/n/<id>/backdrop?<width>")] pub async fn r_item_backdrop( - _session: Session, + _session: A<Session>, db: &State<Database>, - id: NodeID, + id: A<NodeID>, width: Option<usize>, ) -> MyResult<Redirect> { // TODO perm - let node = db.get_node(id)?.ok_or(anyhow!("node does not exist"))?; + let node = db.get_node(id.0)?.ok_or(anyhow!("node does not exist"))?; let mut asset = node.backdrop.clone(); if asset.is_none() { @@ -109,16 +109,16 @@ pub async fn r_item_backdrop( #[get("/n/<id>/person/<index>/asset?<group>&<width>")] pub async fn r_person_asset( - _session: Session, + _session: A<Session>, db: &State<Database>, - id: NodeID, + id: A<NodeID>, index: usize, group: String, width: Option<usize>, ) -> MyResult<Redirect> { // TODO perm - let node = db.get_node(id)?.ok_or(anyhow!("node does not exist"))?; + let node = db.get_node(id.0)?.ok_or(anyhow!("node does not exist"))?; let app = node .people .get(&PeopleGroup::from_str_opt(&group).ok_or(anyhow!("unknown people group"))?) @@ -136,14 +136,14 @@ pub async fn r_person_asset( #[get("/n/<id>/thumbnail?<t>&<width>")] pub async fn r_node_thumbnail( - _session: Session, + _session: A<Session>, db: &State<Database>, fed: &State<Federation>, - id: NodeID, + id: A<NodeID>, t: f64, width: Option<usize>, ) -> MyResult<Redirect> { - let node = db.get_node(id)?.ok_or(anyhow!("node does not exist"))?; + let node = db.get_node(id.0)?.ok_or(anyhow!("node does not exist"))?; let media = node.media.as_ref().ok_or(anyhow!("no media"))?; let (thumb_track_index, thumb_track) = media @@ -184,8 +184,8 @@ pub async fn r_node_thumbnail( ) .await?; - async_cache_file("fed-thumb", (id, t as i64), |out| { - session.node_thumbnail(out, id.into(), 2048, t) + async_cache_file("fed-thumb", (id.0, t as i64), |out| { + session.node_thumbnail(out, id.0.into(), 2048, t) }) .await? } diff --git a/server/src/ui/home.rs b/server/src/ui/home.rs index 6127e8c..9c9c1ca 100644 --- a/server/src/ui/home.rs +++ b/server/src/ui/home.rs @@ -5,7 +5,7 @@ */ use super::error::MyResult; -use crate::{api::AcceptJson, locale::AcceptLanguage}; +use crate::{api::AcceptJson, helper::A, locale::AcceptLanguage}; use jellybase::database::Database; use jellycommon::api::ApiHomeResponse; use jellyimport::is_importing; @@ -15,20 +15,18 @@ use jellyui::{ render_page, scaffold::{RenderInfo, SessionInfo}, }; -use rocket::{ - figment::value::magic::Either, get, response::content::RawHtml, serde::json::Json, State, -}; +use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; #[get("/home")] pub fn r_home( - session: Session, + session: A<Session>, db: &State<Database>, aj: AcceptJson, lang: AcceptLanguage, ) -> MyResult<Either<RawHtml<String>, Json<ApiHomeResponse>>> { let AcceptLanguage(lang) = lang; - let r = jellylogic::home::home(&db, &session)?; + let r = jellylogic::home::home(&db, &session.0)?; Ok(if *aj { Either::Right(Json(r)) @@ -37,7 +35,9 @@ pub fn r_home( &HomePage { lang: &lang, r }, RenderInfo { importing: is_importing(), - session: Some(SessionInfo { user: session.user }), + session: Some(SessionInfo { + user: session.0.user, + }), }, lang, ))) diff --git a/server/src/ui/items.rs b/server/src/ui/items.rs index c7d062d..e5aa050 100644 --- a/server/src/ui/items.rs +++ b/server/src/ui/items.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::error::MyError; -use crate::{api::AcceptJson, database::Database, locale::AcceptLanguage}; +use crate::{api::AcceptJson, database::Database, helper::A, locale::AcceptLanguage}; use jellycommon::api::{ApiItemsResponse, NodeFilterSort}; use jellyimport::is_importing; use jellylogic::{items::all_items, session::Session}; @@ -17,16 +17,16 @@ use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; #[get("/items?<page>&<filter..>")] pub fn r_items( - session: Session, + session: A<Session>, db: &State<Database>, aj: AcceptJson, page: Option<usize>, - filter: NodeFilterSort, + filter: A<NodeFilterSort>, lang: AcceptLanguage, ) -> Result<Either<RawHtml<String>, Json<ApiItemsResponse>>, MyError> { let AcceptLanguage(lang) = lang; - let r = all_items(db, &session, page, filter.clone())?; + let r = all_items(db, &session.0, page, filter.0.clone())?; Ok(if *aj { Either::Right(Json(r)) @@ -35,12 +35,14 @@ pub fn r_items( &ItemsPage { lang: &lang, r, - filter: &filter, + filter: &filter.0, page: page.unwrap_or(0), }, RenderInfo { importing: is_importing(), - session: Some(SessionInfo { user: session.user }), + session: Some(SessionInfo { + user: session.0.user, + }), }, lang, ))) diff --git a/server/src/ui/mod.rs b/server/src/ui/mod.rs index f59118e..17a8b6f 100644 --- a/server/src/ui/mod.rs +++ b/server/src/ui/mod.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::locale::AcceptLanguage; +use crate::{helper::A, locale::AcceptLanguage}; use error::MyResult; use home::rocket_uri_macro_r_home; use jellybase::CONF; @@ -36,7 +36,7 @@ pub mod style; #[get("/")] pub async fn r_index( lang: AcceptLanguage, - sess: Option<Session>, + sess: Option<A<Session>>, ) -> MyResult<Either<Redirect, RawHtml<String>>> { let AcceptLanguage(lang) = lang; if sess.is_some() { diff --git a/server/src/ui/node.rs b/server/src/ui/node.rs index 1a0ff16..6241f4f 100644 --- a/server/src/ui/node.rs +++ b/server/src/ui/node.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::error::MyResult; -use crate::{api::AcceptJson, database::Database, locale::AcceptLanguage}; +use crate::{api::AcceptJson, database::Database, helper::A, locale::AcceptLanguage}; use jellycommon::{ api::{ApiNodeResponse, NodeFilterSort}, NodeID, @@ -20,11 +20,11 @@ use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; #[get("/n/<id>?<parents>&<children>&<filter..>")] pub async fn r_node<'a>( - session: Session, - id: NodeID, + session: A<Session>, + id: A<NodeID>, db: &'a State<Database>, aj: AcceptJson, - filter: NodeFilterSort, + filter: A<NodeFilterSort>, lang: AcceptLanguage, parents: bool, children: bool, @@ -33,11 +33,11 @@ pub async fn r_node<'a>( let r = get_node( &db, - id, - &session, + id.0, + &session.0, !*aj || children, !*aj || parents, - filter.clone(), + filter.0.clone(), )?; Ok(if *aj { @@ -50,13 +50,15 @@ pub async fn r_node<'a>( children: &r.children, parents: &r.parents, similar: &[], - filter: &filter, + filter: &filter.0, lang: &lang, player: false, }, RenderInfo { importing: is_importing(), - session: Some(SessionInfo { user: session.user }), + session: Some(SessionInfo { + user: session.0.user, + }), }, lang, ))) diff --git a/server/src/ui/player.rs b/server/src/ui/player.rs index 573530b..94ca6ac 100644 --- a/server/src/ui/player.rs +++ b/server/src/ui/player.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::error::MyResult; -use crate::{database::Database, locale::AcceptLanguage}; +use crate::{database::Database, helper::A, locale::AcceptLanguage}; use jellybase::CONF; use jellycommon::{ api::NodeFilterSort, @@ -24,6 +24,7 @@ use rocket::{ response::{content::RawHtml, Redirect}, Either, State, }; +use std::time::Duration; fn jellynative_url(action: &str, seek: f64, secret: &str, node: &str, session: &str) -> String { let protocol = if CONF.tls { "https" } else { "http" }; @@ -41,31 +42,38 @@ fn jellynative_url(action: &str, seek: f64, secret: &str, node: &str, session: & #[get("/n/<id>/player?<t>", rank = 4)] pub fn r_player( - session: Session, + session: A<Session>, lang: AcceptLanguage, db: &State<Database>, t: Option<f64>, - id: NodeID, + id: A<NodeID>, ) -> MyResult<Either<RawHtml<String>, Redirect>> { let AcceptLanguage(lang) = lang; - let r = get_node(&db, id, &session, false, true, NodeFilterSort::default())?; + let r = get_node( + &db, + id.0, + &session.0, + false, + true, + NodeFilterSort::default(), + )?; let native_session = |action: &str| { Ok(Either::Right(Redirect::temporary(jellynative_url( action, t.unwrap_or(0.), - &session.user.native_secret, - &id.to_string(), + &session.0.user.native_secret, + &id.0.to_string(), &jellylogic::session::create( - session.user.name, + session.0.user.name.clone(), PermissionSet::default(), // TODO - chrono::Duration::hours(24), + Duration::from_hours(24), ), )))) }; - match session.user.player_preference { + match session.0.user.player_preference { PlayerKind::Browser => (), PlayerKind::Native => { return native_session("player-v2"); @@ -103,7 +111,9 @@ pub fn r_player( }, RenderInfo { importing: is_importing(), - session: Some(SessionInfo { user: session.user }), + session: Some(SessionInfo { + user: session.0.user, + }), }, lang, )))) diff --git a/server/src/ui/search.rs b/server/src/ui/search.rs index bacaaee..1c2ea70 100644 --- a/server/src/ui/search.rs +++ b/server/src/ui/search.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::error::MyResult; -use crate::{api::AcceptJson, locale::AcceptLanguage}; +use crate::{api::AcceptJson, helper::A, locale::AcceptLanguage}; use anyhow::anyhow; use jellybase::database::Database; use jellycommon::api::ApiSearchResponse; @@ -19,7 +19,7 @@ use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; #[get("/search?<query>&<page>")] pub async fn r_search<'a>( - session: Session, + session: A<Session>, db: &State<Database>, aj: AcceptJson, query: Option<&str>, @@ -29,7 +29,7 @@ pub async fn r_search<'a>( let AcceptLanguage(lang) = lang; let r = query - .map(|query| search(db, &session, query, page)) + .map(|query| search(db, &session.0, query, page)) .transpose()?; Ok(if *aj { @@ -46,7 +46,9 @@ pub async fn r_search<'a>( }, RenderInfo { importing: is_importing(), - session: Some(SessionInfo { user: session.user }), + session: Some(SessionInfo { + user: session.0.user, + }), }, lang, ))) diff --git a/server/src/ui/stats.rs b/server/src/ui/stats.rs index 8bfecbf..b6a74e5 100644 --- a/server/src/ui/stats.rs +++ b/server/src/ui/stats.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::error::MyError; -use crate::{api::AcceptJson, database::Database, locale::AcceptLanguage}; +use crate::{api::AcceptJson, database::Database, helper::A, locale::AcceptLanguage}; use jellycommon::api::ApiStatsResponse; use jellyimport::is_importing; use jellylogic::{session::Session, stats::stats}; @@ -17,13 +17,13 @@ use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; #[get("/stats")] pub fn r_stats( - session: Session, + session: A<Session>, db: &State<Database>, aj: AcceptJson, lang: AcceptLanguage, ) -> Result<Either<RawHtml<String>, Json<ApiStatsResponse>>, MyError> { let AcceptLanguage(lang) = lang; - let r = stats(db, &session)?; + let r = stats(db, &session.0)?; Ok(if *aj { Either::Right(Json(r)) @@ -32,7 +32,9 @@ pub fn r_stats( &StatsPage { lang: &lang, r }, RenderInfo { importing: is_importing(), - session: Some(SessionInfo { user: session.user }), + session: Some(SessionInfo { + user: session.0.user, + }), }, lang, ))) diff --git a/ui/src/account/mod.rs b/ui/src/account/mod.rs index bc8d3ce..36a41c5 100644 --- a/ui/src/account/mod.rs +++ b/ui/src/account/mod.rs @@ -3,8 +3,55 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::locale::{Language, tr, trs}; -use jellycommon::routes::u_account_login; +pub mod settings; + +use crate::{ + Page, + locale::{Language, tr, trs}, +}; +use jellycommon::routes::{u_account_login, u_account_register}; + +impl Page for AccountLogin<'_> { + fn title(&self) -> String { + tr( + *self.lang, + if self.logged_in { + "account.login.switch" + } else { + "account.login" + }, + ) + .to_string() + } + + fn to_render(&self) -> markup::DynRender { + markup::new!(@self) + } +} +impl Page for AccountRegister<'_> { + fn title(&self) -> String { + tr(*self.lang, "account.register").to_string() + } + fn to_render(&self) -> markup::DynRender { + markup::new!(@self) + } +} +impl Page for AccountRegisterSuccess<'_> { + fn title(&self) -> String { + tr(*self.lang, "account.register").to_string() + } + fn to_render(&self) -> markup::DynRender { + markup::new!(@self) + } +} +impl Page for AccountLogout<'_> { + fn title(&self) -> String { + tr(*self.lang, "account.logout").to_string() + } + fn to_render(&self) -> markup::DynRender { + markup::new!(@self) + } +} markup::define! { AccountRegister<'a>(lang: &'a Language) { @@ -24,4 +71,36 @@ markup::define! { p { @trs(&lang, "account.register.login") " " a[href=u_account_login()] { @trs(&lang, "account.register.login_here") } } } } + AccountRegisterSuccess<'a>(lang: &'a Language, logged_in: bool) { + h1 { @trs(&lang, if *logged_in { + "account.register.success.switch" + } else { + "account.register.success" + })} + } + AccountLogin<'a>(lang: &'a Language, logged_in: bool) { + form.account[method="POST", action=""] { + h1 { @self.title() } + + 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, if *logged_in { "account.login.submit.switch" } else { "account.login.submit" })]; + + @if *logged_in { + p { @trs(&lang, "account.login.register.switch") " " a[href=u_account_register()] { @trs(&lang, "account.login.register_here") } } + } else { + p { @trs(&lang, "account.login.cookie_note") } + p { @trs(&lang, "account.login.register") " " a[href=u_account_register()] { @trs(&lang, "account.login.register_here") } } + } + } + } + AccountLogout<'a>(lang: &'a Language) { + form.account[method="POST", action=""] { + h1 { @trs(&lang, "account.logout") } + input[type="submit", value=&*tr(**lang, "account.logout.submit")]; + } + } } diff --git a/ui/src/settings.rs b/ui/src/account/settings.rs index 5ff3946..7c5a3b8 100644 --- a/ui/src/settings.rs +++ b/ui/src/account/settings.rs @@ -4,6 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use crate::{ + Page, locale::{Language, tr, trs}, scaffold::SessionInfo, }; @@ -13,8 +14,17 @@ use jellycommon::{ }; use markup::RenderAttributeValue; +impl Page for SettingsPage<'_> { + fn title(&self) -> String { + format!("Settings") + } + fn to_render(&self) -> markup::DynRender { + markup::new!(@self) + } +} + markup::define! { - Settings<'a>(flash: Option<Result<String, String>>, session: &'a SessionInfo, lang: &'a Language) { + SettingsPage<'a>(flash: Option<Result<String, String>>, session: &'a SessionInfo, lang: &'a Language) { h1 { "Settings" } @if let Some(flash) = &flash { @match flash { diff --git a/ui/src/admin/log.rs b/ui/src/admin/log.rs index a69bdfa..3669571 100644 --- a/ui/src/admin/log.rs +++ b/ui/src/admin/log.rs @@ -4,6 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ +use crate::Page; use jellycommon::{ api::{LogLevel, LogLine}, routes::u_admin_log, @@ -11,6 +12,15 @@ use jellycommon::{ use markup::raw; use std::fmt::Write; +impl Page for ServerLogPage<'_> { + fn title(&self) -> String { + "Server Log".to_string() + } + fn to_render(&self) -> markup::DynRender { + markup::new!(@self) + } +} + markup::define! { ServerLogPage<'a>(warnonly: bool, messages: &'a [String]) { h1 { "Server Log" } diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 8a4b950..4f1901a 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -12,7 +12,6 @@ pub mod node_page; pub mod props; pub mod scaffold; pub mod search; -pub mod settings; pub mod stats; pub mod items; pub mod admin; diff --git a/ui/src/stats.rs b/ui/src/stats.rs index c3e5a14..11163f3 100644 --- a/ui/src/stats.rs +++ b/ui/src/stats.rs @@ -5,6 +5,7 @@ */ use crate::{ + Page, format::{format_duration, format_duration_long, format_kind, format_size}, locale::{Language, tr, trs}, }; @@ -14,6 +15,15 @@ use jellycommon::{ }; use markup::raw; +impl Page for StatsPage<'_> { + fn title(&self) -> String { + tr(*self.lang, "stats.title").to_string() + } + fn to_render(&self) -> markup::DynRender { + markup::new!(@self) + } +} + markup::define! { StatsPage<'a>(lang: &'a Language, r: ApiStatsResponse) { .page.stats { @@ -57,12 +67,6 @@ markup::define! { } } -impl StatsPage<'_> { - pub fn title(&self) -> String { - tr(*self.lang, "stats.title").to_string() - } -} - trait BinExt { fn average_runtime(&self) -> f64; fn average_size(&self) -> f64; |