From dd27ed7c4aa9ea226d1fcb9eb817e29d7e74e41b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Wed, 4 Nov 2026 21:34:15 +0800 Subject: [PATCH] Signing up to an account that exists signs in; reset a password on the server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ticking 建立新帳號 for an account that already exists signs in with it; when that fails too, the message says the server has an account and how to reset a forgotten password. - anybot-relay --data DIR --reset-password NAME (only while the server is stopped; asks twice, not shown; signs out that account's phones). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa --- relay/src/main.rs | 50 +++++++++++++++++++++++++++++++++++++++++++++++ src/remote.rs | 11 ++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/relay/src/main.rs b/relay/src/main.rs index 0de710f..d2191b9 100644 --- a/relay/src/main.rs +++ b/relay/src/main.rs @@ -866,6 +866,10 @@ fn main() { let port: u16 = arg("--port").and_then(|p| p.parse().ok()).or_else(|| std::env::var("PORT").ok().and_then(|p| p.parse().ok())).unwrap_or(8080); let bind = arg("--bind").unwrap_or_else(|| "0.0.0.0".into()); let dir = PathBuf::from(arg("--data").unwrap_or_else(|| "relay-data".into())); + // Forgot the password: only someone on this computer can set a new one. + if let Some(user) = arg("--reset-password") { + std::process::exit(reset_password(&dir, &user.trim().to_lowercase(), port)); + } let relay = Arc::new(Relay::load(dir.clone())); let listener = std::net::TcpListener::bind((bind.as_str(), port)).unwrap_or_else(|e| { eprintln!("無法使用連接埠 {port}:{e}"); @@ -884,6 +888,52 @@ fn main() { httpd::serve_upgrade(listener, Arc::new(move |req| r.handle(req)), Some(Arc::new(move |req, c| w.ws(req, c))), Arc::new(|| true)); } +/// `anybot-relay --data DIR --reset-password NAME`: a new password for an account (asked twice, +/// not shown). The server must be stopped first, or it would write its own copy back. +fn reset_password(dir: &std::path::Path, user: &str, port: u16) -> i32 { + if std::net::TcpStream::connect_timeout(&std::net::SocketAddr::from(([127, 0, 0, 1], port)), Duration::from_millis(500)).is_ok() { + eprintln!("帳號伺服器還在執行(埠 {port})。先停掉它再重設密碼,之後再啟動。"); + return 1; + } + let relay = Relay::load(dir.to_path_buf()); + if !relay.data.lock().unwrap().users.iter().any(|u| u.name == user) { + eprintln!("{} 裡沒有帳號「{user}」。", dir.display()); + return 1; + } + let ask = |prompt: &str| -> String { + use std::io::Write; + eprint!("{prompt}"); + let _ = std::io::stderr().flush(); + let hide = std::process::Command::new("stty").arg("-echo").stdin(std::process::Stdio::inherit()).stderr(std::process::Stdio::null()).status().map(|s| s.success()).unwrap_or(false); + let mut s = String::new(); + let _ = std::io::stdin().read_line(&mut s); + if hide { + let _ = std::process::Command::new("stty").arg("echo").stdin(std::process::Stdio::inherit()).stderr(std::process::Stdio::null()).status(); + } + eprintln!(); + s.trim_end_matches(['\n', '\r']).to_string() + }; + let pw = ask(&format!("「{user}」的新密碼(至少 8 個字):")); + if pw.chars().count() < 8 { + eprintln!("密碼至少 8 個字,沒有改。"); + return 1; + } + if ask("再輸入一次:") != pw { + eprintln!("兩次不一樣,沒有改。"); + return 1; + } + let mut d = relay.data.lock().unwrap(); + if let Some(u) = d.users.iter_mut().find(|u| u.name == user) { + u.pass = httpd::hash_password(&pw); + } + // Phones signed in with the old password sign in again. + d.sessions.retain(|s| s.user != user); + relay.save(&d); + eprintln!("已改好「{user}」的密碼。啟動帳號伺服器後,用新密碼登入。"); + eprintln!("(帳號備份是用舊密碼加密的,之後要先在電腦上按一次「立刻備份」。)"); + 0 +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/remote.rs b/src/remote.rs index 1a4ac4a..15fc0a4 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -756,7 +756,16 @@ pub fn relay_join(base_settings: &Settings, url: &str, user: &str, password: &st return Err("帳號伺服器要用 https:// 開頭的網址(這台電腦或 Tailscale 位址可以用 http://)".into()); } if create { - relay_post(&format!("{url}/relay/signup"), "", &json!({"user": user, "password": password, "invite": invite}))?; + if let Err(e) = relay_post(&format!("{url}/relay/signup"), "", &json!({"user": user, "password": password, "invite": invite})) { + // The account may be there already (made earlier, or on the phone): just sign in with it. + if relay_post(&format!("{url}/relay/login"), "", &json!({"user": user, "password": password, "device": host_name})).is_err() { + return Err(if e.contains("不開放註冊") || e.contains("邀請碼") { + "這台帳號伺服器已經有帳號了,不能再註冊新的。請取消勾選「建立新帳號」,用已經有的帳號登入。忘記密碼的話,在帳號伺服器那台電腦執行:anybot-relay --data 資料夾 --reset-password 帳號".into() + } else { + e + }); + } + } } let v = relay_post(&format!("{url}/relay/login"), "", &json!({"user": user, "password": password, "device": host_name}))?; let session = v["token"].as_str().ok_or("帳號伺服器沒有給登入憑證")?.to_string();