Spaces:
Sleeping
Sleeping
Commit ·
5be8e7e
1
Parent(s): 2f83d3e
Fix CachedWidgetWarning: don't cache the CookieManager component
Browse filesst.cache_resource froze the component after its first render, so
cookie reads/writes never reflected later changes. Fixed by
constructing exactly one CookieManager per script run in app.py and
threading it into cookie_token_store's functions as a parameter,
rather than each function lazily constructing (and risking a second,
same-keyed) instance internally -- calling
stx.CookieManager(key=...) twice in one run raises DuplicateWidgetID.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- app.py +12 -9
- src/cookie_token_store.py +18 -11
app.py
CHANGED
|
@@ -41,7 +41,7 @@ def _access_token_valid() -> bool:
|
|
| 41 |
)
|
| 42 |
|
| 43 |
|
| 44 |
-
def _handle_drive_oauth_callback() -> None:
|
| 45 |
"""Completes the one-time "Connect Google Drive" exchange, if we're mid-flow."""
|
| 46 |
if not st.user.is_logged_in:
|
| 47 |
return
|
|
@@ -64,14 +64,14 @@ def _handle_drive_oauth_callback() -> None:
|
|
| 64 |
return
|
| 65 |
|
| 66 |
refresh_token = tokens.get("refresh_token")
|
| 67 |
-
if refresh_token and
|
| 68 |
-
cookie_token_store.save_refresh_token(st.user.email, refresh_token)
|
| 69 |
_store_access_token(tokens)
|
| 70 |
st.query_params.clear()
|
| 71 |
st.rerun()
|
| 72 |
|
| 73 |
|
| 74 |
-
def _ensure_drive_access() -> None:
|
| 75 |
"""Populates session_state's Drive access token, silently refreshing from a
|
| 76 |
stored refresh_token when possible. Shows a one-time "Connect Google Drive"
|
| 77 |
link only when neither a live token nor a stored refresh_token exists.
|
|
@@ -79,8 +79,8 @@ def _ensure_drive_access() -> None:
|
|
| 79 |
if not st.user.is_logged_in or _access_token_valid():
|
| 80 |
return
|
| 81 |
|
| 82 |
-
if
|
| 83 |
-
refresh_token = cookie_token_store.get_refresh_token(st.user.email)
|
| 84 |
if refresh_token:
|
| 85 |
try:
|
| 86 |
tokens = google_oauth.refresh_access_token(
|
|
@@ -91,7 +91,7 @@ def _ensure_drive_access() -> None:
|
|
| 91 |
_store_access_token(tokens)
|
| 92 |
return
|
| 93 |
except Exception:
|
| 94 |
-
cookie_token_store.delete_refresh_token(st.user.email)
|
| 95 |
|
| 96 |
state = uuid.uuid4().hex
|
| 97 |
st.session_state["drive_oauth_state"] = state
|
|
@@ -101,8 +101,11 @@ def _ensure_drive_access() -> None:
|
|
| 101 |
st.link_button("Connect Google Drive", authorize_url)
|
| 102 |
|
| 103 |
|
|
|
|
|
|
|
|
|
|
| 104 |
if "auth" in st.secrets:
|
| 105 |
-
_handle_drive_oauth_callback()
|
| 106 |
|
| 107 |
with st.sidebar:
|
| 108 |
if "auth" not in st.secrets:
|
|
@@ -112,7 +115,7 @@ with st.sidebar:
|
|
| 112 |
else:
|
| 113 |
st.caption(f"Logged in as {st.user.email}")
|
| 114 |
st.button("Log out", on_click=st.logout)
|
| 115 |
-
_ensure_drive_access()
|
| 116 |
|
| 117 |
home_page = st.Page("pages/home.py", title="Home", url_path="home", default=True)
|
| 118 |
skull_king_page = st.Page("pages/skull_king.py", title="Skull King", url_path="skull_king")
|
|
|
|
| 41 |
)
|
| 42 |
|
| 43 |
|
| 44 |
+
def _handle_drive_oauth_callback(cookie_manager) -> None:
|
| 45 |
"""Completes the one-time "Connect Google Drive" exchange, if we're mid-flow."""
|
| 46 |
if not st.user.is_logged_in:
|
| 47 |
return
|
|
|
|
| 64 |
return
|
| 65 |
|
| 66 |
refresh_token = tokens.get("refresh_token")
|
| 67 |
+
if refresh_token and cookie_manager is not None:
|
| 68 |
+
cookie_token_store.save_refresh_token(cookie_manager, st.user.email, refresh_token)
|
| 69 |
_store_access_token(tokens)
|
| 70 |
st.query_params.clear()
|
| 71 |
st.rerun()
|
| 72 |
|
| 73 |
|
| 74 |
+
def _ensure_drive_access(cookie_manager) -> None:
|
| 75 |
"""Populates session_state's Drive access token, silently refreshing from a
|
| 76 |
stored refresh_token when possible. Shows a one-time "Connect Google Drive"
|
| 77 |
link only when neither a live token nor a stored refresh_token exists.
|
|
|
|
| 79 |
if not st.user.is_logged_in or _access_token_valid():
|
| 80 |
return
|
| 81 |
|
| 82 |
+
if cookie_manager is not None:
|
| 83 |
+
refresh_token = cookie_token_store.get_refresh_token(cookie_manager, st.user.email)
|
| 84 |
if refresh_token:
|
| 85 |
try:
|
| 86 |
tokens = google_oauth.refresh_access_token(
|
|
|
|
| 91 |
_store_access_token(tokens)
|
| 92 |
return
|
| 93 |
except Exception:
|
| 94 |
+
cookie_token_store.delete_refresh_token(cookie_manager, st.user.email)
|
| 95 |
|
| 96 |
state = uuid.uuid4().hex
|
| 97 |
st.session_state["drive_oauth_state"] = state
|
|
|
|
| 101 |
st.link_button("Connect Google Drive", authorize_url)
|
| 102 |
|
| 103 |
|
| 104 |
+
# Constructed at most once per script run -- see get_cookie_manager's docstring.
|
| 105 |
+
_cookie_manager = cookie_token_store.get_cookie_manager() if "cookies" in st.secrets else None
|
| 106 |
+
|
| 107 |
if "auth" in st.secrets:
|
| 108 |
+
_handle_drive_oauth_callback(_cookie_manager)
|
| 109 |
|
| 110 |
with st.sidebar:
|
| 111 |
if "auth" not in st.secrets:
|
|
|
|
| 115 |
else:
|
| 116 |
st.caption(f"Logged in as {st.user.email}")
|
| 117 |
st.button("Log out", on_click=st.logout)
|
| 118 |
+
_ensure_drive_access(_cookie_manager)
|
| 119 |
|
| 120 |
home_page = st.Page("pages/home.py", title="Home", url_path="home", default=True)
|
| 121 |
skull_king_page = st.Page("pages/skull_king.py", title="Skull King", url_path="skull_king")
|
src/cookie_token_store.py
CHANGED
|
@@ -8,8 +8,16 @@ from cryptography.fernet import Fernet
|
|
| 8 |
_COOKIE_MAX_AGE_DAYS = 400 # ~ the longest lifetime browsers allow anyway (Chrome caps at 400 days)
|
| 9 |
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
return stx.CookieManager(key="bgt_cookie_manager")
|
| 14 |
|
| 15 |
|
|
@@ -22,16 +30,15 @@ def _cookie_name(email: str) -> str:
|
|
| 22 |
return "bgt_rt_" + hashlib.sha256(email.encode()).hexdigest()[:16]
|
| 23 |
|
| 24 |
|
| 25 |
-
def save_refresh_token(email: str, refresh_token: str) -> None:
|
| 26 |
encrypted = _fernet().encrypt(refresh_token.encode()).decode()
|
| 27 |
expires_at = datetime.now(timezone.utc) + timedelta(days=_COOKIE_MAX_AGE_DAYS)
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
)
|
| 31 |
|
| 32 |
|
| 33 |
-
def get_refresh_token(email: str) -> str | None:
|
| 34 |
-
encrypted =
|
| 35 |
if not encrypted:
|
| 36 |
return None
|
| 37 |
try:
|
|
@@ -40,7 +47,7 @@ def get_refresh_token(email: str) -> str | None:
|
|
| 40 |
return None
|
| 41 |
|
| 42 |
|
| 43 |
-
def delete_refresh_token(email: str) -> None:
|
| 44 |
name = _cookie_name(email)
|
| 45 |
-
if
|
| 46 |
-
|
|
|
|
| 8 |
_COOKIE_MAX_AGE_DAYS = 400 # ~ the longest lifetime browsers allow anyway (Chrome caps at 400 days)
|
| 9 |
|
| 10 |
|
| 11 |
+
def get_cookie_manager() -> stx.CookieManager:
|
| 12 |
+
"""Constructs the cookie-sync component.
|
| 13 |
+
|
| 14 |
+
Must be called exactly once per script run (not cached, not called more
|
| 15 |
+
than once) -- the underlying component re-renders every rerun to report
|
| 16 |
+
the browser's current cookies back to Python, so caching it (via
|
| 17 |
+
st.cache_resource or otherwise) freezes it stale, and constructing a
|
| 18 |
+
second instance with the same key in the same run raises
|
| 19 |
+
DuplicateWidgetID.
|
| 20 |
+
"""
|
| 21 |
return stx.CookieManager(key="bgt_cookie_manager")
|
| 22 |
|
| 23 |
|
|
|
|
| 30 |
return "bgt_rt_" + hashlib.sha256(email.encode()).hexdigest()[:16]
|
| 31 |
|
| 32 |
|
| 33 |
+
def save_refresh_token(cookie_manager: stx.CookieManager, email: str, refresh_token: str) -> None:
|
| 34 |
encrypted = _fernet().encrypt(refresh_token.encode()).decode()
|
| 35 |
expires_at = datetime.now(timezone.utc) + timedelta(days=_COOKIE_MAX_AGE_DAYS)
|
| 36 |
+
name = _cookie_name(email)
|
| 37 |
+
cookie_manager.set(name, encrypted, expires_at=expires_at, key=f"set_{name}")
|
|
|
|
| 38 |
|
| 39 |
|
| 40 |
+
def get_refresh_token(cookie_manager: stx.CookieManager, email: str) -> str | None:
|
| 41 |
+
encrypted = cookie_manager.get(_cookie_name(email))
|
| 42 |
if not encrypted:
|
| 43 |
return None
|
| 44 |
try:
|
|
|
|
| 47 |
return None
|
| 48 |
|
| 49 |
|
| 50 |
+
def delete_refresh_token(cookie_manager: stx.CookieManager, email: str) -> None:
|
| 51 |
name = _cookie_name(email)
|
| 52 |
+
if cookie_manager.get(name) is not None:
|
| 53 |
+
cookie_manager.delete(name, key=f"delete_{name}")
|