diff --git a/CHANGELOG.md b/CHANGELOG.md index a090fd3..1bea689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ The long form, with what was wrong before and how it was found, is in - Settings → Users and `ipx user list` show when each account was added and when it last signed in, to the hour. +- `ipx user rename ` renames an account and keeps its feeds, read state and admin + rights. An account made before the proxy was set up can take the name the proxy signs it in as. ### Changed diff --git a/src/db.rs b/src/db.rs index 06d33fe..5ff16c5 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1069,6 +1069,15 @@ impl Db { Ok(()) } + /// The proxy signs people in by the name it vouches for, so an account made before the proxy + /// was set up has to take that name to be found by it. The name is UNIQUE, so a taken one is + /// refused here as well as by the caller. + pub fn rename_user(&self, id: i64, name: &str) -> Result<()> { + let conn = self.conn.lock().unwrap(); + conn.execute("UPDATE users SET name = ?2 WHERE id = ?1", params![id, name])?; + Ok(()) + } + /// Records a sign-in, to the hour: the proxy vouches for every request, and writing each one /// would buy nothing. pub fn signed_in(&self, id: i64) -> Result<()> { @@ -1571,6 +1580,20 @@ mod tests { assert_eq!(kept, 1); } + #[test] + fn a_renamed_account_keeps_everything_but_its_name() { + let db = Db::memory().unwrap(); + let ray = db.create_user("rays", None, true).unwrap(); + db.create_user("sam", None, false).unwrap(); + db.subscribe(ray, "f").unwrap(); + db.rename_user(ray, "rays@sdf1.net").unwrap(); + assert!(db.user_by_name("rays").unwrap().is_none()); + let renamed = db.user_by_name("RAYS@sdf1.net").unwrap().unwrap(); + assert_eq!((renamed.id, renamed.is_admin), (ray, true), "same account, still the admin"); + assert_eq!(db.subscriptions_for(ray).unwrap().len(), 1, "and still subscribed"); + assert!(db.rename_user(ray, "sam").is_err(), "a taken name is refused"); + } + #[test] fn an_account_knows_when_it_was_made_and_last_signed_in() { let db = Db::memory().unwrap(); diff --git a/src/main.rs b/src/main.rs index 5713b95..5608f1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,6 +99,9 @@ enum UserCmd { Passwd { name: String }, /// Delete an account and everything it knows: its subscriptions and read state Rm { name: String }, + /// Rename an account, keeping its feeds, read state and admin rights. This is how an + /// account made before the proxy takes the name the proxy signs it in as + Rename { name: String, new_name: String }, } /// What a brand new database starts with, so there is always a way in. Announced loudly @@ -297,6 +300,22 @@ fn user_cmd(ctx: &Arc, cmd: UserCmd) -> Result<()> { println!("password changed for {name}"); Ok(()) } + UserCmd::Rename { name, new_name } => { + let name = name.trim().to_ascii_lowercase(); + // The same rules as a name the proxy vouches for, or the proxy would never find it. + let new_name = crate::auth::name_from_header(&new_name) + .ok_or_else(|| anyhow::anyhow!("not a usable name: no commas, semicolons or line breaks"))?; + let user = ctx + .db + .user_by_name(&name)? + .ok_or_else(|| anyhow::anyhow!("no such account: {name}"))?; + if ctx.db.user_by_name(&new_name)?.is_some() { + anyhow::bail!("{new_name} already exists"); + } + ctx.db.rename_user(user.id, &new_name)?; + println!("renamed {name} to {new_name}"); + Ok(()) + } UserCmd::Rm { name } => { let name = name.trim().to_ascii_lowercase(); let user = ctx