diff options
| -rw-r--r-- | common/object/src/lib.rs | 5 | ||||
| -rw-r--r-- | common/src/api.rs | 5 | ||||
| -rw-r--r-- | locale/en.ini | 8 | ||||
| -rw-r--r-- | server/src/auth.rs | 20 | ||||
| -rw-r--r-- | server/src/ui/account/mod.rs | 62 | ||||
| -rw-r--r-- | server/src/ui/admin/import.rs | 2 | ||||
| -rw-r--r-- | ui/src/components/login.rs | 25 | ||||
| -rw-r--r-- | ui/src/components/mod.rs | 4 | ||||
| -rw-r--r-- | ui/src/components/node_page.rs | 6 |
9 files changed, 104 insertions, 33 deletions
diff --git a/common/object/src/lib.rs b/common/object/src/lib.rs index 98e4834..ae8d1cd 100644 --- a/common/object/src/lib.rs +++ b/common/object/src/lib.rs @@ -240,6 +240,11 @@ impl<'a> Object<'a> { self.insert_multi(tag, &[value]) } #[must_use] + #[inline] + pub fn remove<T: ValueStore>(&self, tag: TypedTag<T>) -> ObjectBuffer { + self.insert_multi(tag, &[]) + } + #[must_use] pub fn insert_multi<T: ValueStore>(&self, tag: TypedTag<T>, values: &[T]) -> ObjectBuffer { let prefix = self.tags.partition_point(|&x| x < tag.0.0); let suffix = self.tags.partition_point(|&x| x <= tag.0.0); diff --git a/common/src/api.rs b/common/src/api.rs index 5d9c72d..f791cd8 100644 --- a/common/src/api.rs +++ b/common/src/api.rs @@ -22,7 +22,7 @@ fields! { VIEW_STATTEXT: Object = b"stat"; VIEW_ACCOUNT_LOGIN: () = b"acli"; VIEW_ACCOUNT_LOGOUT: () = b"aclo"; - VIEW_ACCOUNT_SET_PASSWORD: &str = b"acsp"; + VIEW_ACCOUNT_SET_PASSWORD: Object = b"acsp"; VIEW_ADMIN_DASHBOARD: () = b"adda"; VIEW_ADMIN_IMPORT: Object = b"adim"; VIEW_ADMIN_INFO: Object = b"adin"; @@ -37,6 +37,9 @@ fields! { ADMIN_INFO_TEXT: &str = b"aite"; ADMIN_LOG_MESSAGE: &str = b"aite"; + SETPW_USERNAME: &str = b"spwu"; + SETPW_PASSWORD: &str = b"spwp"; + ADMIN_USER_LIST_ITEM: Object = b"item"; NKU_NODE: Object = b"node"; diff --git a/locale/en.ini b/locale/en.ini index ebbacc5..048886f 100644 --- a/locale/en.ini +++ b/locale/en.ini @@ -96,7 +96,7 @@ tag.iden.wkdt=Wikidata tag.iden.ytc1=YouTube Channel tag.iden.ytch=YouTube Channel Handle tag.iden.ytvi=YouTube Video -tag.iden=Identifier +tag.iden=Identifiers tag.kind.chnl=Channel tag.kind.coll=Collection tag.kind.epsd=Episode @@ -137,8 +137,10 @@ tag.rtng.ytvi=Youtube Video ID tag.rtng=Rating tag.sbtl=Subtitle tag.stsz=Size on Filesystem +tag.tag1=Tags tag.tgln=Tagline tag.titl=Title +tag.trak=Tracks tag.Uadm=Admin tag.Ulgn=Login tag.Unam=Name @@ -223,8 +225,12 @@ account.login=Log in account.login.switch=Switch Account account.login.submit=Log in account.login.submit.switch=Switch +account.login.set_password=Update Password +account.login.set_password.par=You are logging in for the first time. Please set a new password and display name. account.username=Username account.password=Password +account.display_name=Display Name +account.new_password=New Password account.logout=Log out account.logout.submit=Log out diff --git a/server/src/auth.rs b/server/src/auth.rs index 6463eb1..26da82b 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -8,8 +8,8 @@ use crate::State; use anyhow::{Result, anyhow, bail}; use argon2::{Argon2, PasswordHasher, password_hash::Salt}; use jellycommon::{ - USER_LOGIN, USER_PASSWORD, jellyobject::{ObjectBuffer, Path}, + *, }; use jellydb::{Filter, Query, Sort}; @@ -25,7 +25,12 @@ pub fn token_to_user(state: &State, token: &str) -> Result<ObjectBuffer> { user.ok_or(anyhow!("user was deleted")) } -pub fn login(state: &State, username: &str, password: &str, expire: Option<i64>) -> Result<String> { +pub fn login( + state: &State, + username: &str, + password: &str, + expire: Option<i64>, +) -> Result<(String, bool)> { let password = hash_password(username, password); let mut user_row = None; @@ -51,10 +56,13 @@ pub fn login(state: &State, username: &str, password: &str, expire: Option<i64>) bail!("incorrect password") } - Ok(token::create( - &state.session_key, - user_row, - expire.unwrap_or(60 * 60 * 24 * 30), + Ok(( + token::create( + &state.session_key, + user_row, + expire.unwrap_or(60 * 60 * 24 * 30), + ), + user.as_object().has(USER_PASSWORD_REQUIRE_CHANGE.0), )) } diff --git a/server/src/ui/account/mod.rs b/server/src/ui/account/mod.rs index 1c44914..837d49a 100644 --- a/server/src/ui/account/mod.rs +++ b/server/src/ui/account/mod.rs @@ -9,16 +9,20 @@ pub mod settings; use super::error::MyError; use crate::{ - auth::login, request_info::RequestInfo, ui::error::MyResult, ui_responder::UiResponse, + auth::{hash_password, login}, + request_info::RequestInfo, + ui::error::MyResult, + ui_responder::UiResponse, }; use anyhow::anyhow; use jellycommon::{ - VIEW_ACCOUNT_LOGIN, VIEW_ACCOUNT_LOGOUT, - jellyobject::OBB, + jellyobject::{OBB, Path}, routes::{u_account_login, u_home}, + *, }; +use jellydb::{Filter, Query, Sort}; use rocket::{ - FromForm, + Either, FromForm, form::{Contextual, Form}, get, http::{Cookie, CookieJar}, @@ -39,10 +43,14 @@ pub fn r_account_logout(ri: RequestInfo<'_>) -> UiResponse { #[derive(FromForm, Serialize, Deserialize)] pub struct LoginForm { - #[field(validate = len(4..32))] + #[field(validate = len(..32))] pub username: String, #[field(validate = len(..64))] pub password: String, + #[field(validate = len(..64))] + pub new_password: Option<String>, + #[field(validate = len(..64))] + pub display_name: Option<String>, #[field(default = 604800)] // one week pub expire: u64, } @@ -52,15 +60,51 @@ pub fn r_account_login_post( ri: RequestInfo<'_>, jar: &CookieJar, form: Form<Contextual<LoginForm>>, -) -> MyResult<Redirect> { +) -> MyResult<Either<Redirect, UiResponse>> { let form = match &form.value { Some(v) => v, None => return Err(MyError(anyhow!(format_form_error(form)))), }; - let session = login(&ri.state, &form.username, &form.password, None)?; - jar.add(Cookie::build(("session", session)).permanent().build()); - Ok(Redirect::found(u_home())) + let (session, need_pw_change) = login(&ri.state, &form.username, &form.password, None)?; + + if need_pw_change { + if let Some(new_password) = &form.new_password { + let password_hash = hash_password(&form.username, &new_password); + ri.state.database.transaction(&mut |txn| { + let user_row = txn.query_single(Query { + filter: Filter::Match(Path(vec![USER_LOGIN.0]), form.username.clone().into()), + sort: Sort::None, + })?; + if let Some(ur) = user_row { + let mut user = txn.get(ur)?.unwrap(); + user = user.as_object().remove(USER_PASSWORD_REQUIRE_CHANGE); + user = user.as_object().insert(USER_PASSWORD, &password_hash); + if let Some(name) = &form.display_name { + user = user.as_object().insert(USER_NAME, &name); + } + txn.update(ur, user)?; + } + Ok(()) + })?; + } else { + return Ok(Either::Right( + ri.respond_ui( + OBB::new().with( + VIEW_ACCOUNT_SET_PASSWORD, + OBB::new() + .with(SETPW_USERNAME, &form.username) + .with(SETPW_PASSWORD, &form.password) + .finish() + .as_object(), + ), + ), + )); + } + } + + jar.add(Cookie::build(("session", session)).permanent().build()); + Ok(Either::Left(Redirect::found(u_home()))) } #[post("/account/logout")] diff --git a/server/src/ui/admin/import.rs b/server/src/ui/admin/import.rs index eba2a3b..78db4a4 100644 --- a/server/src/ui/admin/import.rs +++ b/server/src/ui/admin/import.rs @@ -32,8 +32,8 @@ pub async fn r_admin_import(ri: RequestInfo<'_>) -> MyResult<UiResponse> { .iter() .map(|e| e.as_str()) .collect::<Vec<_>>(); - let mut data = ObjectBuffer::empty(); + let mut data = ObjectBuffer::empty(); if is_importing() { data = data.as_object().insert(ADMIN_IMPORT_BUSY, ()); } diff --git a/ui/src/components/login.rs b/ui/src/components/login.rs index c54a541..d291165 100644 --- a/ui/src/components/login.rs +++ b/ui/src/components/login.rs @@ -4,32 +4,37 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ +use jellycommon::{SETPW_PASSWORD, SETPW_USERNAME, jellyobject::Object}; use jellyui_locale::tr; use crate::RenderInfo; markup::define! { - AccountSetPassword<'a>(ri: &'a RenderInfo<'a>, session: &'a str) { + AccountSetPassword<'a>(ri: &'a RenderInfo<'a>, data: Object<'a>) { form.account[method="POST", action=""] { - h1 { @tr(ri.lang, "account.set_password") } - input[type="text", name="session", hidden, value=session]; br; + h1 { @tr(ri.lang, "account.login.set_password") } + p { @tr(ri.lang, "account.login.set_password.par") } - label[for="inp-password"] { @tr(ri.lang, "account.password") } - input[type="password", id="inp-password", name="password"]; br; + input[type="hidden", name="username", value=data.get(SETPW_USERNAME)]; + input[type="hidden", name="password", value=data.get(SETPW_PASSWORD)]; - input[type="submit", value=tr(ri.lang, "account.register.submit")]; + label[for="password"] { @tr(ri.lang, "account.new_password") } + input[type="password", id="password", name="new_password"]; br; + label[for="display_name"] { @tr(ri.lang, "account.display_name") } + input[type="text", id="display_name", name="display_name"]; br; + input[type="submit", value=tr(ri.lang, "account.login.submit")]; } } AccountLogin<'a>(ri: &'a RenderInfo<'a>) { form.account[method="POST", action=""] { h1 { @tr(ri.lang, "account.login") } - label[for="inp-username"] { @tr(ri.lang, "account.username") } - input[type="text", id="inp-username", name="username"]; br; - label[for="inp-password"] { @tr(ri.lang, "account.password") } - input[type="password", id="inp-password", name="password"]; br; + label[for="username"] { @tr(ri.lang, "account.username") } + input[type="text", id="username", name="username"]; br; + label[for="password"] { @tr(ri.lang, "account.password") } + input[type="password", id="password", name="password"]; br; input[type="submit", value=tr(ri.lang, if ri.user.is_some() { "account.login.submit.switch" } else { "account.login.submit" })]; } diff --git a/ui/src/components/mod.rs b/ui/src/components/mod.rs index 5460090..d87efab 100644 --- a/ui/src/components/mod.rs +++ b/ui/src/components/mod.rs @@ -49,8 +49,8 @@ define! { @if let Some(()) = view.get(VIEW_ACCOUNT_LOGOUT) { @AccountLogout{ ri } } - @if let Some(session) = view.get(VIEW_ACCOUNT_SET_PASSWORD) { - @AccountSetPassword { ri, session } + @if let Some(data) = view.get(VIEW_ACCOUNT_SET_PASSWORD) { + @AccountSetPassword { ri, data } } @if let Some(()) = view.get(VIEW_ADMIN_DASHBOARD) { @AdminDashboard { ri } diff --git a/ui/src/components/node_page.rs b/ui/src/components/node_page.rs index 53534cf..f40aa73 100644 --- a/ui/src/components/node_page.rs +++ b/ui/src/components/node_page.rs @@ -81,7 +81,7 @@ markup::define! { // } @if node.has(NO_TRACK.0) { details { - summary { @tr(ri.lang, "media.tracks") } + summary { @tr(ri.lang, "tag.trak") } ol { @for track in node.iter(NO_TRACK) { li { "track" @track.get(TR_NAME) } }} @@ -89,7 +89,7 @@ markup::define! { } @if let Some(idents) = node.get(NO_IDENTIFIERS) { details { - summary { @tr(ri.lang, "node.external_ids") } + summary { @tr(ri.lang, "tag.iden") } table { @for (key, value) in idents.entries::<&str>() { tr { td { @tr(ri.lang, &format!("tag.iden.{key}")) } @@ -104,7 +104,7 @@ markup::define! { } @if node.has(NO_TAG.0) { details { - summary { @tr(ri.lang, "node.tags") } + summary { @tr(ri.lang, "tag.tag1") } ol { @for tag in node.iter(NO_TAG) { li { @tag } }} |