diff options
| -rw-r--r-- | common/src/api.rs | 3 | ||||
| -rw-r--r-- | common/src/routes.rs | 3 | ||||
| -rw-r--r-- | database/src/query_syntax.rs | 2 | ||||
| -rw-r--r-- | locale/en.ini | 6 | ||||
| -rw-r--r-- | server/src/routes.rs | 26 | ||||
| -rw-r--r-- | server/src/ui/admin/mod.rs | 34 | ||||
| -rw-r--r-- | server/src/ui/admin/user.rs | 93 | ||||
| -rw-r--r-- | server/src/ui/mod.rs | 1 | ||||
| -rw-r--r-- | server/src/ui/user.rs | 28 | ||||
| -rw-r--r-- | ui/src/components/admin.rs | 12 | ||||
| -rw-r--r-- | ui/src/components/mod.rs | 5 | ||||
| -rw-r--r-- | ui/src/components/user.rs | 5 | ||||
| -rw-r--r-- | ui/src/old/admin/user.rs | 81 |
13 files changed, 92 insertions, 207 deletions
diff --git a/common/src/api.rs b/common/src/api.rs index bec33ba..ea1a0c6 100644 --- a/common/src/api.rs +++ b/common/src/api.rs @@ -27,6 +27,7 @@ fields! { VIEW_ADMIN_IMPORT: Object = b"adim"; VIEW_ADMIN_INFO: Object = b"adin"; VIEW_ADMIN_LOG: Object = b"adlo"; + VIEW_ADMIN_USER_LIST: Object = b"adul"; VIEW_USER_SETTINGS: Object = b"uset"; ADMIN_IMPORT_BUSY: () = b"busy"; @@ -35,6 +36,8 @@ fields! { ADMIN_INFO_TEXT: &str = b"aite"; ADMIN_LOG_MESSAGE: &str = b"aite"; + ADMIN_USER_LIST_ITEM: Object = b"item"; + NKU_NODE: Object = b"node"; NKU_UDATA: Object = b"udat"; NKU_ROLE: &str = b"role"; diff --git a/common/src/routes.rs b/common/src/routes.rs index 3ee1e4c..3266745 100644 --- a/common/src/routes.rs +++ b/common/src/routes.rs @@ -24,6 +24,9 @@ pub fn u_image(path: &str, size: usize) -> String { pub fn u_image_fallback_person(name: &str, size: usize) -> String { format!("/image_fallback/person/{name}?size={size}") } +pub fn u_user(login: &str) -> String { + format!("/u/{login}") +} pub fn u_node_slug_watched(node: &str, state: &str) -> String { format!("/n/{node}/watched?state={state}") } diff --git a/database/src/query_syntax.rs b/database/src/query_syntax.rs index b192361..f3d1166 100644 --- a/database/src/query_syntax.rs +++ b/database/src/query_syntax.rs @@ -131,7 +131,7 @@ impl FromStr for Filter<'static> { Path::from_str(l.trim()).map_err(|e| anyhow!("{e}"))?, Value::from_str(r.trim()).map_err(|e| anyhow!("{e}"))?, ), - _ => bail!("invalid filter"), + x => Self::Has(Path::from_str(x.trim()).map_err(|e| anyhow!("{e}"))?), }) } } diff --git a/locale/en.ini b/locale/en.ini index 8ec8c32..db7b077 100644 --- a/locale/en.ini +++ b/locale/en.ini @@ -197,7 +197,7 @@ admin.dashboard.invites=Invitations admin.dashboard.users=Users admin.dashboard.library=Library admin.import.running=Import Running... -admin.import.title=Import +admin.import=Import admin.invite_create_success=Invite created: {invite} admin.invite_delete_success=Invite deleted admin.import_success=Import finished @@ -205,9 +205,7 @@ admin.import_errors=The last import resulted in {n} errors: admin.update_search_success=Search index updated admin.users.remove_success=User removed admin.users.permission_update_success=Permissions updated -admin.users.title=User Management -admin.users.user_list=All Users -admin.users.return_to_list=Back to user list +admin.users=Users admin.log.warnonly=Server Log (Warnings only) admin.log.full=Server Log (Full) diff --git a/server/src/routes.rs b/server/src/routes.rs index 2b7fed9..7068fe0 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_dashboard, r_admin_users, }, assets::{r_image, r_image_fallback_person}, error::{r_api_catch, r_catch}, @@ -28,6 +28,7 @@ use crate::{ player::r_player, r_favicon, r_index, style::{r_assets_css, r_assets_font, r_assets_js, r_assets_js_map}, + user::{r_user, r_user_remove}, }, }; use rocket::{ @@ -74,29 +75,19 @@ pub(super) fn build_rocket(state: Arc<State>) -> Rocket<Build> { .mount( "/", routes![ - // Frontend r_account_login_post, r_account_login, r_account_logout_post, r_account_logout, - // r_account_register_post, - // r_account_register, r_account_settings_post, r_account_settings, r_admin_dashboard, r_admin_import, r_admin_import_post, r_admin_import_stream, - // r_admin_invite, r_admin_log_stream, r_admin_log, - // r_admin_remove_invite, - // r_admin_remove_user, - // r_admin_update_search, - // r_admin_user_permission, - // r_admin_user, - // r_admin_users, - // r_items, + r_admin_users, r_image, r_image_fallback_person, r_assets_font, @@ -106,19 +97,12 @@ pub(super) fn build_rocket(state: Arc<State>) -> Rocket<Build> { r_favicon, r_home, r_index, - // r_item_poster, + r_user, + r_user_remove, r_node, - // r_node_thumbnail, - // r_node_userdata_progress, - // r_node_userdata_rating, - // r_node_userdata_watched, - // r_node_userdata, r_player, r_playersync, - // r_search, - // r_stats, r_stream, - // API r_api_root, r_version, // Compat diff --git a/server/src/ui/admin/mod.rs b/server/src/ui/admin/mod.rs index 8413ead..3fa0591 100644 --- a/server/src/ui/admin/mod.rs +++ b/server/src/ui/admin/mod.rs @@ -9,9 +9,14 @@ pub mod log; use super::error::MyResult; use crate::{request_info::RequestInfo, ui_responder::UiResponse}; -use jellycommon::{jellyobject::ObjectBuffer, *}; +use jellycommon::{ + jellyobject::{ObjectBuffer, ObjectBufferBuilder}, + *, +}; +use jellydb::Query; use jellyui::tr; use rocket::get; +use std::str::FromStr; #[get("/admin/dashboard")] pub async fn r_admin_dashboard(ri: RequestInfo<'_>) -> MyResult<UiResponse> { @@ -35,3 +40,30 @@ pub async fn r_admin_dashboard(ri: RequestInfo<'_>) -> MyResult<UiResponse> { ), ]))) } + +#[get("/admin/users")] +pub fn r_admin_users(ri: RequestInfo) -> MyResult<UiResponse> { + ri.require_admin()?; + + let mut users = Vec::new(); + ri.state.database.transaction(&mut |txn| { + users.clear(); + let rows = txn + .query(Query::from_str("FILTER Ulgn")?)? + .collect::<Vec<_>>(); + for row in rows { + let (row, _) = row?; + users.push(txn.get(row)?.unwrap()); + } + Ok(()) + })?; + + let mut list = ObjectBufferBuilder::default(); + 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(), + )]))) +} diff --git a/server/src/ui/admin/user.rs b/server/src/ui/admin/user.rs deleted file mode 100644 index b4770c8..0000000 --- a/server/src/ui/admin/user.rs +++ /dev/null @@ -1,93 +0,0 @@ -/* - 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) 2026 metamuffin <metamuffin.org> -*/ -use crate::{request_info::RequestInfo, ui::error::MyResult}; -use anyhow::Context; -use jellycommon::routes::{u_admin_user, u_admin_users}; -use rocket::{ - FromForm, FromFormField, - form::Form, - get, post, - response::{Flash, Redirect, content::RawHtml}, -}; - -#[get("/admin/users")] -pub fn r_admin_users(ri: RequestInfo) -> MyResult<RawHtml<String>> { - ri.session.assert_admin()?; - let r = admin_users(&ri.session)?; - Ok(RawHtml(render_page( - &AdminUsersPage { - flash: &None, - lang: &ri.lang, - users: &r.users, - }, - ri.render_info(), - ))) -} - -#[get("/admin/user/<name>")] -pub fn r_admin_user(ri: RequestInfo, name: &str) -> MyResult<RawHtml<String>> { - ri.session.assert_admin()?; - let user = get_user(&ri.session, name)?; - - Ok(RawHtml(render_page( - &AdminUserPage { - flash: &None, - lang: &ri.lang, - user: &user, - }, - ri.render_info(), - ))) -} - -#[derive(FromForm)] -pub struct UserPermissionForm { - permission: String, - action: UrlGrantState, -} - -#[derive(FromFormField)] -pub enum UrlGrantState { - Grant, - Revoke, - Unset, -} - -#[post("/admin/user/<name>/update_permission", data = "<form>")] -pub fn r_admin_user_permission( - ri: RequestInfo, - form: Form<UserPermissionForm>, - name: &str, -) -> MyResult<Flash<Redirect>> { - ri.session.assert_admin()?; - let perm = serde_json::from_str::<UserPermission>(&form.permission) - .context("parsing provided permission")?; - - update_user_perms( - &ri.session, - name, - perm, - match form.action { - UrlGrantState::Grant => GrantState::Grant, - UrlGrantState::Revoke => GrantState::Revoke, - UrlGrantState::Unset => GrantState::Unset, - }, - )?; - - Ok(Flash::success( - Redirect::to(u_admin_user(name)), - tr(ri.lang, "admin.users.permission_update_success"), - )) -} - -#[post("/admin/<name>/remove")] -pub fn r_admin_remove_user(ri: RequestInfo, name: &str) -> MyResult<Flash<Redirect>> { - ri.session.assert_admin()?; - delete_user(&ri.session, name)?; - Ok(Flash::success( - Redirect::to(u_admin_users()), - tr(ri.lang, "admin.users.remove_success"), - )) -} diff --git a/server/src/ui/mod.rs b/server/src/ui/mod.rs index 116ed3c..28762f1 100644 --- a/server/src/ui/mod.rs +++ b/server/src/ui/mod.rs @@ -18,6 +18,7 @@ pub mod home; pub mod node; pub mod style; pub mod player; +pub mod user; #[get("/")] pub async fn r_index(ri: RequestInfo<'_>) -> MyResult<Redirect> { diff --git a/server/src/ui/user.rs b/server/src/ui/user.rs new file mode 100644 index 0000000..31e521e --- /dev/null +++ b/server/src/ui/user.rs @@ -0,0 +1,28 @@ +/* + 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) 2026 metamuffin <metamuffin.org> +*/ +use crate::{request_info::RequestInfo, ui::error::MyResult}; +use jellycommon::routes::u_admin_users; +use jellyui::tr; +use rocket::{ + get, post, + response::{Flash, Redirect, content::RawHtml}, +}; + +#[get("/u/<name>")] +pub fn r_user(ri: RequestInfo<'_>, name: &str) -> MyResult<RawHtml<String>> { + ri.require_user()?; + todo!() +} + +#[post("/u/<name>/remove")] +pub fn r_user_remove(ri: RequestInfo<'_>, name: &str) -> MyResult<Flash<Redirect>> { + ri.require_admin()?; + todo!(); + Ok(Flash::success( + Redirect::to(u_admin_users()), + tr(ri.lang, "admin.users.remove_success"), + )) +} diff --git a/ui/src/components/admin.rs b/ui/src/components/admin.rs index cddb0e2..cd691a2 100644 --- a/ui/src/components/admin.rs +++ b/ui/src/components/admin.rs @@ -7,7 +7,7 @@ use crate::RenderInfo; use jellycommon::{ jellyobject::Object, - routes::{u_admin_import, u_admin_import_post, u_admin_log}, + routes::{u_admin_import, u_admin_import_post, u_admin_log, u_admin_users, u_user}, *, }; use jellyui_locale::tr; @@ -20,7 +20,8 @@ markup::define!( li{a[href=u_admin_log(false)] { @tr(ri.lang, "admin.log.full") }} } - a[href=u_admin_import()] { h2 { @tr(ri.lang, "admin.import.title") }} + a[href=u_admin_import()] { h2 { @tr(ri.lang, "admin.import") }} + a[href=u_admin_users()] { h2 { @tr(ri.lang, "admin.users") }} } AdminImport<'a>(ri: &'a RenderInfo<'a>, data: Object<'a>) { @@ -55,4 +56,11 @@ markup::define!( pre { @data.get(ADMIN_INFO_TEXT) } } + AdminUserList<'a>(ri: &'a RenderInfo<'a>, data: Object<'a>) { + h1 { @tr(ri.lang, "admin.users") } + 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) } } + }} + } + ); diff --git a/ui/src/components/mod.rs b/ui/src/components/mod.rs index dde77b0..e7e5f9c 100644 --- a/ui/src/components/mod.rs +++ b/ui/src/components/mod.rs @@ -18,7 +18,7 @@ pub mod user; use crate::{ RenderInfo, components::{ - admin::{AdminDashboard, AdminImport, AdminInfo}, + admin::{AdminDashboard, AdminImport, AdminInfo, AdminUserList}, login::{AccountLogin, AccountLogout, AccountSetPassword}, message::Message, node_list::NodeList, @@ -64,5 +64,8 @@ define! { @if let Some(user) = view.get(VIEW_USER_SETTINGS) { @UserSettings { ri, user } } + @if let Some(data) = view.get(VIEW_ADMIN_USER_LIST) { + @AdminUserList { ri, data } + } } } diff --git a/ui/src/components/user.rs b/ui/src/components/user.rs index cf1c123..22b296e 100644 --- a/ui/src/components/user.rs +++ b/ui/src/components/user.rs @@ -17,8 +17,8 @@ markup::define! { h1 { @tr(ri.lang, "settings") } h2 { @tr(ri.lang, "settings.account") } - a.switch_account[href=u_account_login()] { button { "Switch Account" } } - a.switch_account[href=u_account_logout()] { button { "Log out" } } + a[href=u_account_login()] { button { "Switch Account" } } + a[href=u_account_logout()] { button { "Log out" } } p { @tr(ri.lang, "tag.Ulgn") ": " @user.get(USER_LOGIN) } form[method="POST", action=u_account_settings()] { label[for="name"] { @tr(ri.lang, "tag.Unam") } @@ -41,7 +41,6 @@ markup::define! { input[type="range", id="accent", name="theme_accent", min=0, max=360, step=1, value=user.get(USER_THEME_ACCENT)]; input[type="submit", value=tr(ri.lang, "settings.update")]; } - // h2 { @tr(ri.lang, "settings.appearance") } // form[method="POST", action=u_account_settings()] { // fieldset { diff --git a/ui/src/old/admin/user.rs b/ui/src/old/admin/user.rs deleted file mode 100644 index e4a8975..0000000 --- a/ui/src/old/admin/user.rs +++ /dev/null @@ -1,81 +0,0 @@ -/* - 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) 2026 metamuffin <metamuffin.org> -*/ - -use crate::{FlashM, Page, scaffold::FlashDisplay}; -use jellycommon::routes::{ - u_admin_user, u_admin_user_permission, u_admin_user_remove, u_admin_users, -}; - -impl Page for AdminUserPage<'_> { - fn title(&self) -> String { - "User Management".to_string() - } - fn to_render(&self) -> markup::DynRender<'_> { - markup::new!(@self) - } -} -impl Page for AdminUsersPage<'_> { - fn title(&self) -> String { - "User Management".to_string() - } - fn to_render(&self) -> markup::DynRender<'_> { - markup::new!(@self) - } -} - -markup::define! { - AdminUsersPage<'a>(lang: &'a Language, users: &'a [User], flash: &'a FlashM) { - h1 { @trs(lang, "admin.users.title") } - @FlashDisplay { flash } - h2 { @trs(lang, "admin.users.user_list") } - ul { @for u in *users { - li { - a[href=u_admin_user(&u.name)] { @format!("{:?}", u.display_name) " (" @u.name ")" } - } - }} - } - AdminUserPage<'a>(lang: &'a Language, user: &'a User, flash: &'a FlashM) { - h1 { @format!("{:?}", user.display_name) " (" @user.name ")" } - a[href=u_admin_users()] { @trs(lang, "admin.users.return_to_list") } - @FlashDisplay { flash } - form[method="POST", action=u_admin_user_remove(&user.name)] { - // input[type="text", name="name", value=&user.name, hidden]; - input.danger[type="submit", value="Remove user(!)"]; - } - - h2 { "Permissions" } - @PermissionDisplay { perms: &user.permissions } - - form[method="POST", action=u_admin_user_permission(&user.name)] { - // input[type="text", name="name", value=&user.name, hidden]; - fieldset.perms { - legend { "Permission" } - @for p in UserPermission::ALL_ENUMERABLE { - label { - input[type="radio", name="permission", value=serde_json::to_string(p).unwrap()]; - @format!("{p}") - } br; - } - } - fieldset.perms { - legend { "State" } - label { input[type="radio", name="action", value="unset"]; "Unset" } br; - label { input[type="radio", name="action", value="grant"]; "Grant" } br; - label { input[type="radio", name="action", value="revoke"]; "Revoke" } br; - } - input[type="submit", value="Update"]; - } - } - PermissionDisplay<'a>(perms: &'a PermissionSet) { - ul { @for (perm,grant) in &perms.0 { - @if *grant { - li[class="perm-grant"] { @format!("Allow {}", perm) } - } else { - li[class="perm-revoke"] { @format!("Deny {}", perm) } - } - }} - } -} |