406 lines
15 KiB
JavaScript
406 lines
15 KiB
JavaScript
// Auth: connect/disconnect Adobe Sign and Docusign, account picker, auth chips
|
|
|
|
import { api } from './api.js';
|
|
import { state, setState } from './state.js';
|
|
import { escHtml, initials } 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',
|
|
adobeAccountId: data.adobe_account_id || null,
|
|
adobeAccountName: data.adobe_account_name || null,
|
|
docusignLabel: data.docusign_label || 'Docusign',
|
|
docusignAccountId: data.docusign_account_id || null,
|
|
docusignAccountName: data.docusign_account_name || null,
|
|
docusignAccountsCount: data.docusign_accounts_count || 0,
|
|
docusignAccountSelectionRequired: !!data.docusign_account_selection_required,
|
|
isAdmin: !!data.is_admin,
|
|
});
|
|
} catch (e) {
|
|
console.warn('Auth status failed:', e.message);
|
|
}
|
|
renderAuthChips();
|
|
if (state.auth.docusign && state.auth.docusignAccountSelectionRequired) {
|
|
showDocusignAccountPicker();
|
|
}
|
|
}
|
|
|
|
// ── Render connection pills in top bar ─────────────────────────────────────
|
|
|
|
export function renderAuthChips() {
|
|
renderChip(
|
|
'chip-adobe',
|
|
state.auth.adobe,
|
|
state.auth.adobe ? `Adobe: ${state.auth.adobeAccountName || state.auth.adobeLabel || 'Connected'}` : 'Adobe Sign',
|
|
onClickAdobe
|
|
);
|
|
renderChip(
|
|
'chip-docusign',
|
|
state.auth.docusign,
|
|
state.auth.docusign ? `Docusign: ${state.auth.docusignAccountName || state.auth.docusignLabel || 'Connected'}` : 'Docusign',
|
|
onClickDocusign
|
|
);
|
|
renderAvatar();
|
|
}
|
|
|
|
function renderAvatar() {
|
|
const el = document.getElementById('topbar-avatar');
|
|
if (!el) return;
|
|
|
|
const name = state.auth.docusignLabel && state.auth.docusignLabel !== 'Docusign'
|
|
? state.auth.docusignLabel
|
|
: state.auth.docusignAccountName || '';
|
|
|
|
el.textContent = name ? initials(name) : '?';
|
|
el.title = name || 'User';
|
|
el.setAttribute('aria-label', name ? `User ${name}` : 'User');
|
|
}
|
|
|
|
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 connectAdobe();
|
|
}
|
|
}
|
|
|
|
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;
|
|
closeAuthMenu();
|
|
closeDocusignAccountPicker();
|
|
setChipConnecting(platform === 'adobe' ? 'chip-adobe' : 'chip-docusign');
|
|
try {
|
|
await api.auth.disconnect(platform);
|
|
if (platform === 'docusign') {
|
|
setState('auth', {
|
|
...state.auth,
|
|
docusign: false,
|
|
docusignAccountId: null,
|
|
docusignAccountName: null,
|
|
docusignAccountsCount: 0,
|
|
docusignAccountSelectionRequired: false,
|
|
});
|
|
} else {
|
|
setState('auth', {
|
|
...state.auth,
|
|
adobe: false,
|
|
adobeAccountId: null,
|
|
adobeAccountName: null,
|
|
adobeLabel: 'Adobe Sign',
|
|
});
|
|
}
|
|
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();
|
|
if (platform === 'docusign') {
|
|
await showDocusignAccountPicker({ forceRefresh: true });
|
|
return;
|
|
}
|
|
await disconnectPlatform(platform, { silent: true, skipRefresh: true });
|
|
showToast('Starting a fresh Adobe Sign authorization…', 'info');
|
|
await connectAdobe(true);
|
|
}
|
|
|
|
async function connectAdobe(forceOauth = false) {
|
|
closeAuthMenu();
|
|
setChipConnecting('chip-adobe');
|
|
try {
|
|
const data = await api.auth.connectAdobe(forceOauth, window.location.hash || '#/templates');
|
|
if (data.connected) {
|
|
setState('auth', { ...state.auth, adobe: 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('Adobe Sign error: ' + (data.error || 'unknown'), 'error');
|
|
}
|
|
} catch (e) {
|
|
renderAuthChips();
|
|
showToast('Adobe Sign connection failed: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function connectDocusign() {
|
|
closeAuthMenu();
|
|
setChipConnecting('chip-docusign');
|
|
try {
|
|
const data = await api.auth.connectDocusign(window.location.hash || '#/templates');
|
|
if (data.connected) {
|
|
await refreshAuth();
|
|
if (!data.account_selection_required) {
|
|
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>`;
|
|
}
|
|
|
|
// ── Top-bar menu ───────────────────────────────────────────────────────────
|
|
|
|
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'
|
|
? 'Pick a different DocuSign account from your account list.'
|
|
: '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);
|
|
}
|
|
|
|
// ── DocuSign account picker ────────────────────────────────────────────────
|
|
|
|
export async function showDocusignAccountPicker(opts = {}) {
|
|
const { forceRefresh = false } = opts;
|
|
if (!forceRefresh && document.getElementById('docusign-account-dialog')) return;
|
|
|
|
let data;
|
|
try {
|
|
data = await api.auth.docusignAccounts();
|
|
} catch (e) {
|
|
showToast('Failed to load DocuSign accounts: ' + e.message, 'error');
|
|
return;
|
|
}
|
|
|
|
const accounts = [...(data.accounts || [])].sort((a, b) => {
|
|
const nameCmp = (a.account_name || '').localeCompare(b.account_name || '', undefined, { sensitivity: 'base' });
|
|
return nameCmp || (a.account_id || '').localeCompare(b.account_id || '', undefined, { sensitivity: 'base' });
|
|
});
|
|
|
|
if (!accounts.length) {
|
|
showToast('No DocuSign accounts were returned for this user.', 'error');
|
|
return;
|
|
}
|
|
|
|
if (accounts.length === 1) {
|
|
await selectDocusignAccount(accounts[0].account_id);
|
|
return;
|
|
}
|
|
|
|
closeDocusignAccountPicker();
|
|
const dialog = document.createElement('div');
|
|
dialog.id = 'docusign-account-dialog';
|
|
dialog.innerHTML = `
|
|
<div class="modal-backdrop"></div>
|
|
<div class="modal-box modal-box-wide">
|
|
<div class="modal-header">
|
|
<span class="modal-title">Choose DocuSign Account</span>
|
|
<button class="btn btn-ghost btn-icon" id="docusign-account-close">✕</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div style="display:flex;gap:12px;align-items:center;justify-content:space-between;margin-bottom:14px;flex-wrap:wrap">
|
|
<div style="font-size:13px;color:var(--text-muted)">
|
|
${accounts.length} account${accounts.length === 1 ? '' : 's'} found. Choose the account this session should use.
|
|
</div>
|
|
<input type="text" id="docusign-account-search" class="form-input" placeholder="Search accounts..." style="max-width:320px" />
|
|
</div>
|
|
<div id="docusign-account-error" style="color:var(--error);font-size:12px;min-height:18px;margin-bottom:8px"></div>
|
|
<div id="docusign-account-list" class="docusign-account-list"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" id="docusign-account-cancel">Close</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(dialog);
|
|
|
|
const listEl = document.getElementById('docusign-account-list');
|
|
const searchEl = document.getElementById('docusign-account-search');
|
|
const errorEl = document.getElementById('docusign-account-error');
|
|
|
|
const renderList = () => {
|
|
const query = (searchEl?.value || '').trim().toLowerCase();
|
|
const filtered = accounts.filter(acc => {
|
|
const haystack = `${acc.account_name || ''} ${acc.account_id || ''} ${acc.organization_name || ''}`.toLowerCase();
|
|
return !query || haystack.includes(query);
|
|
});
|
|
|
|
if (!filtered.length) {
|
|
listEl.innerHTML = `<div class="empty-state" style="padding:24px 12px"><div class="empty-state-title">No matching accounts</div></div>`;
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = filtered.map(acc => `
|
|
<button class="docusign-account-item ${data.selected_account_id === acc.account_id ? 'selected' : ''}" data-account-id="${escHtml(acc.account_id)}">
|
|
<span class="docusign-account-name">${escHtml(acc.account_name || acc.account_id)}</span>
|
|
<span class="docusign-account-meta mono">${escHtml(acc.account_id)}</span>
|
|
<span class="docusign-account-meta">${escHtml(acc.organization_name || '')}</span>
|
|
</button>
|
|
`).join('');
|
|
|
|
listEl.querySelectorAll('.docusign-account-item').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
errorEl.textContent = '';
|
|
await selectDocusignAccount(btn.dataset.accountId, errorEl);
|
|
});
|
|
});
|
|
};
|
|
|
|
searchEl?.addEventListener('input', renderList);
|
|
document.getElementById('docusign-account-close')?.addEventListener('click', closeDocusignAccountPicker);
|
|
document.getElementById('docusign-account-cancel')?.addEventListener('click', closeDocusignAccountPicker);
|
|
renderList();
|
|
}
|
|
|
|
function closeDocusignAccountPicker() {
|
|
document.getElementById('docusign-account-dialog')?.remove();
|
|
}
|
|
|
|
async function selectDocusignAccount(accountId, errorEl = null) {
|
|
try {
|
|
await api.auth.selectDocusignAccount(accountId);
|
|
closeDocusignAccountPicker();
|
|
await refreshAuth();
|
|
setState('templatesError', null);
|
|
const { refreshTemplates, renderTemplates } = await import('./templates.js');
|
|
await refreshTemplates();
|
|
if ((window.location.hash || '#/templates').startsWith('#/templates')) {
|
|
await renderTemplates();
|
|
}
|
|
showToast('DocuSign account selected.', 'success');
|
|
} catch (e) {
|
|
if (errorEl) {
|
|
errorEl.textContent = e.data?.error || e.message || 'Failed to select account.';
|
|
} else {
|
|
showToast('Failed to select account: ' + e.message, 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 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:420px;animation:fadeIn 0.2s ease;
|
|
`;
|
|
toast.textContent = message;
|
|
container.appendChild(toast);
|
|
setTimeout(() => toast.remove(), 4500);
|
|
}
|