Signing up to an account that exists signs in; reset a password on the server

- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
This commit is contained in:
王性驊 2026-11-04 21:34:15 +08:00
parent 365dc87e54
commit dd27ed7c4a
2 changed files with 60 additions and 1 deletions

View File

@ -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 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 bind = arg("--bind").unwrap_or_else(|| "0.0.0.0".into());
let dir = PathBuf::from(arg("--data").unwrap_or_else(|| "relay-data".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 relay = Arc::new(Relay::load(dir.clone()));
let listener = std::net::TcpListener::bind((bind.as_str(), port)).unwrap_or_else(|e| { let listener = std::net::TcpListener::bind((bind.as_str(), port)).unwrap_or_else(|e| {
eprintln!("無法使用連接埠 {port}:{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)); 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -756,7 +756,16 @@ pub fn relay_join(base_settings: &Settings, url: &str, user: &str, password: &st
return Err("帳號伺服器要用 https:// 開頭的網址(這台電腦或 Tailscale 位址可以用 http://)".into()); return Err("帳號伺服器要用 https:// 開頭的網址(這台電腦或 Tailscale 位址可以用 http://)".into());
} }
if create { 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 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(); let session = v["token"].as_str().ok_or("帳號伺服器沒有給登入憑證")?.to_string();