Seth commited on
Commit
187f183
·
1 Parent(s): c828443
backend/app/main.py CHANGED
@@ -114,6 +114,8 @@ app.include_router(outreach_router)
114
  # Create uploads directory
115
  UPLOAD_DIR = Path("/data/uploads")
116
  UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
 
 
117
  UNIPILE_API_BASE = os.getenv("UNIPILE_API_BASE", "").rstrip("/")
118
  UNIPILE_API_KEY = os.getenv("UNIPILE_API_KEY", "")
119
  FRONTEND_ORIGIN = os.getenv("FRONTEND_ORIGIN", "").rstrip("/")
@@ -224,17 +226,26 @@ def _linkedin_public_identifier(raw: str) -> str:
224
  return s.split("?")[0].strip("/").strip()
225
 
226
 
 
 
 
 
 
 
 
227
  def _require_unipile_config():
228
- if not UNIPILE_API_BASE or not UNIPILE_API_KEY:
 
229
  raise HTTPException(
230
  status_code=400,
231
- detail="UniPile is not configured. Set UNIPILE_API_BASE and UNIPILE_API_KEY in backend env.",
232
  )
233
 
234
 
235
  def _unipile_headers():
 
236
  return {
237
- "X-API-KEY": UNIPILE_API_KEY,
238
  "accept": "application/json",
239
  "content-type": "application/json",
240
  }
@@ -246,7 +257,8 @@ def _unipile_call(method: str, path: str, payload: Optional[dict] = None):
246
  Raises HTTPException only on transport failure (not on 4xx/5xx responses).
247
  """
248
  _require_unipile_config()
249
- url = f"{UNIPILE_API_BASE}{path}"
 
250
  try:
251
  resp = requests.request(
252
  method=method.upper(),
@@ -278,6 +290,14 @@ def _unipile_request(method: str, path: str, payload: Optional[dict] = None):
278
  )
279
  if isinstance(msg, (dict, list)):
280
  msg = json.dumps(msg)
 
 
 
 
 
 
 
 
281
  raise HTTPException(
282
  status_code=status,
283
  detail=msg or f"UniPile error ({status}): {data if not isinstance(data, dict) else json.dumps(data)}",
@@ -1400,10 +1420,11 @@ async def create_unipile_mailbox_hosted_link(
1400
  t.db.add(state)
1401
  t.db.commit()
1402
  expires_iso = expires_at.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
 
1403
  payload = {
1404
  "type": "create",
1405
  "providers": ["GOOGLE"],
1406
- "api_url": UNIPILE_API_BASE,
1407
  "expiresOn": expires_iso,
1408
  "notify_url": f"{origin}/api/unipile/mailbox/hosted-callback",
1409
  "success_redirect_url": f"{origin}/settings?mailbox=connected",
@@ -1587,10 +1608,11 @@ async def create_unipile_linkedin_hosted_link(
1587
  t.db.commit()
1588
 
1589
  expires_iso = expires_at.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
 
1590
  payload = {
1591
  "type": "create",
1592
  "providers": ["LINKEDIN"],
1593
- "api_url": UNIPILE_API_BASE,
1594
  "expiresOn": expires_iso,
1595
  "notify_url": f"{origin}/api/unipile/linkedin/hosted-callback",
1596
  "success_redirect_url": f"{origin}/settings?linkedin=connected",
 
114
  # Create uploads directory
115
  UPLOAD_DIR = Path("/data/uploads")
116
  UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
117
+ # UniPile credentials are read fresh on each request (see _load_unipile_env) so Spaces secret
118
+ # updates and stripping whitespace behave reliably. Module-level kept for backwards compat.
119
  UNIPILE_API_BASE = os.getenv("UNIPILE_API_BASE", "").rstrip("/")
120
  UNIPILE_API_KEY = os.getenv("UNIPILE_API_KEY", "")
121
  FRONTEND_ORIGIN = os.getenv("FRONTEND_ORIGIN", "").rstrip("/")
 
226
  return s.split("?")[0].strip("/").strip()
227
 
228
 
229
+ def _load_unipile_env() -> tuple[str, str]:
230
+ """Read UniPile DSN and API key from the environment (strip whitespace / newlines from secrets)."""
231
+ base = (os.getenv("UNIPILE_API_BASE") or "").strip().rstrip("/")
232
+ key = (os.getenv("UNIPILE_API_KEY") or "").strip()
233
+ return base, key
234
+
235
+
236
  def _require_unipile_config():
237
+ base, key = _load_unipile_env()
238
+ if not base or not key:
239
  raise HTTPException(
240
  status_code=400,
241
+ detail="UniPile is not configured. Set UNIPILE_API_BASE and UNIPILE_API_KEY in backend env (Hugging Face: Space → Settings → Secrets).",
242
  )
243
 
244
 
245
  def _unipile_headers():
246
+ _, key = _load_unipile_env()
247
  return {
248
+ "X-API-KEY": key,
249
  "accept": "application/json",
250
  "content-type": "application/json",
251
  }
 
257
  Raises HTTPException only on transport failure (not on 4xx/5xx responses).
258
  """
259
  _require_unipile_config()
260
+ base, _ = _load_unipile_env()
261
+ url = f"{base}{path}"
262
  try:
263
  resp = requests.request(
264
  method=method.upper(),
 
290
  )
291
  if isinstance(msg, (dict, list)):
292
  msg = json.dumps(msg)
293
+ err_type = _safe_str(data.get("type"))
294
+ if status == 401 and err_type == "errors/missing_credentials":
295
+ msg = (
296
+ "UniPile rejected the request (missing or invalid API credentials). "
297
+ "In Hugging Face Space → Settings → Secrets, set UNIPILE_API_KEY to your UniPile API access token "
298
+ "(Dashboard → API keys) and UNIPILE_API_BASE to your UniPile API URL (no trailing slash). "
299
+ "Remove accidental quotes or spaces when pasting; redeploy/restart the Space after saving."
300
+ )
301
  raise HTTPException(
302
  status_code=status,
303
  detail=msg or f"UniPile error ({status}): {data if not isinstance(data, dict) else json.dumps(data)}",
 
1420
  t.db.add(state)
1421
  t.db.commit()
1422
  expires_iso = expires_at.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
1423
+ api_base, _ = _load_unipile_env()
1424
  payload = {
1425
  "type": "create",
1426
  "providers": ["GOOGLE"],
1427
+ "api_url": api_base,
1428
  "expiresOn": expires_iso,
1429
  "notify_url": f"{origin}/api/unipile/mailbox/hosted-callback",
1430
  "success_redirect_url": f"{origin}/settings?mailbox=connected",
 
1608
  t.db.commit()
1609
 
1610
  expires_iso = expires_at.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
1611
+ api_base, _ = _load_unipile_env()
1612
  payload = {
1613
  "type": "create",
1614
  "providers": ["LINKEDIN"],
1615
+ "api_url": api_base,
1616
  "expiresOn": expires_iso,
1617
  "notify_url": f"{origin}/api/unipile/linkedin/hosted-callback",
1618
  "success_redirect_url": f"{origin}/settings?linkedin=connected",
backend/app/outreach_routes.py CHANGED
@@ -95,8 +95,8 @@ def _linkedin_public_identifier(raw: str) -> str:
95
 
96
 
97
  def _unipile_env() -> Tuple[str, str]:
98
- base = os.getenv("UNIPILE_API_BASE", "").rstrip("/")
99
- key = os.getenv("UNIPILE_API_KEY", "")
100
  return base, key
101
 
102
 
 
95
 
96
 
97
  def _unipile_env() -> Tuple[str, str]:
98
+ base = (os.getenv("UNIPILE_API_BASE") or "").strip().rstrip("/")
99
+ key = (os.getenv("UNIPILE_API_KEY") or "").strip()
100
  return base, key
101
 
102