diff options
| author | metamuffin <metamuffin@disroot.org> | 2026-02-25 13:25:41 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2026-02-25 13:25:41 +0100 |
| commit | 9c08495cca8b9aaf297c88da9ec98a619eb90762 (patch) | |
| tree | fedb0ba09e45c51f7f1b9d5ce9ea5b9a47ce6f01 | |
| parent | 6949f8d40d1784d5a9c54dbe44e212fe2fae76f4 (diff) | |
| download | jellything-9c08495cca8b9aaf297c88da9ec98a619eb90762.tar jellything-9c08495cca8b9aaf297c88da9ec98a619eb90762.tar.bz2 jellything-9c08495cca8b9aaf297c88da9ec98a619eb90762.tar.zst | |
user creation; flash message in request info
| -rw-r--r-- | common/object/src/buffer.rs | 9 | ||||
| -rw-r--r-- | common/src/routes.rs | 21 | ||||
| -rw-r--r-- | common/src/user.rs | 1 | ||||
| -rw-r--r-- | server/src/request_info.rs | 4 | ||||
| -rw-r--r-- | server/src/routes.rs | 17 | ||||
| -rw-r--r-- | server/src/ui/account/mod.rs | 12 | ||||
| -rw-r--r-- | server/src/ui/account/settings.rs | 19 | ||||
| -rw-r--r-- | server/src/ui/admin/import.rs | 6 | ||||
| -rw-r--r-- | server/src/ui/admin/mod.rs | 74 | ||||
| -rw-r--r-- | server/src/ui/home.rs | 2 | ||||
| -rw-r--r-- | server/src/ui/node.rs | 10 | ||||
| -rw-r--r-- | server/src/ui/player.rs | 10 | ||||
| -rw-r--r-- | server/src/ui_responder.rs | 18 | ||||
| -rw-r--r-- | ui/src/components/admin.rs | 9 |
14 files changed, 128 insertions, 84 deletions
diff --git a/common/object/src/buffer.rs b/common/object/src/buffer.rs index 703e203..1f8cec6 100644 --- a/common/object/src/buffer.rs +++ b/common/object/src/buffer.rs @@ -54,9 +54,14 @@ impl ObjectBuffer { } } +pub type OBB = ObjectBufferBuilder; + #[derive(Default)] pub struct ObjectBufferBuilder(Vec<(Tag, u32, Vec<u32>)>); impl ObjectBufferBuilder { + pub fn new() -> Self { + Self::default() + } pub fn push<T: ValueStore>(&mut self, tag: TypedTag<T>, value: T) { let ty = value.get_type(); let tyb = (ty as u32) << 2; @@ -71,6 +76,10 @@ impl ObjectBufferBuilder { self.0.push((tag.0, tyb | pad, vec_u8_to_u32(buf))); } } + pub fn with<T: ValueStore>(mut self, tag: TypedTag<T>, value: T) -> Self { + self.push(tag, value); + self + } pub fn finish(mut self) -> ObjectBuffer { let mut tags = Vec::new(); let mut offsets = Vec::new(); diff --git a/common/src/routes.rs b/common/src/routes.rs index 3266745..b2a10b1 100644 --- a/common/src/routes.rs +++ b/common/src/routes.rs @@ -52,33 +52,15 @@ pub fn u_items_filter(page: usize) -> String { pub fn u_admin_users() -> String { "/admin/users".to_string() } -pub fn u_admin_user(name: &str) -> String { - format!("/admin/user/{name}") -} -pub fn u_admin_user_permission(name: &str) -> String { - format!("/admin/user/{name}/update_permissions") -} -pub fn u_admin_user_remove(name: &str) -> String { - format!("/admin/user/{name}/remove") -} pub fn u_admin_log(warn_only: bool) -> String { format!("/admin/log?warn_only={warn_only}") } -pub fn u_admin_invite_create() -> String { - "/admin/generate_invite".to_string() -} -pub fn u_admin_invite_remove() -> String { - "/admin/remove_invite".to_string() -} pub fn u_admin_import() -> String { format!("/admin/import") } pub fn u_admin_import_post(incremental: bool) -> String { format!("/admin/import?incremental={incremental}") } -pub fn u_admin_update_search() -> String { - "/admin/update_search".to_string() -} pub fn u_account_login() -> String { "/account/login".to_owned() } @@ -88,6 +70,9 @@ pub fn u_account_logout() -> String { pub fn u_admin_dashboard() -> String { "/admin/dashboard".to_owned() } +pub fn u_admin_new_user() -> String { + "/admin/new_user".to_owned() +} pub fn u_account_settings() -> String { "/account/settings".to_owned() } diff --git a/common/src/user.rs b/common/src/user.rs index 0a4111d..636046f 100644 --- a/common/src/user.rs +++ b/common/src/user.rs @@ -9,6 +9,7 @@ use jellyobject::{Tag, enums, fields}; fields! { USER_LOGIN: &str = b"Ulgn"; USER_PASSWORD: &[u8] = b"Upwd"; + USER_PASSWORD_REQUIRE_CHANGE: () = b"Upwc"; USER_NAME: &str = b"Unam"; USER_ADMIN: () = b"Uadm"; USER_THEME_PRESET: Tag = b"Utpr"; diff --git a/server/src/request_info.rs b/server/src/request_info.rs index 49b416a..4a0a781 100644 --- a/server/src/request_info.rs +++ b/server/src/request_info.rs @@ -18,7 +18,7 @@ use jellyui::RenderInfo; use rocket::{ Request, async_trait, http::{MediaType, Status}, - request::{FromRequest, Outcome}, + request::{FlashMessage, FromRequest, Outcome}, }; use std::sync::Arc; @@ -28,6 +28,7 @@ pub struct RequestInfo<'a> { pub debug: &'a str, pub user: Option<ObjectBuffer>, pub state: Arc<State>, + pub flash: Option<FlashMessage<'a>>, } #[async_trait] @@ -54,6 +55,7 @@ impl<'a> RequestInfo<'a> { .transpose() .unwrap() .unwrap_or("none"), + flash: FlashMessage::from_request(request).await.succeeded(), }) } pub fn require_user(&'a self) -> MyResult<Object<'a>> { diff --git a/server/src/routes.rs b/server/src/routes.rs index 7068fe0..0df3aa7 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -19,7 +19,7 @@ use crate::{ admin::{ import::{r_admin_import, r_admin_import_post, r_admin_import_stream}, log::{r_admin_log, r_admin_log_stream}, - r_admin_dashboard, r_admin_users, + r_admin_dashboard, r_admin_new_user, r_admin_users, }, assets::{r_image, r_image_fallback_person}, error::{r_api_catch, r_catch}, @@ -82,28 +82,29 @@ pub(super) fn build_rocket(state: Arc<State>) -> Rocket<Build> { r_account_settings_post, r_account_settings, r_admin_dashboard, - r_admin_import, r_admin_import_post, r_admin_import_stream, + r_admin_import, r_admin_log_stream, r_admin_log, + r_admin_new_user, r_admin_users, - r_image, - r_image_fallback_person, + r_api_root, + r_assets_css, r_assets_font, r_assets_js_map, r_assets_js, - r_assets_css, r_favicon, r_home, + r_image_fallback_person, + r_image, r_index, - r_user, - r_user_remove, r_node, r_player, r_playersync, r_stream, - r_api_root, + r_user_remove, + r_user, r_version, // Compat // r_jellyfin_artists, diff --git a/server/src/ui/account/mod.rs b/server/src/ui/account/mod.rs index 448f91e..1c44914 100644 --- a/server/src/ui/account/mod.rs +++ b/server/src/ui/account/mod.rs @@ -12,7 +12,11 @@ use crate::{ auth::login, request_info::RequestInfo, ui::error::MyResult, ui_responder::UiResponse, }; use anyhow::anyhow; -use jellycommon::{VIEW_ACCOUNT_LOGIN, VIEW_ACCOUNT_LOGOUT, jellyobject::Object, routes::u_home}; +use jellycommon::{ + VIEW_ACCOUNT_LOGIN, VIEW_ACCOUNT_LOGOUT, + jellyobject::OBB, + routes::{u_account_login, u_home}, +}; use rocket::{ FromForm, form::{Contextual, Form}, @@ -25,12 +29,12 @@ use serde::{Deserialize, Serialize}; #[get("/account/login")] pub async fn r_account_login(ri: RequestInfo<'_>) -> UiResponse { - ri.respond_ui(Object::EMPTY.insert(VIEW_ACCOUNT_LOGIN, ())) + ri.respond_ui(OBB::new().with(VIEW_ACCOUNT_LOGIN, ())) } #[get("/account/logout")] pub fn r_account_logout(ri: RequestInfo<'_>) -> UiResponse { - ri.respond_ui(Object::EMPTY.insert(VIEW_ACCOUNT_LOGOUT, ())) + ri.respond_ui(OBB::new().with(VIEW_ACCOUNT_LOGOUT, ())) } #[derive(FromForm, Serialize, Deserialize)] @@ -63,7 +67,7 @@ pub fn r_account_login_post( pub fn r_account_logout_post(jar: &CookieJar) -> MyResult<Flash<Redirect>> { jar.remove(Cookie::build("session")); Ok(Flash::new( - Redirect::found(u_home()), + Redirect::found(u_account_login()), "success", "Logged out!", )) diff --git a/server/src/ui/account/settings.rs b/server/src/ui/account/settings.rs index 2052c5e..bb4b323 100644 --- a/server/src/ui/account/settings.rs +++ b/server/src/ui/account/settings.rs @@ -9,7 +9,7 @@ use crate::{ }; use anyhow::anyhow; use jellycommon::{ - jellyobject::{Object, ObjectBuffer, ObjectBufferBuilder, Path, Tag}, + jellyobject::{OBB, Object, ObjectBuffer, Path, Tag}, routes::u_account_settings, *, }; @@ -19,7 +19,6 @@ use rocket::{ FromForm, form::{self, Contextual, Form, validate::len}, get, post, - request::FlashMessage, response::{Flash, Redirect}, }; use std::ops::Range; @@ -39,21 +38,9 @@ fn option_len<'v>(value: &Option<String>, range: Range<usize>) -> form::Result<' } #[get("/account/settings")] -pub fn r_account_settings(ri: RequestInfo, flash: Option<FlashMessage>) -> MyResult<UiResponse> { +pub fn r_account_settings(ri: RequestInfo) -> MyResult<UiResponse> { let user = ri.require_user()?; - let mut view = ObjectBufferBuilder::default(); - view.push(VIEW_USER_SETTINGS, user); - if let Some(flash) = flash { - view.push( - VIEW_MESSAGE, - ObjectBuffer::new(&mut [ - (MESSAGE_KIND.0, &flash.kind()), - (MESSAGE_TEXT.0, &flash.message()), - ]) - .as_object(), - ); - } - Ok(ri.respond_ui(view.finish())) + Ok(ri.respond_ui(OBB::new().with(VIEW_USER_SETTINGS, user))) } #[post("/account/settings", data = "<form>")] diff --git a/server/src/ui/admin/import.rs b/server/src/ui/admin/import.rs index b263705..eba2a3b 100644 --- a/server/src/ui/admin/import.rs +++ b/server/src/ui/admin/import.rs @@ -6,9 +6,9 @@ use crate::{request_info::RequestInfo, ui::error::MyResult, ui_responder::UiResponse}; use jellycommon::{ - ADMIN_IMPORT_BUSY, ADMIN_IMPORT_ERROR, VIEW_ADMIN_IMPORT, - jellyobject::{Object, ObjectBuffer}, + jellyobject::{OBB, ObjectBuffer}, routes::u_admin_import, + *, }; use jellyimport::{ ImportConfig, import_wrap, is_importing, @@ -41,7 +41,7 @@ pub async fn r_admin_import(ri: RequestInfo<'_>) -> MyResult<UiResponse> { .as_object() .insert_multi(ADMIN_IMPORT_ERROR, &last_import_err); - Ok(ri.respond_ui(Object::EMPTY.insert(VIEW_ADMIN_IMPORT, data.as_object()))) + Ok(ri.respond_ui(OBB::new().with(VIEW_ADMIN_IMPORT, data.as_object()))) } #[post("/admin/import?<incremental>")] diff --git a/server/src/ui/admin/mod.rs b/server/src/ui/admin/mod.rs index 3fa0591..79c9241 100644 --- a/server/src/ui/admin/mod.rs +++ b/server/src/ui/admin/mod.rs @@ -8,14 +8,22 @@ pub mod import; pub mod log; use super::error::MyResult; -use crate::{request_info::RequestInfo, ui_responder::UiResponse}; +use crate::{auth::hash_password, request_info::RequestInfo, ui_responder::UiResponse}; +use base64::{Engine, prelude::BASE64_URL_SAFE}; use jellycommon::{ - jellyobject::{ObjectBuffer, ObjectBufferBuilder}, + jellyobject::{OBB, ObjectBuffer, ObjectBufferBuilder}, + routes::u_admin_users, *, }; use jellydb::Query; use jellyui::tr; -use rocket::get; +use rand::random; +use rocket::{ + FromForm, + form::Form, + get, post, + response::{Flash, Redirect}, +}; use std::str::FromStr; #[get("/admin/dashboard")] @@ -28,17 +36,18 @@ pub async fn r_admin_dashboard(ri: RequestInfo<'_>) -> MyResult<UiResponse> { Ok(()) })?; - Ok(ri.respond_ui(ObjectBuffer::new(&mut [ - (VIEW_TITLE.0, &&*tr(ri.lang, "admin.dashboard.title")), - (VIEW_ADMIN_DASHBOARD.0, &()), - ( - VIEW_ADMIN_INFO.0, - &ObjectBuffer::new(&mut [ - (ADMIN_INFO_TITLE.0, &"Database Debug"), - (ADMIN_INFO_TEXT.0, &db_debug.as_str()), - ]), - ), - ]))) + let mut page = OBB::new(); + page.push(VIEW_TITLE, &*tr(ri.lang, "admin.dashboard.title")); + page.push(VIEW_ADMIN_DASHBOARD, ()); + page.push( + VIEW_ADMIN_INFO, + ObjectBuffer::new(&mut [ + (ADMIN_INFO_TITLE.0, &"Database Debug"), + (ADMIN_INFO_TEXT.0, &db_debug.as_str()), + ]) + .as_object(), + ); + Ok(ri.respond_ui(page)) } #[get("/admin/users")] @@ -62,8 +71,37 @@ pub fn r_admin_users(ri: RequestInfo) -> MyResult<UiResponse> { for u in users { list.push(ADMIN_USER_LIST_ITEM, u.as_object()); } - Ok(ri.respond_ui(ObjectBuffer::new(&mut [( - VIEW_ADMIN_USER_LIST.0, - &list.finish().as_object(), - )]))) + + let mut page = ObjectBufferBuilder::default(); + page.push(VIEW_TITLE, &*tr(ri.lang, "admin.users")); + page.push(VIEW_ADMIN_USER_LIST, list.finish().as_object()); + Ok(ri.respond_ui(page)) +} + +#[derive(FromForm)] +pub struct NewUser { + login: String, +} + +#[post("/admin/new_user", data = "<form>")] +pub fn r_admin_new_user(ri: RequestInfo, form: Form<NewUser>) -> MyResult<Flash<Redirect>> { + ri.require_admin()?; + + let password = BASE64_URL_SAFE.encode([(); 12].map(|()| random())); + let password_hashed = hash_password(&form.login, &password); + + ri.state.database.transaction(&mut |txn| { + let mut user = ObjectBufferBuilder::default(); + user.push(USER_LOGIN, &form.login); + user.push(USER_PASSWORD, &password_hashed); + user.push(USER_PASSWORD_REQUIRE_CHANGE, ()); + txn.insert(user.finish())?; + Ok(()) + })?; + + Ok(Flash::new( + Redirect::to(u_admin_users()), + "success", + format!("User created; password: {password}"), + )) } diff --git a/server/src/ui/home.rs b/server/src/ui/home.rs index 67e6e90..1a7da36 100644 --- a/server/src/ui/home.rs +++ b/server/src/ui/home.rs @@ -71,7 +71,7 @@ pub fn r_home(ri: RequestInfo<'_>) -> MyResult<UiResponse> { .as_object(), ); - Ok(ri.respond_ui(page.finish())) + Ok(ri.respond_ui(page)) } fn home_row(ri: &RequestInfo<'_>, title: &str, query: &str) -> Result<ObjectBuffer> { diff --git a/server/src/ui/node.rs b/server/src/ui/node.rs index 14c90c1..509e9ae 100644 --- a/server/src/ui/node.rs +++ b/server/src/ui/node.rs @@ -8,7 +8,7 @@ use super::error::MyResult; use crate::{request_info::RequestInfo, ui_responder::UiResponse}; use anyhow::Result; use jellycommon::{ - jellyobject::{Object, ObjectBuffer, ObjectBufferBuilder, Path}, + jellyobject::{OBB, Object, ObjectBuffer, ObjectBufferBuilder, Path}, *, }; use jellydb::{Filter, MultiBehaviour, Query, Sort, SortOrder, Transaction, ValueSort}; @@ -19,7 +19,7 @@ use std::collections::BTreeMap; pub fn r_node(ri: RequestInfo<'_>, slug: &str) -> MyResult<UiResponse> { ri.require_user()?; - let mut page_out = ObjectBuffer::empty(); + let mut page = OBB::new(); ri.state.database.transaction(&mut |txn| { if let Some(row) = txn.query_single(Query { filter: Filter::Match(Path(vec![NO_SLUG.0]), slug.into()), @@ -29,7 +29,7 @@ pub fn r_node(ri: RequestInfo<'_>, slug: &str) -> MyResult<UiResponse> { let nku = Object::EMPTY.insert(NKU_NODE, n.as_object()); let nku = nku.as_object(); - let mut page = ObjectBufferBuilder::default(); + page = OBB::new(); let title = nku .get(NKU_NODE) .unwrap_or_default() @@ -41,13 +41,11 @@ pub fn r_node(ri: RequestInfo<'_>, slug: &str) -> MyResult<UiResponse> { c_children(&mut page, txn, row, &nku)?; c_credits(&mut page, txn, &nku)?; c_credited(&mut page, txn, row)?; - - page_out = page.finish(); } Ok(()) })?; - Ok(ri.respond_ui(page_out)) + Ok(ri.respond_ui(page)) } fn c_children( diff --git a/server/src/ui/player.rs b/server/src/ui/player.rs index a03e455..4c592e4 100644 --- a/server/src/ui/player.rs +++ b/server/src/ui/player.rs @@ -6,7 +6,7 @@ use super::error::MyResult; use crate::{request_info::RequestInfo, ui_responder::UiResponse}; use jellycommon::{ - jellyobject::{Object, ObjectBuffer, ObjectBufferBuilder, Path}, + jellyobject::{OBB, Object, Path}, *, }; use jellydb::{Filter, Query, Sort}; @@ -30,7 +30,7 @@ pub fn r_player(ri: RequestInfo<'_>, t: Option<f64>, slug: &str) -> MyResult<UiR ri.require_user()?; let _ = t; - let mut page_out = ObjectBuffer::empty(); + let mut page = OBB::new(); ri.state.database.transaction(&mut |txn| { if let Some(row) = txn.query_single(Query { filter: Filter::Match(Path(vec![NO_SLUG.0]), slug.into()), @@ -39,7 +39,7 @@ pub fn r_player(ri: RequestInfo<'_>, t: Option<f64>, slug: &str) -> MyResult<UiR let n = txn.get(row)?.unwrap(); let nku = Object::EMPTY.insert(NKU_NODE, n.as_object()); - let mut page = ObjectBufferBuilder::default(); + page = OBB::new(); let title = nku .as_object() .get(NKU_NODE) @@ -48,13 +48,11 @@ pub fn r_player(ri: RequestInfo<'_>, t: Option<f64>, slug: &str) -> MyResult<UiR .unwrap_or_default(); page.push(VIEW_TITLE, title); page.push(VIEW_PLAYER, nku.as_object()); - - page_out = page.finish(); } Ok(()) })?; - Ok(ri.respond_ui(page_out)) + Ok(ri.respond_ui(page)) } // pub fn player_conf<'a>(item: Arc<Node>, playing: bool) -> anyhow::Result<DynRender<'a>> { diff --git a/server/src/ui_responder.rs b/server/src/ui_responder.rs index eb503a1..2c4adea 100644 --- a/server/src/ui_responder.rs +++ b/server/src/ui_responder.rs @@ -5,7 +5,10 @@ */ use crate::request_info::RequestInfo; -use jellycommon::jellyobject::{ObjectBuffer, json::object_to_json}; +use jellycommon::{ + jellyobject::{ObjectBuffer, ObjectBufferBuilder, json::object_to_json}, + *, +}; use jellyui::render_view; use rocket::response::{ Responder, @@ -19,7 +22,18 @@ pub enum UiResponse { } impl RequestInfo<'_> { - pub fn respond_ui(&self, view: ObjectBuffer) -> UiResponse { + pub fn respond_ui(&self, mut view: ObjectBufferBuilder) -> UiResponse { + if let Some(flash) = &self.flash { + view.push( + VIEW_MESSAGE, + ObjectBuffer::new(&mut [ + (MESSAGE_KIND.0, &flash.kind()), + (MESSAGE_TEXT.0, &flash.message()), + ]) + .as_object(), + ); + } + let view = view.finish(); if self.accept.is_json() || self.debug == "json" { let value = object_to_json(view.as_object()); UiResponse::Json(serde_json::to_string(&value).unwrap()) diff --git a/ui/src/components/admin.rs b/ui/src/components/admin.rs index cd691a2..7e7ccdf 100644 --- a/ui/src/components/admin.rs +++ b/ui/src/components/admin.rs @@ -7,7 +7,9 @@ use crate::RenderInfo; use jellycommon::{ jellyobject::Object, - routes::{u_admin_import, u_admin_import_post, u_admin_log, u_admin_users, u_user}, + routes::{ + u_admin_import, u_admin_import_post, u_admin_log, u_admin_new_user, u_admin_users, u_user, + }, *, }; use jellyui_locale::tr; @@ -58,6 +60,11 @@ markup::define!( AdminUserList<'a>(ri: &'a RenderInfo<'a>, data: Object<'a>) { h1 { @tr(ri.lang, "admin.users") } + form[method="POST", action=u_admin_new_user()] { + label[for="login"] { "Login: " } + input[type="text", id="login", name="login"]; br; + input[type="submit", value="Create new user"]; + } ul { @for u in data.iter(ADMIN_USER_LIST_ITEM) { li { a[href=u_user(u.get(USER_LOGIN).unwrap_or_default())] { @u.get(USER_LOGIN) } } }} |