From 586d2c07a163f02a597aa6ccfd396837a22eb309 Mon Sep 17 00:00:00 2001 From: rays Date: Sat, 12 Sep 2026 13:54:29 +0000 Subject: [PATCH] ipx user rename: give an account the name the proxy signs it in as An account made by hand before the proxy was set up is called what it was given ('rays'), while Cloudflare Access vouches for an email address. With auto_create_users on, the first visit through the tunnel would make a second, empty account. Renaming keeps the id, so feeds, read state and admin rights go with it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TAC7sLVqfKmY6rsTLXzNgk --- CHANGELOG.md | 2 ++ src/db.rs | 23 +++++++++++++++++++++++ src/main.rs | 19 +++++++++++++++++++ 3 files changed, 44 insertions(+) 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