carrief0908 commited on
Commit
33b7da9
Β·
verified Β·
1 Parent(s): 2fb6dc8

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. 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
- return payload["access_token"]
 
 
 
 
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["code"]
162
- state = query_params.get("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
- st.query_params.clear()
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
- auth_url = build_auth_url()
593
- st.markdown(
594
- f'<div class="login-btn"><a href="{auth_url}" target="_self">πŸ” Sign in with Microsoft</a></div>',
595
- unsafe_allow_html=True,
596
- )
597
- st.caption("You'll be redirected to Microsoft's login page and back automatically.")
 
 
 
 
 
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
  )