| """Streamlit sign-in / sign-up gate and session-cookie glue. |
| |
| Kept out of the main app file so the auth flow is self-contained. The cookie |
| component (``extra_streamlit_components``) is imported lazily; if it is not |
| installed the app still works, but "stay logged in" downgrades to per-session |
| (a browser refresh will require signing in again). |
| """ |
|
|
| from __future__ import annotations |
|
|
| import datetime as _dt |
| import logging |
| from typing import Any, Dict, Optional |
|
|
| import streamlit as st |
|
|
| from .auth import AuthError |
|
|
| logger = logging.getLogger("agadvisor.accounts.ui") |
|
|
| COOKIE_NAME = "agadvisor_session" |
| _COOKIE_TTL_DAYS = 30 |
|
|
|
|
| def _get_cookie_manager(): |
| """Return a cached CookieManager, or None if the component is unavailable.""" |
| if "cookie_manager" in st.session_state: |
| return st.session_state.cookie_manager |
| try: |
| import extra_streamlit_components as stx |
|
|
| mgr = stx.CookieManager(key="agadvisor_cookie_mgr") |
| st.session_state.cookie_manager = mgr |
| return mgr |
| except Exception as e: |
| logger.info("Cookie manager unavailable (%s); using session-only auth.", e) |
| st.session_state.cookie_manager = None |
| return None |
|
|
|
|
| def _set_session_cookie(token: str) -> None: |
| mgr = _get_cookie_manager() |
| if mgr is None: |
| return |
| try: |
| expires = _dt.datetime.now() + _dt.timedelta(days=_COOKIE_TTL_DAYS) |
| mgr.set(COOKIE_NAME, token, expires_at=expires, key="set_session_cookie") |
| except Exception as e: |
| logger.info("Could not set session cookie: %s", e) |
|
|
|
|
| def _clear_session_cookie() -> None: |
| mgr = _get_cookie_manager() |
| if mgr is None: |
| return |
| try: |
| mgr.delete(COOKIE_NAME, key="del_session_cookie") |
| except Exception as e: |
| logger.info("Could not clear session cookie: %s", e) |
|
|
|
|
| def _read_cookie_token() -> Optional[str]: |
| mgr = _get_cookie_manager() |
| if mgr is None: |
| return None |
| try: |
| return mgr.get(COOKIE_NAME) |
| except Exception: |
| return None |
|
|
|
|
| def _resolve_user(service) -> Optional[Dict[str, Any]]: |
| """Return the logged-in user from session_state, else from a valid cookie.""" |
| if st.session_state.get("user"): |
| return st.session_state["user"] |
| token = _read_cookie_token() |
| if token: |
| user = service.user_from_token(token) |
| if user: |
| st.session_state["user"] = {"id": user["id"], "username": user["username"]} |
| return st.session_state["user"] |
| return None |
|
|
|
|
| def logout(service=None) -> None: |
| """Flush any pending sync, clear the cookie and session, and rerun.""" |
| try: |
| if service is not None and getattr(service, "sync", None) is not None: |
| service.sync.flush() |
| except Exception: |
| pass |
| _clear_session_cookie() |
| for k in ("user", "chats", "current_chat_id", "chat_counter"): |
| st.session_state.pop(k, None) |
| st.rerun() |
|
|
|
|
| def render_login_gate(service) -> Optional[Dict[str, Any]]: |
| """Render the sign-in / sign-up UI. Returns the user on success, else None |
| (call ``st.stop()`` in the caller when None).""" |
| st.markdown("### 🌿 Welcome to AgAdvisor") |
| st.caption("Sign in to save your conversations. Your chat history is private to your account.") |
|
|
| tab_login, tab_signup = st.tabs(["Sign in", "Create account"]) |
|
|
| with tab_login: |
| with st.form("login_form", clear_on_submit=False): |
| username = st.text_input("Username", key="login_username") |
| password = st.text_input("Password", type="password", key="login_password") |
| submitted = st.form_submit_button("Sign in", use_container_width=True, type="primary") |
| if submitted: |
| try: |
| user = service.login(username, password) |
| except AuthError as e: |
| st.error(str(e)) |
| else: |
| st.session_state["user"] = user |
| _set_session_cookie(service.issue_token(user["id"])) |
| st.success(f"Welcome back, {user['username']}!") |
| st.rerun() |
|
|
| with tab_signup: |
| with st.form("signup_form", clear_on_submit=False): |
| new_username = st.text_input("Choose a username", key="signup_username") |
| new_password = st.text_input("Choose a password", type="password", key="signup_password") |
| confirm = st.text_input("Confirm password", type="password", key="signup_confirm") |
| submitted = st.form_submit_button("Create account", use_container_width=True, type="primary") |
| if submitted: |
| if new_password != confirm: |
| st.error("Passwords do not match.") |
| else: |
| try: |
| user = service.signup(new_username, new_password) |
| except AuthError as e: |
| st.error(str(e)) |
| else: |
| st.session_state["user"] = user |
| _set_session_cookie(service.issue_token(user["id"])) |
| st.success(f"Account created — welcome, {user['username']}!") |
| st.rerun() |
|
|
| return None |
|
|
|
|
| def require_auth(service) -> Dict[str, Any]: |
| """Gate the app: return the current user, or render the login gate and stop.""" |
| user = _resolve_user(service) |
| if user: |
| return user |
| render_login_gate(service) |
| st.stop() |
|
|