Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +66 -12
src/streamlit_app.py
CHANGED
|
@@ -75,7 +75,9 @@ st.markdown("""
|
|
| 75 |
CLIENT_ID = os.environ.get("AZURE_CLIENT_ID", "bfcbb298-4cc1-496e-9d9b-ff8c2d967a3a")
|
| 76 |
CLIENT_SECRET = os.environ.get("AZURE_CLIENT_SECRET", "") # set in HF Secrets
|
| 77 |
TENANT_ID = os.environ.get("AZURE_TENANT_ID", "5dac2bf2-8842-4788-ae07-33fb103b55d6")
|
| 78 |
-
REDIRECT_URI = os.environ.get("REDIRECT_URI", "") # e.g. https://yourspace.hf.space/
|
|
|
|
|
|
|
| 79 |
|
| 80 |
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
|
| 81 |
AUTH_ENDPOINT = f"{AUTHORITY}/oauth2/v2.0/authorize"
|
|
@@ -104,6 +106,36 @@ def configured_group_ids():
|
|
| 104 |
|
| 105 |
DEFAULT_GROUP_IDS = configured_group_ids()
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
# ββ Session defaults βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 108 |
for k, v in {
|
| 109 |
"token": None,
|
|
@@ -124,6 +156,8 @@ for k, v in {
|
|
| 124 |
# ββ OAuth helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 125 |
|
| 126 |
def build_auth_url():
|
|
|
|
|
|
|
| 127 |
state = secrets.token_urlsafe(16)
|
| 128 |
st.session_state.oauth_state = state
|
| 129 |
params = {
|
|
@@ -152,27 +186,36 @@ def exchange_code_for_token(code: str) -> str:
|
|
| 152 |
if not resp.ok:
|
| 153 |
detail = payload.get("error_description") or payload.get("error") or resp.text
|
| 154 |
raise RuntimeError(f"Token exchange failed: {detail}")
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
|
| 158 |
# ββ Check for OAuth callback (code in URL query params) βββββββββββββββββββββββ
|
| 159 |
query_params = st.query_params
|
| 160 |
if not st.session_state.token and "code" in query_params:
|
| 161 |
-
code = query_params
|
| 162 |
-
state =
|
| 163 |
if state == st.session_state.oauth_state or not st.session_state.oauth_state:
|
| 164 |
try:
|
| 165 |
with st.spinner("Completing sign-in..."):
|
| 166 |
st.session_state.token = exchange_code_for_token(code)
|
| 167 |
# Clear the code from the URL
|
| 168 |
-
|
| 169 |
st.rerun()
|
| 170 |
except Exception as e:
|
| 171 |
st.error(f"Sign-in failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
# ββ Email helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 174 |
|
| 175 |
def get_emails(token, top=50):
|
|
|
|
| 176 |
url = "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages"
|
| 177 |
headers = {
|
| 178 |
"Authorization": f"Bearer {token}",
|
|
@@ -189,6 +232,7 @@ def get_emails(token, top=50):
|
|
| 189 |
|
| 190 |
|
| 191 |
def graph_get_json(token, url, params=None):
|
|
|
|
| 192 |
headers = {
|
| 193 |
"Authorization": f"Bearer {token}",
|
| 194 |
"Prefer": 'outlook.body-content-type="html"',
|
|
@@ -200,6 +244,8 @@ def graph_get_json(token, url, params=None):
|
|
| 200 |
payload = {"error": {"message": r.text}}
|
| 201 |
if not r.ok:
|
| 202 |
detail = (payload.get("error") or {}).get("message") or r.text
|
|
|
|
|
|
|
| 203 |
raise RuntimeError(f"Graph API error ({r.status_code}): {detail}")
|
| 204 |
return payload
|
| 205 |
|
|
@@ -589,12 +635,17 @@ with col2:
|
|
| 589 |
st.markdown('<span class="badge-pending">Not signed in</span>', unsafe_allow_html=True)
|
| 590 |
|
| 591 |
if not st.session_state.token:
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 598 |
else:
|
| 599 |
st.markdown("You are signed in. β")
|
| 600 |
if st.button("Sign out"):
|
|
@@ -656,6 +707,10 @@ if st.button("βΆ Run", disabled=not st.session_state.token):
|
|
| 656 |
raw = get_emails(st.session_state.token, int(top_n))
|
| 657 |
st.session_state.messages = raw
|
| 658 |
st.session_state.messages_df = pd.DataFrame(build_fetched_emails(raw)) if raw else pd.DataFrame()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
except ValueError as e:
|
| 660 |
st.error(str(e))
|
| 661 |
st.stop()
|
|
@@ -784,4 +839,3 @@ if st.session_state.rj_emails:
|
|
| 784 |
file_name=f"rj_emails_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx",
|
| 785 |
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 786 |
)
|
| 787 |
-
|
|
|
|
| 75 |
CLIENT_ID = os.environ.get("AZURE_CLIENT_ID", "bfcbb298-4cc1-496e-9d9b-ff8c2d967a3a")
|
| 76 |
CLIENT_SECRET = os.environ.get("AZURE_CLIENT_SECRET", "") # set in HF Secrets
|
| 77 |
TENANT_ID = os.environ.get("AZURE_TENANT_ID", "5dac2bf2-8842-4788-ae07-33fb103b55d6")
|
| 78 |
+
REDIRECT_URI = os.environ.get("REDIRECT_URI", "").strip() # e.g. https://yourspace.hf.space/
|
| 79 |
+
if not REDIRECT_URI and os.environ.get("SPACE_HOST"):
|
| 80 |
+
REDIRECT_URI = f"https://{os.environ['SPACE_HOST'].strip('/')}/"
|
| 81 |
|
| 82 |
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
|
| 83 |
AUTH_ENDPOINT = f"{AUTHORITY}/oauth2/v2.0/authorize"
|
|
|
|
| 106 |
|
| 107 |
DEFAULT_GROUP_IDS = configured_group_ids()
|
| 108 |
|
| 109 |
+
|
| 110 |
+
class AuthError(RuntimeError):
|
| 111 |
+
pass
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def get_query_param_value(params, key, default=""):
|
| 115 |
+
value = params.get(key, default)
|
| 116 |
+
if isinstance(value, list):
|
| 117 |
+
return value[0] if value else default
|
| 118 |
+
return value or default
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def is_probably_graph_access_token(token) -> bool:
|
| 122 |
+
token = (token or "").strip()
|
| 123 |
+
return token.count(".") in (2, 4)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def require_valid_access_token(token) -> str:
|
| 127 |
+
token = (token or "").strip()
|
| 128 |
+
if not is_probably_graph_access_token(token):
|
| 129 |
+
raise AuthError("Microsoft sign-in did not produce a valid Graph access token. Please sign in again.")
|
| 130 |
+
return token
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def clear_oauth_query_params():
|
| 134 |
+
try:
|
| 135 |
+
st.query_params.clear()
|
| 136 |
+
except Exception:
|
| 137 |
+
pass
|
| 138 |
+
|
| 139 |
# ββ Session defaults βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 140 |
for k, v in {
|
| 141 |
"token": None,
|
|
|
|
| 156 |
# ββ OAuth helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 157 |
|
| 158 |
def build_auth_url():
|
| 159 |
+
if not REDIRECT_URI:
|
| 160 |
+
raise AuthError("REDIRECT_URI is not configured. Set it to this app's public URL registered in Azure.")
|
| 161 |
state = secrets.token_urlsafe(16)
|
| 162 |
st.session_state.oauth_state = state
|
| 163 |
params = {
|
|
|
|
| 186 |
if not resp.ok:
|
| 187 |
detail = payload.get("error_description") or payload.get("error") or resp.text
|
| 188 |
raise RuntimeError(f"Token exchange failed: {detail}")
|
| 189 |
+
access_token = payload.get("access_token")
|
| 190 |
+
if not is_probably_graph_access_token(access_token):
|
| 191 |
+
detail = payload.get("error_description") or payload.get("error") or "No valid access_token was returned."
|
| 192 |
+
raise AuthError(f"Token exchange failed: {detail}")
|
| 193 |
+
return access_token
|
| 194 |
|
| 195 |
|
| 196 |
# ββ Check for OAuth callback (code in URL query params) βββββββββββββββββββββββ
|
| 197 |
query_params = st.query_params
|
| 198 |
if not st.session_state.token and "code" in query_params:
|
| 199 |
+
code = get_query_param_value(query_params, "code")
|
| 200 |
+
state = get_query_param_value(query_params, "state")
|
| 201 |
if state == st.session_state.oauth_state or not st.session_state.oauth_state:
|
| 202 |
try:
|
| 203 |
with st.spinner("Completing sign-in..."):
|
| 204 |
st.session_state.token = exchange_code_for_token(code)
|
| 205 |
# Clear the code from the URL
|
| 206 |
+
clear_oauth_query_params()
|
| 207 |
st.rerun()
|
| 208 |
except Exception as e:
|
| 209 |
st.error(f"Sign-in failed: {e}")
|
| 210 |
+
clear_oauth_query_params()
|
| 211 |
+
else:
|
| 212 |
+
st.error("Sign-in failed: Microsoft returned an unexpected state. Please try signing in again.")
|
| 213 |
+
clear_oauth_query_params()
|
| 214 |
|
| 215 |
# ββ Email helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 216 |
|
| 217 |
def get_emails(token, top=50):
|
| 218 |
+
token = require_valid_access_token(token)
|
| 219 |
url = "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages"
|
| 220 |
headers = {
|
| 221 |
"Authorization": f"Bearer {token}",
|
|
|
|
| 232 |
|
| 233 |
|
| 234 |
def graph_get_json(token, url, params=None):
|
| 235 |
+
token = require_valid_access_token(token)
|
| 236 |
headers = {
|
| 237 |
"Authorization": f"Bearer {token}",
|
| 238 |
"Prefer": 'outlook.body-content-type="html"',
|
|
|
|
| 244 |
payload = {"error": {"message": r.text}}
|
| 245 |
if not r.ok:
|
| 246 |
detail = (payload.get("error") or {}).get("message") or r.text
|
| 247 |
+
if r.status_code == 401:
|
| 248 |
+
raise AuthError(f"Microsoft session is invalid or expired: {detail}")
|
| 249 |
raise RuntimeError(f"Graph API error ({r.status_code}): {detail}")
|
| 250 |
return payload
|
| 251 |
|
|
|
|
| 635 |
st.markdown('<span class="badge-pending">Not signed in</span>', unsafe_allow_html=True)
|
| 636 |
|
| 637 |
if not st.session_state.token:
|
| 638 |
+
try:
|
| 639 |
+
auth_url = build_auth_url()
|
| 640 |
+
except AuthError as e:
|
| 641 |
+
st.error(str(e))
|
| 642 |
+
auth_url = None
|
| 643 |
+
if auth_url:
|
| 644 |
+
st.markdown(
|
| 645 |
+
f'<div class="login-btn"><a href="{auth_url}" target="_self">π Sign in with Microsoft</a></div>',
|
| 646 |
+
unsafe_allow_html=True,
|
| 647 |
+
)
|
| 648 |
+
st.caption("You'll be redirected to Microsoft's login page and back automatically.")
|
| 649 |
else:
|
| 650 |
st.markdown("You are signed in. β")
|
| 651 |
if st.button("Sign out"):
|
|
|
|
| 707 |
raw = get_emails(st.session_state.token, int(top_n))
|
| 708 |
st.session_state.messages = raw
|
| 709 |
st.session_state.messages_df = pd.DataFrame(build_fetched_emails(raw)) if raw else pd.DataFrame()
|
| 710 |
+
except AuthError as e:
|
| 711 |
+
st.error(str(e))
|
| 712 |
+
st.session_state.token = None
|
| 713 |
+
st.stop()
|
| 714 |
except ValueError as e:
|
| 715 |
st.error(str(e))
|
| 716 |
st.stop()
|
|
|
|
| 839 |
file_name=f"rj_emails_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx",
|
| 840 |
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 841 |
)
|
|
|