aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-02-25 13:25:41 +0100
committermetamuffin <metamuffin@disroot.org>2026-02-25 13:25:41 +0100
commit9c08495cca8b9aaf297c88da9ec98a619eb90762 (patch)
treefedb0ba09e45c51f7f1b9d5ce9ea5b9a47ce6f01
parent6949f8d40d1784d5a9c54dbe44e212fe2fae76f4 (diff)
downloadjellything-9c08495cca8b9aaf297c88da9ec98a619eb90762.tar
jellything-9c08495cca8b9aaf297c88da9ec98a619eb90762.tar.bz2
jellything-9c08495cca8b9aaf297c88da9ec98a619eb90762.tar.zst
user creation; flash message in request info
-rw-r--r--common/object/src/buffer.rs9
-rw-r--r--common/src/routes.rs21
-rw-r--r--common/src/user.rs1
-rw-r--r--server/src/request_info.rs4
-rw-r--r--server/src/routes.rs17
-rw-r--r--server/src/ui/account/mod.rs12
-rw-r--r--server/src/ui/account/settings.rs19
-rw-r--r--server/src/ui/admin/import.rs6
-rw-r--r--server/src/ui/admin/mod.rs74
-rw-r--r--server/src/ui/home.rs2
-rw-r--r--server/src/ui/node.rs10
-rw-r--r--server/src/ui/player.rs10
-rw-r--r--server/src/ui_responder.rs18
-rw-r--r--ui/src/components/admin.rs9
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) } }
}}