adobe-to-docusign-migrator/web/static/js/auth.js

306 lines
11 KiB
JavaScript

// Auth: connect/disconnect Adobe Sign and Docusign, auth status chips
import { api } from './api.js';
import { state, setState } from './state.js';
import { escHtml } from './utils.js';
// ── Refresh auth state and update chips ────────────────────────────────────
export async function refreshAuth() {
try {
const data = await api.auth.status();
setState('auth', {
adobe: !!data.adobe,
docusign: !!data.docusign,
adobeLabel: data.adobe_label || 'Adobe Sign',
docusignLabel: data.docusign_label || 'Docusign',
});
} catch (e) {
console.warn('Auth status failed:', e.message);
}
renderAuthChips();
}
// ── Render connection pills in top bar ─────────────────────────────────────
export function renderAuthChips() {
renderChip('chip-adobe', state.auth.adobe, 'Adobe Sign', onClickAdobe);
renderChip('chip-docusign', state.auth.docusign, 'Docusign', onClickDocusign);
}
function renderChip(id, connected, label, onClick) {
const el = document.getElementById(id);
if (!el) return;
el.className = 'conn-pill ' + (connected ? 'connected' : 'disconnected');
el.innerHTML = `<span class="conn-dot"></span><span class="conn-pill-label">${escHtml(label)}</span>${connected ? '<span class="conn-caret">▾</span>' : ''}`;
el.onclick = onClick;
}
// ── Click handlers ─────────────────────────────────────────────────────────
async function onClickAdobe() {
if (state.auth.adobe) {
showAuthMenu('adobe', 'chip-adobe');
} else {
await connectAdobeEnv();
}
}
async function onClickDocusign() {
if (state.auth.docusign) {
showAuthMenu('docusign', 'chip-docusign');
} else {
await connectDocusign();
}
}
export async function disconnectPlatform(platform, opts = {}) {
const { silent = false, skipRefresh = false } = opts;
setChipConnecting(platform === 'adobe' ? 'chip-adobe' : 'chip-docusign');
try {
await api.auth.disconnect(platform);
setState('auth', { ...state.auth, [platform]: false });
renderAuthChips();
if (!skipRefresh) {
const { refreshTemplates } = await import('./templates.js');
refreshTemplates();
}
if (!silent) {
showToast(`${platform === 'adobe' ? 'Adobe Sign' : 'Docusign'} disconnected.`, 'info');
}
} catch (e) {
console.error('Disconnect failed:', e.message);
renderAuthChips();
if (!silent) {
showToast(`Disconnect failed: ${e.message}`, 'error');
}
}
}
export async function switchAccount(platform) {
closeAuthMenu();
await disconnectPlatform(platform, { silent: true, skipRefresh: true });
if (platform === 'docusign') {
showToast('Starting a fresh Docusign authorization. If Docusign signs you in automatically, sign out there and try again to choose a different account.', 'info');
window.location.href = '/api/auth/docusign/start';
return;
}
if (platform === 'adobe') {
showToast('Adobe Sign disconnected. Reconnect to continue.', 'info');
await connectAdobeEnv();
}
}
async function connectAdobeEnv() {
closeAuthMenu();
setChipConnecting('chip-adobe');
try {
const data = await api.auth.connectAdobe();
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 {
renderAuthChips();
showToast('Adobe Sign error: ' + (data.error || 'unknown'), 'error');
}
} catch (e) {
renderAuthChips();
showAdobeOAuthDialog();
}
}
async function connectDocusign() {
closeAuthMenu();
setChipConnecting('chip-docusign');
try {
const data = await api.auth.connectDocusign();
if (data.connected) {
setState('auth', { ...state.auth, docusign: true });
renderAuthChips();
const { refreshTemplates } = await import('./templates.js');
refreshTemplates();
} else if (data.authorization_required && data.authorization_url) {
window.location.href = data.authorization_url;
} else {
renderAuthChips();
showToast('Docusign error: ' + (data.error || 'unknown'), 'error');
}
} catch (e) {
renderAuthChips();
showToast('Docusign connection failed: ' + e.message, 'error');
}
}
function setChipConnecting(id) {
const el = document.getElementById(id);
if (!el) return;
el.className = 'conn-pill connecting';
el.innerHTML = `<span class="conn-dot"></span><span class="spinner spinner-sm"></span>`;
}
function closeAuthMenu() {
document.getElementById('auth-chip-menu')?.remove();
document.removeEventListener('click', onDocumentClickCloseMenu, true);
document.removeEventListener('keydown', onEscapeCloseMenu, true);
}
function onDocumentClickCloseMenu(event) {
const menu = document.getElementById('auth-chip-menu');
if (!menu) return;
if (menu.contains(event.target)) return;
if (event.target.closest('.conn-pill')) return;
closeAuthMenu();
}
function onEscapeCloseMenu(event) {
if (event.key === 'Escape') closeAuthMenu();
}
function showAuthMenu(platform, anchorId) {
const anchor = document.getElementById(anchorId);
if (!anchor) return;
const existing = document.getElementById('auth-chip-menu');
if (existing && existing.dataset.platform === platform && existing.dataset.anchorId === anchorId) {
closeAuthMenu();
return;
}
closeAuthMenu();
const rect = anchor.getBoundingClientRect();
const menu = document.createElement('div');
menu.id = 'auth-chip-menu';
menu.dataset.platform = platform;
menu.dataset.anchorId = anchorId;
menu.className = 'auth-chip-menu';
menu.style.top = `${rect.bottom + 8}px`;
menu.style.left = `${Math.max(12, rect.right - 220)}px`;
const accountLabel = platform === 'docusign' ? 'Docusign' : 'Adobe Sign';
const switchLabel = platform === 'docusign' ? 'Switch Account' : 'Reconnect';
const switchHelp = platform === 'docusign'
? 'Clear this browser session and start a fresh login flow.'
: 'Disconnect and reconnect Adobe Sign.';
menu.innerHTML = `
<div class="auth-chip-menu-title">${escHtml(accountLabel)}</div>
<button class="auth-chip-menu-item" data-action="disconnect">
<span class="auth-chip-menu-label">Disconnect</span>
<span class="auth-chip-menu-help">Clear this app session.</span>
</button>
<button class="auth-chip-menu-item" data-action="switch">
<span class="auth-chip-menu-label">${escHtml(switchLabel)}</span>
<span class="auth-chip-menu-help">${escHtml(switchHelp)}</span>
</button>
`;
menu.querySelector('[data-action="disconnect"]')?.addEventListener('click', async () => {
closeAuthMenu();
await disconnectPlatform(platform);
});
menu.querySelector('[data-action="switch"]')?.addEventListener('click', async () => {
await switchAccount(platform);
});
document.body.appendChild(menu);
document.addEventListener('click', onDocumentClickCloseMenu, true);
document.addEventListener('keydown', onEscapeCloseMenu, true);
}
// ── 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 {
const data = 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') {
let container = document.getElementById('toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'toast-container';
container.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px;';
document.body.appendChild(container);
}
const toast = document.createElement('div');
const colors = { info: 'var(--cobalt-light)', error: 'var(--error-bg)', success: 'var(--success-bg)' };
const borders = { info: 'var(--cobalt)', error: 'var(--error)', success: 'var(--success)' };
toast.style.cssText = `
padding:10px 16px;border-radius:6px;font-size:13px;font-weight:500;
background:${colors[type]||colors.info};border:1px solid ${borders[type]||borders.info};
box-shadow:var(--shadow-md);max-width:360px;animation:fadeIn 0.2s ease;
`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => toast.remove(), 4000);
}