feat(auth): show the Google address beside the one the account registered with (E9.5 S3)

An account can now hold two true addresses: the one its owner typed, which a Google
sign-in no longer overwrites, and the one Google answers with. Only the first was
stored, so Settings could say 'Connected' without saying connected AS WHOM -- exactly
what someone who registered with a +tag needs to know.

users.google_email (migration 0080) records Google's answer at sign-in and at an
in-app link. /api/me exposes it ONLY when it differs from the account address, so the
comparison lives in one place and the majority, whose two addresses are the same
string, see no change at all. Rendered in the account menu as a muted third line under
the identity, and in the Settings sign-in row under the existing hint.

A short 'Google' label rather than a brand glyph: the app draws one icon set (lucide),
and a generic mail icon next to an email address says nothing. It also reads to a
screen reader, which an unlabelled logo would not.

No backfill is possible -- Google's answer is only known at sign-in -- so accounts
linked earlier stay NULL until their owner next signs in with Google.
This commit is contained in:
2026-09-04 01:23:11 +02:00
parent c74487fbb3
commit a614ee2435
12 changed files with 136 additions and 4 deletions
+49
View File
@@ -13,6 +13,8 @@ hold for Google Workspace custom domains unless their admin enabled it.
import pytest
from sqlalchemy import select, text
_UAT_PW = "correct horse battery"
from app.auth import is_allowed
from app.models import Invite, User
from app.utils import canonical_email
@@ -293,3 +295,50 @@ def test_a_mailbox_change_is_refused_when_another_account_owns_it(db):
_sync_google_email(db, user, "taken@example.com")
assert user.email == "mover@gmail.com" # unique constraint would have 500'd the sign-in
# --- showing BOTH addresses, once there are two -------------------------------------------------
def test_the_google_address_is_recorded_beside_the_registered_one(db):
"""Since the sign-in no longer overwrites the registered address, the account has two true
addresses and only one was stored. This is the other half, for the UI to name."""
from app.routes.auth import _sync_google_email
user = User(email="me+siftlode@gmail.com", email_verified=True, is_active=True)
db.add(user)
db.commit()
user.google_email = "me@gmail.com"
_sync_google_email(db, user, "me@gmail.com")
db.commit()
assert user.email == "me+siftlode@gmail.com" # still theirs
assert user.google_email == "me@gmail.com" # …and now we know the other one
def test_me_reports_the_google_address_only_when_it_differs(client, db, make_user):
"""The comparison lives in one place so no surface has to repeat it — and so the account menu
stays exactly as it is for the majority, whose two addresses are the same string."""
same = make_user(email="plain@gmail.com", password=_UAT_PW)
same.google_email = "plain@gmail.com"
db.commit()
assert (
client.post(
"/auth/password-login", json={"email": "plain@gmail.com", "password": _UAT_PW}
).status_code
== 200
)
assert client.get("/api/me").json()["google_email"] is None
client.post("/auth/logout")
tagged = make_user(email="me+siftlode@gmail.com", password=_UAT_PW)
tagged.google_email = "me@gmail.com"
db.commit()
assert (
client.post(
"/auth/password-login", json={"email": "me+siftlode@gmail.com", "password": _UAT_PW}
).status_code
== 200
)
assert client.get("/api/me").json()["google_email"] == "me@gmail.com"