aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-02-25 17:20:58 +0100
committermetamuffin <metamuffin@disroot.org>2026-02-25 17:20:58 +0100
commit7f7deec27e69ed110c52caddaa3a0c04430e71d9 (patch)
treeb0880d0407c09bc7fb0e83734c4d4683316fb429
parent3b9ba348c8e77a47786bfcc1af6756e8d3bc7499 (diff)
downloadjellything-7f7deec27e69ed110c52caddaa3a0c04430e71d9.tar
jellything-7f7deec27e69ed110c52caddaa3a0c04430e71d9.tar.bz2
jellything-7f7deec27e69ed110c52caddaa3a0c04430e71d9.tar.zst
initial account setup
-rw-r--r--common/object/src/lib.rs5
-rw-r--r--common/src/api.rs5
-rw-r--r--locale/en.ini8
-rw-r--r--server/src/auth.rs20
-rw-r--r--server/src/ui/account/mod.rs62
-rw-r--r--server/src/ui/admin/import.rs2
-rw-r--r--ui/src/components/login.rs25
-rw-r--r--ui/src/components/mod.rs4
-rw-r--r--ui/src/components/node_page.rs6
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 }
}}