From 48948e6d213e0f3c3df5629a1826750abb0c40a9 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 1 Aug 2026 08:39:00 +0000 Subject: [PATCH] fix(join): stop rotating a returning member's webmail password handleJoin called setWebmailPassword unconditionally, so every visit to join@ minted a fresh Roundcube password -- including for members who registered and verified long ago. join@ is the address people remember, so returning members type it out of habit. Doing so silently invalidated their webmail login: the replacement password scrolled past once in the join output, the old one was already dead, and nothing in the session said a credential had changed. The failure surfaces later, as "my password stopped working", with no way to connect it back to having typed join@ -- and it reads like a compromised account, which is an alarming thing to hand someone over a no-op visit. Mint the password only while actually onboarding: a new key registering, or an account that completes email verification in this session. A returning member is told the password is unchanged and pointed at passwd@, which already sets one password across git, mail and chat and is key-gated, so it doubles as the forgot-password path. Also replace the bare webmail URL line shown when no password is minted with the full url/login block, so the address and where to get credentials are always visible, and distinguish "unchanged" (returning) from "not set yet" (onboarding, but Mailu was unreachable). Verified by inspection only: this box has no Go toolchain (repo needs 1.26), so the build is left to CI. Co-Authored-By: Claude Opus 5 (1M context) --- cmd/agentbbs/main.go | 54 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/cmd/agentbbs/main.go b/cmd/agentbbs/main.go index 0befd22..bd4a8e7 100644 --- a/cmd/agentbbs/main.go +++ b/cmd/agentbbs/main.go @@ -753,6 +753,10 @@ func (a *app) handleJoin(s ssh.Session) { wish.Fatalln(s, "registration error: "+err.Error()) return } + // Whether this session actually onboarded anyone: a brand-new key here, or an + // account that finishes email verification below. Everything with a credential + // side effect keys off this rather than off reaching join@ at all. + onboarding := !found if !found { // New key: show the acceptable-use terms and require acceptance before // creating the account, then let the visitor pick their own handle (a @@ -783,6 +787,7 @@ func (a *app) handleJoin(s ssh.Session) { return } a.notifySignup(u) + onboarding = true } // Every verified member gets a homepage at https:///~ and a @@ -791,11 +796,26 @@ func (a *app) handleJoin(s ssh.Session) { _ = a.ensureMailbox(u) // Give them a webmail password so free members can log into webmail. The // in-BBS reader uses the gateway master user and needs no password, but - // Roundcube does. (Re)set on each join@; they can change it in webmail. - webmailPW := a.setWebmailPassword(u) - + // Roundcube does. + // + // Only while ONBOARDING. This used to run on every join@, which quietly broke + // returning members: join@ is the address people remember, so typing it again + // out of habit rotated a live Roundcube password: the replacement scrolled past + // in this output once, the old one was already dead, and nothing said a + // credential had changed. Rotating a working password must never be a side + // effect of saying hello -- passwd@ is the deliberate way to change one, and it + // is already key-gated, so it doubles as the forgot-password path. + var webmailPW string + if onboarding { + webmailPW = a.setWebmailPassword(u) + } + + lead := " You're in. One login gets you everything — no other servers to ssh into:" + if !onboarding { + lead = " You're already a member — nothing changed. One login gets you everything:" + } includes := []string{ - " You're in. One login gets you everything — no other servers to ssh into:", + lead, "", " ssh " + u.Name + "@" + a.host, "", @@ -806,7 +826,8 @@ func (a *app) handleJoin(s ssh.Session) { " • the arcade & games", " • your homepage https://" + a.host + "/~" + u.Name, } - if a.webmailURL != "" && webmailPW != "" { + switch { + case a.webmailURL != "" && webmailPW != "": includes = append(includes, "", " Webmail (read your mail in a browser):", @@ -814,8 +835,27 @@ func (a *app) handleJoin(s ssh.Session) { " • login "+a.mailAddress(u.Name), " • password "+webmailPW+" (change it in webmail Settings)", ) - } else if a.webmailURL != "" { - includes = append(includes, " • webmail "+a.webmailURL) + case a.webmailURL != "" && !onboarding: + // Say the password is untouched. A returning member who came here looking + // for their credentials needs to know this visit did not change them, and + // where to go if they have lost them. + includes = append(includes, + "", + " Webmail (read your mail in a browser):", + " • url "+a.webmailURL, + " • login "+a.mailAddress(u.Name), + " • password unchanged — forgot it? ssh passwd@"+a.host, + ) + case a.webmailURL != "": + // Onboarding, but minting the password failed (Mailu down or unconfigured). + // Don't imply one is waiting for them -- point at the way to set one. + includes = append(includes, + "", + " Webmail (read your mail in a browser):", + " • url "+a.webmailURL, + " • login "+a.mailAddress(u.Name), + " • password not set yet — set one: ssh passwd@"+a.host, + ) } wish.Println(s, "\n"+strings.Join(includes, "\n"))