fix: Adobe Sign connects via .env refresh token; restore DocuSign OAuth
- GET /api/auth/adobe/connect: reads ADOBE_REFRESH_TOKEN from .env, refreshes the access token, stores in session — no login required - Falls back to OAuth dialog only if no .env credentials exist - Restores DocuSign OAuth start/callback endpoints alongside JWT connect - 33/33 tests passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
aa88ba363d
commit
c63d49e208
|
|
@ -37,6 +37,31 @@ def test_adobe_url_returns_auth_url():
|
|||
assert "localhost%3A8080" in data["url"] or "localhost:8080" in data["url"]
|
||||
|
||||
|
||||
def test_adobe_connect_env_stores_token(monkeypatch):
|
||||
"""GET /api/auth/adobe/connect uses .env refresh token → session connected."""
|
||||
monkeypatch.setenv("ADOBE_ACCESS_TOKEN", "existing-token")
|
||||
monkeypatch.setenv("ADOBE_REFRESH_TOKEN", "existing-refresh")
|
||||
|
||||
from unittest.mock import patch
|
||||
with patch("adobe_api._refresh_access_token", return_value="refreshed-token"):
|
||||
resp = client.get("/api/auth/adobe/connect")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["connected"] is True
|
||||
session_cookie = resp.cookies.get("migrator_session")
|
||||
status_resp = client.get("/api/auth/status", cookies={"migrator_session": session_cookie})
|
||||
assert status_resp.json()["adobe"] is True
|
||||
|
||||
|
||||
def test_adobe_connect_env_fails_without_credentials(monkeypatch):
|
||||
"""GET /api/auth/adobe/connect with no .env tokens → 400."""
|
||||
monkeypatch.delenv("ADOBE_ACCESS_TOKEN", raising=False)
|
||||
monkeypatch.delenv("ADOBE_REFRESH_TOKEN", raising=False)
|
||||
resp = client.get("/api/auth/adobe/connect")
|
||||
assert resp.status_code == 400
|
||||
assert "No Adobe Sign credentials" in resp.json()["error"]
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_adobe_exchange_stores_token():
|
||||
"""POST /api/auth/adobe/exchange with a valid redirect URL → session connected."""
|
||||
|
|
|
|||
|
|
@ -116,6 +116,43 @@ async def adobe_exchange(body: AdobeExchangeRequest, request: Request):
|
|||
return response
|
||||
|
||||
|
||||
@router.get("/adobe/connect")
|
||||
def adobe_connect_env(request: Request):
|
||||
"""
|
||||
Load Adobe Sign credentials directly from .env (ADOBE_ACCESS_TOKEN /
|
||||
ADOBE_REFRESH_TOKEN). Refreshes the token if needed. No browser login required
|
||||
when a valid refresh token already exists from a previous CLI auth session.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
|
||||
from adobe_api import _refresh_access_token
|
||||
|
||||
token = os.getenv("ADOBE_ACCESS_TOKEN")
|
||||
refresh_token = os.getenv("ADOBE_REFRESH_TOKEN")
|
||||
|
||||
if not token and not refresh_token:
|
||||
return JSONResponse(
|
||||
{"error": "No Adobe Sign credentials found in .env. Run src/adobe_auth.py first."},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# Always refresh to ensure the token is fresh (access tokens expire in ~1h)
|
||||
if refresh_token:
|
||||
try:
|
||||
token = _refresh_access_token()
|
||||
except RuntimeError as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
session = get_session(request)
|
||||
session["adobe_access_token"] = token
|
||||
session["adobe_refresh_token"] = refresh_token
|
||||
|
||||
response = JSONResponse({"connected": True})
|
||||
save_session(response, session)
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/adobe/disconnect")
|
||||
def adobe_disconnect(request: Request):
|
||||
session = get_session(request)
|
||||
|
|
@ -127,7 +164,7 @@ def adobe_disconnect(request: Request):
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DocuSign — JWT grant using existing .env credentials
|
||||
# DocuSign — JWT grant (.env) or OAuth redirect
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/docusign/connect")
|
||||
|
|
@ -155,10 +192,59 @@ def docusign_connect(request: Request):
|
|||
return response
|
||||
|
||||
|
||||
@router.get("/docusign/start")
|
||||
def docusign_start():
|
||||
"""Redirect to DocuSign OAuth (alternative to JWT grant)."""
|
||||
import base64 as _b64
|
||||
params = (
|
||||
f"?response_type=code"
|
||||
f"&scope=signature"
|
||||
f"&client_id={settings.docusign_client_id}"
|
||||
f"&redirect_uri={settings.docusign_redirect_uri}"
|
||||
)
|
||||
return RedirectResponse(f"https://{settings.docusign_auth_server}/oauth/auth" + params)
|
||||
|
||||
|
||||
@router.get("/docusign/callback")
|
||||
async def docusign_callback(request: Request, code: str = ""):
|
||||
"""Handle DocuSign OAuth redirect callback."""
|
||||
import base64
|
||||
if not code:
|
||||
return JSONResponse({"error": "missing code"}, status_code=400)
|
||||
|
||||
credentials = base64.b64encode(
|
||||
f"{settings.docusign_client_id}:{settings.docusign_client_secret}".encode()
|
||||
).decode()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
f"https://{settings.docusign_auth_server}/oauth/token",
|
||||
headers={"Authorization": f"Basic {credentials}"},
|
||||
data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": settings.docusign_redirect_uri,
|
||||
},
|
||||
)
|
||||
|
||||
if not resp.is_success:
|
||||
return JSONResponse({"error": "token exchange failed", "detail": resp.text}, status_code=502)
|
||||
|
||||
token_data = resp.json()
|
||||
session = get_session(request)
|
||||
session["docusign_access_token"] = token_data.get("access_token")
|
||||
session["docusign_refresh_token"] = token_data.get("refresh_token")
|
||||
|
||||
response = RedirectResponse("/")
|
||||
save_session(response, session)
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/docusign/disconnect")
|
||||
def docusign_disconnect(request: Request):
|
||||
session = get_session(request)
|
||||
session.pop("docusign_access_token", None)
|
||||
session.pop("docusign_refresh_token", None)
|
||||
response = JSONResponse({"disconnected": "docusign"})
|
||||
save_session(response, session)
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ async function refreshAuth() {
|
|||
}
|
||||
|
||||
function renderAuthBar() {
|
||||
// Adobe: manual paste flow
|
||||
// Adobe: use .env credentials (primary), OAuth dialog (secondary)
|
||||
const adobeEl = $('badge-adobe');
|
||||
adobeEl.textContent = authState.adobe ? '✓ Adobe Sign' : 'Connect Adobe Sign';
|
||||
adobeEl.className = 'auth-badge' + (authState.adobe ? ' connected' : '');
|
||||
adobeEl.onclick = authState.adobe
|
||||
? () => disconnectPlatform('adobe')
|
||||
: () => startAdobeAuth();
|
||||
: () => connectAdobeEnv();
|
||||
|
||||
// DocuSign: JWT grant from .env — no browser sign-in needed
|
||||
const dsEl = $('badge-docusign');
|
||||
|
|
@ -54,6 +54,26 @@ async function disconnectPlatform(platform) {
|
|||
await refreshTemplates();
|
||||
}
|
||||
|
||||
async function connectAdobeEnv() {
|
||||
const el = $('badge-adobe');
|
||||
el.textContent = 'Connecting…';
|
||||
const resp = await fetch('/api/auth/adobe/connect');
|
||||
const data = await resp.json();
|
||||
if (data.connected) {
|
||||
authState.adobe = true;
|
||||
renderAuthBar();
|
||||
await refreshTemplates();
|
||||
} else {
|
||||
el.textContent = 'Connect Adobe Sign';
|
||||
// If .env has no credentials, fall back to the OAuth dialog
|
||||
if (data.error && data.error.includes('No Adobe Sign credentials')) {
|
||||
startAdobeAuth();
|
||||
} else {
|
||||
setStatus('Adobe Sign error: ' + (data.error || 'unknown'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function connectDocusign() {
|
||||
const dsEl = $('badge-docusign');
|
||||
dsEl.textContent = 'Connecting…';
|
||||
|
|
|
|||
Loading…
Reference in New Issue