Use Adobe OAuth callback flow in web UI
This commit is contained in:
parent
8f0b14bc62
commit
e19bd68ebd
|
|
@ -13,6 +13,7 @@ ADOBE_CLIENT_SECRET=your-adobe-client-secret
|
|||
ADOBE_ACCESS_TOKEN=
|
||||
ADOBE_REFRESH_TOKEN=
|
||||
ADOBE_SIGN_BASE_URL=https://api.eu2.adobesign.com/api/rest/v6
|
||||
ADOBE_REDIRECT_URI=http://localhost:8000/api/auth/adobe/callback
|
||||
|
||||
# ─── DocuSign ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ variables with descriptions. Use `account-d.docusign.com` and
|
|||
`https://demo.docusign.net/restapi` for sandbox; for production replace with
|
||||
`account.docusign.com` and your account's base URL (e.g. `https://na3.docusign.net/restapi`).
|
||||
|
||||
**3. Authenticate with Adobe Sign** (one-time):
|
||||
**3. Authenticate with Adobe Sign** (one-time for CLI use):
|
||||
```bash
|
||||
python3 src/adobe_auth.py
|
||||
```
|
||||
|
|
@ -126,6 +126,7 @@ The web UI now supports concurrent testers on one shared deployment:
|
|||
- each browser gets its own server-side session file
|
||||
- DocuSign web OAuth is isolated per tester session
|
||||
- migration history and batch-job polling are scoped to that tester session
|
||||
- Adobe Sign web auth now supports the same redirect-based browser callback pattern as DocuSign
|
||||
- Adobe Sign can still be connected from shared `.env` credentials if you use the top-bar Adobe connect flow
|
||||
|
||||
Important behavior:
|
||||
|
|
@ -152,6 +153,7 @@ Important behavior:
|
|||
1. **Create a project** — the switcher modal opens on first run; name it after the customer.
|
||||
2. **Connect platforms** — click the Adobe Sign and Docusign chips in the top bar.
|
||||
- For group testing, each tester should connect Docusign in their own browser.
|
||||
- Adobe Sign also supports a normal browser redirect callback when shared `.env` credentials are not being used.
|
||||
- If your DocuSign login belongs to multiple accounts, the app will prompt you to choose one account for this session.
|
||||
- Settings now shows the browser session ID, auth mode, and selected DocuSign account for easier troubleshooting.
|
||||
3. **Review templates** — the Templates view shows readiness badges:
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ def _userinfo_payload():
|
|||
def temp_session_store(tmp_path, monkeypatch):
|
||||
import web.config as cfg
|
||||
monkeypatch.setattr(cfg.settings, "session_store_dir", str(tmp_path / ".session-store"))
|
||||
monkeypatch.setattr(cfg.settings, "adobe_redirect_uri", "http://localhost:8000/api/auth/adobe/callback")
|
||||
client.cookies.clear()
|
||||
yield
|
||||
client.cookies.clear()
|
||||
|
|
@ -64,8 +65,8 @@ def test_adobe_url_returns_auth_url():
|
|||
assert "url" in data
|
||||
assert "adobesign.com" in data["url"]
|
||||
assert "response_type=code" in data["url"]
|
||||
# Must use the registered redirect URI
|
||||
assert "localhost%3A8080" in data["url"] or "localhost:8080" in data["url"]
|
||||
assert "redirect_uri=http://localhost:8000/api/auth/adobe/callback" in data["url"]
|
||||
assert resp.cookies.get("migrator_session") is not None
|
||||
|
||||
|
||||
def test_adobe_connect_env_stores_token(monkeypatch):
|
||||
|
|
@ -93,13 +94,14 @@ def test_adobe_connect_env_stores_token(monkeypatch):
|
|||
assert status_resp.json()["adobe_label"] == "Paul Sandbox"
|
||||
|
||||
|
||||
def test_adobe_connect_env_fails_without_credentials(monkeypatch):
|
||||
"""GET /api/auth/adobe/connect with no .env tokens → 400."""
|
||||
def test_adobe_connect_requests_authorization_without_credentials(monkeypatch):
|
||||
"""GET /api/auth/adobe/connect with no .env tokens returns an auth URL."""
|
||||
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"]
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["authorization_required"] is True
|
||||
assert "/api/auth/adobe/callback" in resp.json()["authorization_url"]
|
||||
|
||||
|
||||
@respx.mock
|
||||
|
|
@ -126,6 +128,48 @@ def test_adobe_exchange_stores_token():
|
|||
assert status_resp.json()["adobe"] is True
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_adobe_callback_stores_session_tokens():
|
||||
"""GET /api/auth/adobe/callback stores tokens in this browser session."""
|
||||
respx.post(_ADOBE_TOKEN_URL).mock(
|
||||
return_value=httpx.Response(200, json={
|
||||
"access_token": "adobe-test-token",
|
||||
"refresh_token": "adobe-refresh",
|
||||
})
|
||||
)
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
cookie = create_test_session({"adobe_oauth_state": "expected-state"})
|
||||
with patch("web.routers.auth._fetch_adobe_profile", return_value={
|
||||
"adobe_user_name": "Paul Adobe",
|
||||
"adobe_user_email": "paul@example.com",
|
||||
"adobe_account_name": "Paul Sandbox",
|
||||
"adobe_account_id": "adobe-account-123",
|
||||
}):
|
||||
resp = client.get(
|
||||
"/api/auth/adobe/callback?code=authcode123&state=expected-state",
|
||||
cookies={"migrator_session": cookie},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
assert resp.status_code in (302, 307)
|
||||
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
|
||||
assert status_resp.json()["adobe_auth_mode"] == "session_oauth"
|
||||
|
||||
|
||||
def test_adobe_callback_requires_matching_state():
|
||||
cookie = create_test_session({"adobe_oauth_state": "expected-state"})
|
||||
resp = client.get(
|
||||
"/api/auth/adobe/callback?code=authcode123&state=wrong-state",
|
||||
cookies={"migrator_session": cookie},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert "invalid oauth state" in resp.json()["error"]
|
||||
|
||||
|
||||
def test_adobe_exchange_rejects_missing_code():
|
||||
"""POST /api/auth/adobe/exchange with no code in URL → 400."""
|
||||
resp = client.post(
|
||||
|
|
@ -274,7 +318,7 @@ def test_disconnect_clears_token():
|
|||
# Connect Adobe via exchange
|
||||
connect_resp = client.post(
|
||||
"/api/auth/adobe/exchange",
|
||||
json={"redirect_url": "https://localhost:8080/callback?code=abc"},
|
||||
json={"redirect_url": "http://localhost:8000/api/auth/adobe/callback?code=abc"},
|
||||
)
|
||||
session_cookie = connect_resp.cookies["migrator_session"]
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,9 @@ web/routers/auth.py
|
|||
-------------------
|
||||
OAuth endpoints for Adobe Sign and DocuSign.
|
||||
|
||||
Adobe Sign uses the same redirect URI as the CLI (https://localhost:8080/callback).
|
||||
Since nothing runs on that port, the browser lands on a failed page. The user copies
|
||||
the URL and submits it via POST /api/auth/adobe/exchange — identical to the CLI flow.
|
||||
|
||||
DocuSign uses a standard redirect callback handled directly by this server.
|
||||
|
||||
Tokens are stored in a server-side session keyed by a signed browser cookie.
|
||||
Both providers now support standard browser redirect callbacks handled directly by
|
||||
this server. Tokens are stored in a server-side session keyed by a signed browser
|
||||
cookie.
|
||||
"""
|
||||
|
||||
import secrets
|
||||
|
|
@ -35,12 +31,44 @@ from web.session import get_session, save_session, session_public_view
|
|||
|
||||
router = APIRouter()
|
||||
|
||||
# Adobe Sign registers https://localhost:8080/callback — same as the CLI script.
|
||||
_ADOBE_REDIRECT_URI = "https://localhost:8080/callback"
|
||||
_ADOBE_AUTH_URL = "https://secure.eu2.adobesign.com/public/oauth/v2"
|
||||
_ADOBE_TOKEN_URL = "https://api.eu2.adobesign.com/oauth/v2/token"
|
||||
|
||||
|
||||
def _adobe_redirect_uri() -> str:
|
||||
return settings.adobe_redirect_uri
|
||||
|
||||
|
||||
def _build_adobe_authorization_url(state: str) -> str:
|
||||
return (
|
||||
f"{_ADOBE_AUTH_URL}"
|
||||
f"?response_type=code"
|
||||
f"&client_id={settings.adobe_client_id}"
|
||||
f"&redirect_uri={_adobe_redirect_uri()}"
|
||||
f"&scope=library_read:self+library_write:self+user_read:self"
|
||||
f"&state={state}"
|
||||
)
|
||||
|
||||
|
||||
async def _refresh_adobe_session_token(refresh_token: str) -> dict:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
_ADOBE_TOKEN_URL,
|
||||
data={
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refresh_token,
|
||||
"client_id": settings.adobe_client_id,
|
||||
"client_secret": settings.adobe_client_secret,
|
||||
},
|
||||
)
|
||||
if not resp.is_success:
|
||||
raise RuntimeError(f"Adobe Sign token refresh failed ({resp.status_code})")
|
||||
token_data = resp.json()
|
||||
if "error" in token_data:
|
||||
raise RuntimeError(token_data.get("error_description", token_data["error"]))
|
||||
return token_data
|
||||
|
||||
|
||||
async def _fetch_adobe_profile(access_token: str) -> dict:
|
||||
"""
|
||||
Best-effort Adobe Sign profile lookup used only for nicer UI labels.
|
||||
|
|
@ -122,23 +150,18 @@ def auth_status(request: Request):
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Adobe Sign — manual paste flow (matches CLI behaviour)
|
||||
# Adobe Sign — OAuth Authorization Code Grant
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/adobe/url")
|
||||
def adobe_auth_url():
|
||||
"""
|
||||
Return the Adobe Sign authorization URL for the frontend to open in a new tab.
|
||||
The user authorizes, lands on a failed page (nothing runs on :8080), copies
|
||||
the URL, and submits it to POST /api/auth/adobe/exchange.
|
||||
"""
|
||||
params = (
|
||||
f"?response_type=code"
|
||||
f"&client_id={settings.adobe_client_id}"
|
||||
f"&redirect_uri={_ADOBE_REDIRECT_URI}"
|
||||
f"&scope=library_read:self+library_write:self+user_read:self"
|
||||
)
|
||||
return {"url": _ADOBE_AUTH_URL + params}
|
||||
def adobe_auth_url(request: Request):
|
||||
session = get_session(request)
|
||||
state = secrets.token_urlsafe(24)
|
||||
session["adobe_oauth_state"] = state
|
||||
session["adobe_auth_mode"] = "authorization_pending"
|
||||
response = JSONResponse({"url": _build_adobe_authorization_url(state)})
|
||||
save_session(response, session)
|
||||
return response
|
||||
|
||||
|
||||
class AdobeExchangeRequest(BaseModel):
|
||||
|
|
@ -176,7 +199,7 @@ async def adobe_exchange(body: AdobeExchangeRequest, request: Request):
|
|||
"grant_type": "authorization_code",
|
||||
"client_id": settings.adobe_client_id,
|
||||
"client_secret": settings.adobe_client_secret,
|
||||
"redirect_uri": _ADOBE_REDIRECT_URI,
|
||||
"redirect_uri": _adobe_redirect_uri(),
|
||||
"code": code,
|
||||
},
|
||||
)
|
||||
|
|
@ -206,34 +229,32 @@ async def adobe_exchange(body: AdobeExchangeRequest, request: Request):
|
|||
|
||||
|
||||
@router.get("/adobe/connect")
|
||||
async def adobe_connect_env(request: Request):
|
||||
async def adobe_connect(request: Request, force_oauth: bool = False):
|
||||
"""
|
||||
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.
|
||||
Obtain an Adobe Sign access token for this browser session.
|
||||
If session/env tokens are unavailable or force_oauth=true, return an
|
||||
authorization URL so the frontend can start a normal OAuth flow.
|
||||
"""
|
||||
session = get_session(request)
|
||||
token = session.get("adobe_access_token")
|
||||
refresh_token = session.get("adobe_refresh_token")
|
||||
|
||||
if not force_oauth and not token and not refresh_token:
|
||||
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)
|
||||
env_token = os.getenv("ADOBE_ACCESS_TOKEN")
|
||||
env_refresh_token = os.getenv("ADOBE_REFRESH_TOKEN")
|
||||
if env_token or env_refresh_token:
|
||||
token = env_token
|
||||
refresh_token = env_refresh_token
|
||||
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
|
||||
session["adobe_auth_mode"] = "shared_env"
|
||||
|
|
@ -244,11 +265,102 @@ async def adobe_connect_env(request: Request):
|
|||
"adobe_connected",
|
||||
{"auth_mode": "shared_env", "source": "server_env"},
|
||||
)
|
||||
|
||||
response = JSONResponse({"connected": True})
|
||||
save_session(response, session)
|
||||
return response
|
||||
|
||||
if not force_oauth and not token and refresh_token:
|
||||
try:
|
||||
token_data = await _refresh_adobe_session_token(refresh_token)
|
||||
except RuntimeError as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
token = token_data.get("access_token")
|
||||
session["adobe_access_token"] = token
|
||||
session["adobe_refresh_token"] = token_data.get("refresh_token", refresh_token)
|
||||
session["adobe_auth_mode"] = "session_oauth"
|
||||
session = await _merge_adobe_profile(session, token)
|
||||
log_event(
|
||||
request,
|
||||
session,
|
||||
"adobe_connected",
|
||||
{"auth_mode": "session_oauth", "source": "session_refresh"},
|
||||
)
|
||||
response = JSONResponse({"connected": True})
|
||||
save_session(response, session)
|
||||
return response
|
||||
|
||||
if not force_oauth and token:
|
||||
response = JSONResponse({"connected": True})
|
||||
save_session(response, session)
|
||||
return response
|
||||
|
||||
state = secrets.token_urlsafe(24)
|
||||
session["adobe_oauth_state"] = state
|
||||
session["adobe_auth_mode"] = "authorization_pending"
|
||||
authorization_url = _build_adobe_authorization_url(state)
|
||||
log_event(
|
||||
request,
|
||||
session,
|
||||
"adobe_authorization_requested",
|
||||
{"auth_mode": "authorization_pending"},
|
||||
)
|
||||
response = JSONResponse(
|
||||
{
|
||||
"connected": False,
|
||||
"authorization_required": True,
|
||||
"authorization_url": authorization_url,
|
||||
}
|
||||
)
|
||||
save_session(response, session)
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/adobe/callback")
|
||||
async def adobe_callback(request: Request, code: str = "", state: str = ""):
|
||||
"""Handle Adobe Sign OAuth redirect callback."""
|
||||
if not code:
|
||||
return JSONResponse({"error": "missing code"}, status_code=400)
|
||||
|
||||
session = get_session(request)
|
||||
expected_state = session.get("adobe_oauth_state")
|
||||
if not expected_state or state != expected_state:
|
||||
return JSONResponse({"error": "invalid oauth state"}, status_code=400)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
_ADOBE_TOKEN_URL,
|
||||
data={
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": settings.adobe_client_id,
|
||||
"client_secret": settings.adobe_client_secret,
|
||||
"redirect_uri": _adobe_redirect_uri(),
|
||||
"code": code,
|
||||
},
|
||||
)
|
||||
|
||||
if not resp.is_success:
|
||||
return JSONResponse({"error": "token exchange failed", "detail": resp.text}, status_code=502)
|
||||
|
||||
token_data = resp.json()
|
||||
if "error" in token_data:
|
||||
return JSONResponse({"error": token_data.get("error_description", token_data["error"])}, status_code=400)
|
||||
|
||||
session["adobe_access_token"] = token_data.get("access_token")
|
||||
session["adobe_refresh_token"] = token_data.get("refresh_token")
|
||||
session["adobe_auth_mode"] = "session_oauth"
|
||||
session.pop("adobe_oauth_state", None)
|
||||
session = await _merge_adobe_profile(session, session["adobe_access_token"])
|
||||
log_event(
|
||||
request,
|
||||
session,
|
||||
"adobe_connected",
|
||||
{"auth_mode": "session_oauth", "source": "browser_callback"},
|
||||
)
|
||||
|
||||
response = RedirectResponse("/#/settings")
|
||||
save_session(response, session)
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/adobe/disconnect")
|
||||
def adobe_disconnect(request: Request):
|
||||
|
|
@ -260,6 +372,7 @@ def adobe_disconnect(request: Request):
|
|||
session.pop("adobe_user_email", None)
|
||||
session.pop("adobe_account_name", None)
|
||||
session.pop("adobe_account_id", None)
|
||||
session.pop("adobe_oauth_state", None)
|
||||
session["adobe_auth_mode"] = "disconnected"
|
||||
log_event(
|
||||
request,
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ export const api = {
|
|||
status() {
|
||||
return GET('/api/auth/status');
|
||||
},
|
||||
connectAdobe() {
|
||||
return GET('/api/auth/adobe/connect');
|
||||
connectAdobe(forceOauth = false) {
|
||||
return GET(`/api/auth/adobe/connect${forceOauth ? '?force_oauth=true' : ''}`);
|
||||
},
|
||||
adobeUrl() {
|
||||
return GET('/api/auth/adobe/url');
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ async function onClickAdobe() {
|
|||
if (state.auth.adobe) {
|
||||
showAuthMenu('adobe', 'chip-adobe');
|
||||
} else {
|
||||
await connectAdobeEnv();
|
||||
await connectAdobe();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,30 +135,29 @@ export async function switchAccount(platform) {
|
|||
return;
|
||||
}
|
||||
await disconnectPlatform(platform, { silent: true, skipRefresh: true });
|
||||
showToast('Adobe Sign disconnected. Reconnect to continue.', 'info');
|
||||
await connectAdobeEnv();
|
||||
showToast('Starting a fresh Adobe Sign authorization…', 'info');
|
||||
await connectAdobe(true);
|
||||
}
|
||||
|
||||
async function connectAdobeEnv() {
|
||||
async function connectAdobe(forceOauth = false) {
|
||||
closeAuthMenu();
|
||||
setChipConnecting('chip-adobe');
|
||||
try {
|
||||
const data = await api.auth.connectAdobe();
|
||||
const data = await api.auth.connectAdobe(forceOauth);
|
||||
if (data.connected) {
|
||||
setState('auth', { ...state.auth, adobe: true });
|
||||
renderAuthChips();
|
||||
const { refreshTemplates } = await import('./templates.js');
|
||||
refreshTemplates();
|
||||
} else if (data.error && data.error.includes('No Adobe Sign credentials')) {
|
||||
renderAuthChips();
|
||||
showAdobeOAuthDialog();
|
||||
} else if (data.authorization_required && data.authorization_url) {
|
||||
window.location.href = data.authorization_url;
|
||||
} else {
|
||||
renderAuthChips();
|
||||
showToast('Adobe Sign error: ' + (data.error || 'unknown'), 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
renderAuthChips();
|
||||
showAdobeOAuthDialog();
|
||||
showToast('Adobe Sign connection failed: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -378,73 +377,6 @@ async function selectDocusignAccount(accountId, errorEl = null) {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Adobe OAuth dialog (manual redirect URL paste) ─────────────────────────
|
||||
|
||||
async function showAdobeOAuthDialog() {
|
||||
const { url } = await api.auth.adobeUrl().catch(() => ({ url: '#' }));
|
||||
|
||||
const existing = document.getElementById('adobe-auth-dialog');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const dialog = document.createElement('div');
|
||||
dialog.id = 'adobe-auth-dialog';
|
||||
dialog.innerHTML = `
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-box">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">Connect Adobe Sign</span>
|
||||
<button class="btn btn-ghost btn-icon" id="adobe-dialog-close">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ol style="padding-left:18px;line-height:1.8;margin-bottom:14px;font-size:13px">
|
||||
<li><a href="${escHtml(url)}" target="_blank" rel="noopener" style="color:var(--cobalt)">Click here to authorize in Adobe Sign ↗</a></li>
|
||||
<li>After authorizing, your browser will show a page that fails to load — that's expected.</li>
|
||||
<li>Copy the full URL from the address bar and paste it below.</li>
|
||||
</ol>
|
||||
<input type="text" id="adobe-redirect-input" class="form-input"
|
||||
placeholder="https://localhost:8080/callback?code=…" />
|
||||
<div id="adobe-dialog-error" style="color:var(--error);font-size:12px;min-height:18px;margin-top:6px"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" id="adobe-dialog-cancel">Cancel</button>
|
||||
<button class="btn btn-primary" id="adobe-dialog-submit">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
document.getElementById('adobe-dialog-close').onclick = () => dialog.remove();
|
||||
document.getElementById('adobe-dialog-cancel').onclick = () => dialog.remove();
|
||||
document.getElementById('adobe-dialog-submit').onclick = () => submitAdobeCode(dialog);
|
||||
document.getElementById('adobe-redirect-input').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') submitAdobeCode(dialog);
|
||||
});
|
||||
}
|
||||
|
||||
async function submitAdobeCode(dialog) {
|
||||
const url = document.getElementById('adobe-redirect-input').value.trim();
|
||||
if (!url) return;
|
||||
|
||||
const submitBtn = document.getElementById('adobe-dialog-submit');
|
||||
const errorEl = document.getElementById('adobe-dialog-error');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Connecting…';
|
||||
errorEl.textContent = '';
|
||||
|
||||
try {
|
||||
await api.auth.exchangeAdobe(url);
|
||||
dialog.remove();
|
||||
setState('auth', { ...state.auth, adobe: true });
|
||||
renderAuthChips();
|
||||
const { refreshTemplates } = await import('./templates.js');
|
||||
refreshTemplates();
|
||||
} catch (e) {
|
||||
errorEl.textContent = e.data?.error || e.message || 'Connection failed.';
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Connect';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Toast notification ─────────────────────────────────────────────────────
|
||||
|
||||
export function showToast(message, type = 'info') {
|
||||
|
|
|
|||
Loading…
Reference in New Issue