aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--logic/src/lib.rs1
-rw-r--r--logic/src/login.rs61
-rw-r--r--server/src/api.rs13
-rw-r--r--server/src/compat/jellyfin/mod.rs8
-rw-r--r--server/src/helper/filter_sort.rs45
-rw-r--r--server/src/helper/mod.rs4
-rw-r--r--server/src/main.rs5
-rw-r--r--server/src/routes.rs6
-rw-r--r--server/src/ui/account/mod.rs171
-rw-r--r--server/src/ui/account/settings.rs47
-rw-r--r--server/src/ui/admin/log.rs41
-rw-r--r--server/src/ui/admin/mod.rs18
-rw-r--r--server/src/ui/admin/user.rs24
-rw-r--r--server/src/ui/assets.rs32
-rw-r--r--server/src/ui/home.rs14
-rw-r--r--server/src/ui/items.rs14
-rw-r--r--server/src/ui/mod.rs4
-rw-r--r--server/src/ui/node.rs20
-rw-r--r--server/src/ui/player.rs30
-rw-r--r--server/src/ui/search.rs10
-rw-r--r--server/src/ui/stats.rs10
-rw-r--r--ui/src/account/mod.rs83
-rw-r--r--ui/src/account/settings.rs (renamed from ui/src/settings.rs)12
-rw-r--r--ui/src/admin/log.rs10
-rw-r--r--ui/src/lib.rs1
-rw-r--r--ui/src/stats.rs16
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;