A new password signs the account out everywhere (#334)

ipx user passwd, the one way to change a password and the step taken
when one may be known to someone else, only replaced the hash. Every
session stayed, and session_user moves `seen` on each use, so a
session someone kept using never reached the idle cutoff.

set_password now deletes the account's sessions too. API tokens stay:
a reset that broke every script would be a reason not to reset, and
the owner lists and removes tokens in Settings. The command says how
many still work.

Also corrects the first-start log line from #321: there is no way to
change a password in the page, so it names the command alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 14:45:41 +00:00
parent d1149e1236
commit 37c08195f1
4 changed files with 33 additions and 5 deletions

View File

@@ -2059,8 +2059,14 @@ impl Db {
Ok(())
}
/// Signs the account out everywhere, too: a password is changed because someone else may
/// know it, and a session they signed in with stayed good, every use putting off its expiry
/// (#334). API tokens stay; they are listed and removed in Settings.
pub async fn set_password(&self, id: i64, hash: &str) -> Result<()> {
self.update_user(id, users::ActiveModel { pass_hash: Set(Some(hash.to_owned())), ..Default::default() }).await
self.update_user(id, users::ActiveModel { pass_hash: Set(Some(hash.to_owned())), ..Default::default() })
.await?;
sessions::Entity::delete_many().filter(sessions::Column::UserId.eq(id)).exec(&self.orm).await?;
Ok(())
}
pub async fn set_admin(&self, id: i64, admin: bool) -> Result<()> {
@@ -3003,6 +3009,22 @@ pub fn now() -> i64 {
mod tests {
use super::*;
#[tokio::test]
async fn a_new_password_signs_the_account_out_everywhere_but_keeps_its_tokens() {
let db = Db::memory().await.unwrap();
let (me, you) =
(db.create_user("me", None, false).await.unwrap(), db.create_user("you", None, false).await.unwrap());
db.create_session(me, "mine").await.unwrap();
db.create_session(you, "yours").await.unwrap();
db.create_api_token(me, "script", "hash").await.unwrap();
db.set_password(me, "new-hash").await.unwrap();
assert!(db.session_user("mine", 3600).await.unwrap().is_none(), "a stolen cookie stops working (#334)");
assert!(db.session_user("yours", 3600).await.unwrap().is_some(), "nobody else is signed out");
assert!(db.api_token_user("hash").await.unwrap().is_some());
}
#[tokio::test]
async fn deadair_looks_at_a_file_once_per_download_and_counts_an_episode_once() {
let db = Db::memory().await.unwrap();