fix(auth,webmail): list a pre-accounts session in the switcher; preview as plain text

A browser signed in before account cookies existed has only lm_user_session,
so the switcher showed it as loading forever. The active session now always
counts and is filed on first listing. Message previews drop the HTML and
entities the server stored.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-22 15:23:40 -03:00
1 parent 026f24a64a
commit cc0adcd94d
5 files changed
+84 -2

No files matched your search

+7
View File
@@ -1260,6 +1260,13 @@ describeDb("moving between the surfaces", () => {
expect(unknown.status).toBe(404);
}, 60000);
it("lists a session that predates account cookies and files it", async () => {
const a = await signIn("multi1");
const listed = await call("/api/v1/auth/accounts", `lm_user_session=${a.token}`);
expect(listed.body.accounts).toEqual([{ id: a.id, email: emailOf("multi1"), active: true }]);
expect(valueOf(listed.cookies, `lm_acct_${a.id}`)).toBe(a.token);
}, 60000);
it("will not switch to a token that belongs to another account's cookie", async () => {
const a = await signIn("multi1");
const b = await signIn("multi2");
+16 -1
View File
@@ -642,7 +642,15 @@ async function mfaVerify(req: IncomingMessage, res: ServerResponse): Promise<voi
*/
async function liveAccounts(req: IncomingMessage): Promise<{ id: string; email: string; token: string }[]> {
const live: { id: string; email: string; token: string }[] = [];
for (const { id, token } of accountTokens(req.headers.cookie)) {
// The active session counts even without an account cookie of its own: a
// browser signed in before accounts existed has only lm_user_session.
const filed = accountTokens(req.headers.cookie);
const activeToken = activeUserToken(req);
const activeClaims = activeToken ? verifyJwt(activeToken) : null;
if (activeClaims && !filed.some((a) => a.id === activeClaims.sub.toLowerCase())) {
filed.push({ id: activeClaims.sub.toLowerCase(), token: activeToken });
}
for (const { id, token } of filed) {
const payload = verifyJwt(token);
if (!payload || !isSurfaceAuthorized(payload, "user") || payload.sub.toLowerCase() !== id) continue;
if (await repo.isSessionRevoked(sha256(token))) continue;
@@ -662,6 +670,13 @@ async function listAccounts(req: IncomingMessage, res: ServerResponse): Promise<
email,
active: active?.sub.toLowerCase() === id,
}));
// Files the active session under its account so a later switch away and back
// finds it, which a session from before accounts existed would not.
if (active && !accountTokens(req.headers.cookie).some((a) => a.id === active.sub.toLowerCase())) {
res.setHeader("Set-Cookie", [
sessionCookie(accountCookieName(active.sub), activeUserToken(req), USER_SESSION_TTL_SECONDS, isSecureRequest()),
]);
}
sendJson(res, 200, { accounts });
}