User avatar (Google picture), avatar in mobile You tab, /account page

- Capture the Google profile picture (picture claim) into users.avatar_url; an
  Avatar component shows it, falling back to the initial. Used in the desktop
  header and the mobile "You" tab (which now shows the user when signed in).
- Move account/settings to its own route /account (robust + scrolls to top),
  reached by the desktop avatar and the mobile You tab; drop the inline "You"
  sheet. AccountPanel gains a Sign out action; the page links to Saved/History/
  Boundaries via home intent params (?view= / ?open=).
- db: users.avatar_url (schema + idempotent migration). 118 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-03 14:41:43 +00:00
parent bb008cfaa5
commit 15728c3bcb
11 changed files with 168 additions and 87 deletions
+14 -8
View File
@@ -104,6 +104,15 @@ def _require_user(conn: sqlite3.Connection, request: Request) -> sqlite3.Row:
return user
def _user_out(user: sqlite3.Row) -> dict:
return {
"id": user["id"],
"email": user["email"],
"display_name": user["display_name"],
"avatar_url": user["avatar_url"],
}
def _send_link_safe(email: str, link: str) -> None:
"""Send the magic link, swallowing failures (runs off the request path)."""
try:
@@ -284,6 +293,7 @@ class UserOut(BaseModel):
id: int
email: str
display_name: str | None = None
avatar_url: str | None = None
class SessionOut(BaseModel):
@@ -370,18 +380,13 @@ def create_app() -> FastAPI:
conn.commit()
user = auth.get_user(conn, user_id)
_set_session_cookie(response, token)
return SessionOut(
user=UserOut(id=user["id"], email=user["email"], display_name=user["display_name"]),
token=token,
)
return SessionOut(user=UserOut(**_user_out(user)), token=token)
@app.get("/api/auth/me", response_model=UserOut | None)
def auth_me(request: Request) -> UserOut | None:
with get_conn() as conn:
user = _current_user(conn, request)
if not user:
return None
return UserOut(id=user["id"], email=user["email"], display_name=user["display_name"])
return UserOut(**_user_out(user)) if user else None
@app.post("/api/auth/logout")
def auth_logout(request: Request, response: Response) -> dict:
@@ -431,7 +436,8 @@ def create_app() -> FastAPI:
return fail
with get_conn() as conn:
user_id = auth.find_or_create_user(
conn, info["email"], "google", info["sub"], display_name=info.get("name")
conn, info["email"], "google", info["sub"],
display_name=info.get("name"), avatar_url=info.get("picture"),
)
token = auth.create_session(conn, user_id, user_agent=request.headers.get("User-Agent"))
conn.commit()