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"]
|
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
|
@respx.mock
|
||||||
def test_adobe_exchange_stores_token():
|
def test_adobe_exchange_stores_token():
|
||||||
"""POST /api/auth/adobe/exchange with a valid redirect URL → session connected."""
|
"""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
|
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")
|
@router.get("/adobe/disconnect")
|
||||||
def adobe_disconnect(request: Request):
|
def adobe_disconnect(request: Request):
|
||||||
session = get_session(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")
|
@router.get("/docusign/connect")
|
||||||
|
|
@ -155,10 +192,59 @@ def docusign_connect(request: Request):
|
||||||
return response
|
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")
|
@router.get("/docusign/disconnect")
|
||||||
def docusign_disconnect(request: Request):
|
def docusign_disconnect(request: Request):
|
||||||
session = get_session(request)
|
session = get_session(request)
|
||||||
session.pop("docusign_access_token", None)
|
session.pop("docusign_access_token", None)
|
||||||
|
session.pop("docusign_refresh_token", None)
|
||||||
response = JSONResponse({"disconnected": "docusign"})
|
response = JSONResponse({"disconnected": "docusign"})
|
||||||
save_session(response, session)
|
save_session(response, session)
|
||||||
return response
|
return response
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,13 @@ async function refreshAuth() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAuthBar() {
|
function renderAuthBar() {
|
||||||
// Adobe: manual paste flow
|
// Adobe: use .env credentials (primary), OAuth dialog (secondary)
|
||||||
const adobeEl = $('badge-adobe');
|
const adobeEl = $('badge-adobe');
|
||||||
adobeEl.textContent = authState.adobe ? '✓ Adobe Sign' : 'Connect Adobe Sign';
|
adobeEl.textContent = authState.adobe ? '✓ Adobe Sign' : 'Connect Adobe Sign';
|
||||||
adobeEl.className = 'auth-badge' + (authState.adobe ? ' connected' : '');
|
adobeEl.className = 'auth-badge' + (authState.adobe ? ' connected' : '');
|
||||||
adobeEl.onclick = authState.adobe
|
adobeEl.onclick = authState.adobe
|
||||||
? () => disconnectPlatform('adobe')
|
? () => disconnectPlatform('adobe')
|
||||||
: () => startAdobeAuth();
|
: () => connectAdobeEnv();
|
||||||
|
|
||||||
// DocuSign: JWT grant from .env — no browser sign-in needed
|
// DocuSign: JWT grant from .env — no browser sign-in needed
|
||||||
const dsEl = $('badge-docusign');
|
const dsEl = $('badge-docusign');
|
||||||
|
|
@ -54,6 +54,26 @@ async function disconnectPlatform(platform) {
|
||||||
await refreshTemplates();
|
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() {
|
async function connectDocusign() {
|
||||||
const dsEl = $('badge-docusign');
|
const dsEl = $('badge-docusign');
|
||||||
dsEl.textContent = 'Connecting…';
|
dsEl.textContent = 'Connecting…';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue