← back to Ventura Claw
server/public/oauth-setup.html
179 lines
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>OAuth Setup — VenturaClaw</title>
<link rel="stylesheet" href="/static/style.css" />
<style>
.setup-page { max-width: 1080px; margin: 0 auto; padding: 28px; }
.setup-head h1 { font-family: var(--serif); font-weight: 500; font-size: 40px; letter-spacing: -.01em; line-height: 1; margin: 0 0 8px; }
.setup-head h1 em { font-style: italic; color: var(--gold); }
.setup-head .sub { font-family: var(--mono); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-mute); margin-bottom: 24px; }
.setup-lede { font-family: var(--serif); font-style: italic; font-size: 19px; color: var(--ink-soft); margin-bottom: 28px; line-height: 1.5; max-width: 70ch; }
.contact { background: var(--gold-soft); border: 1px solid var(--gold); border-radius: 4px; padding: 12px 18px; margin-bottom: 28px; font-family: var(--mono); font-size: 11px; letter-spacing: .08em; color: var(--ink); display: flex; gap: 14px; align-items: center; }
.contact b { color: var(--gold); }
.provider-card { background: var(--bg-elevated); border: 1px solid var(--rule-strong); border-radius: 4px; padding: 24px 28px; margin-bottom: 18px; }
.provider-card.connected { border-left: 3px solid var(--good); }
.provider-card .head { display: flex; align-items: center; gap: 16px; margin-bottom: 14px; }
.provider-card .lt { width: 44px; height: 44px; border-radius: 6px; background: rgba(244,241,234,0.92); padding: 6px; display: flex; align-items: center; justify-content: center; flex: 0 0 44px; }
.provider-card .lt img { width: 100%; height: 100%; object-fit: contain; }
.provider-card .name { font-family: var(--serif); font-weight: 500; font-size: 24px; letter-spacing: -.005em; }
.provider-card .status { font-family: var(--mono); font-size: 9px; letter-spacing: .16em; text-transform: uppercase; padding: 4px 10px; border-radius: 2px; }
.provider-card .status.unset { color: var(--ink-mute); border: 1px solid var(--rule); }
.provider-card .status.set { color: var(--good); border: 1px solid var(--good); background: rgba(143,184,154,0.10); }
.provider-card .note { font-family: var(--mono); font-size: 10px; letter-spacing: .04em; color: var(--ink-soft); line-height: 1.6; margin-bottom: 14px; padding: 12px 14px; background: rgba(244,241,234,0.04); border-left: 2px solid var(--gold); border-radius: 2px; }
.steps { display: grid; grid-template-columns: auto 1fr auto; gap: 10px 14px; align-items: center; margin-bottom: 16px; font-size: 13px; color: var(--ink-soft); }
.steps .num { font-family: var(--serif); font-style: italic; font-size: 22px; color: var(--gold); line-height: 1; }
.steps .lbl b { color: var(--ink); font-weight: 500; }
.steps code { font-family: var(--mono); font-size: 11px; background: rgba(0,0,0,0.4); color: var(--gold); padding: 2px 8px; border-radius: 2px; word-break: break-all; }
.steps .copy { font-family: var(--mono); font-size: 9px; padding: 4px 10px; background: transparent; border: 1px solid var(--rule); color: var(--ink-soft); cursor: pointer; border-radius: 2px; transition: all 140ms ease; }
.steps .copy:hover { border-color: var(--gold); color: var(--gold); }
.cred-form { display: grid; grid-template-columns: 1fr 1fr auto; gap: 10px; margin-top: 14px; }
.cred-form input { font-family: var(--mono); font-size: 12px; }
.cred-form input::placeholder { color: var(--ink-faint); font-family: var(--mono); }
@media (max-width: 700px) { .cred-form { grid-template-columns: 1fr; } }
</style></head><body>
<header class="topbar">
<div class="brand">
<div class="logo-dot"></div>
<div>
<div class="brand-name">Ventura<em style="font-style:italic;color:var(--gold)">Claw</em></div>
<div class="brand-sub">OAuth Setup · Admin</div>
</div>
</div>
<div style="display:flex;gap:10px;align-items:center">
<a href="/admin" class="btn btn-ghost">Admin</a>
<a href="/admin/connectors" class="btn btn-ghost">Connectors</a>
<a href="/chat" class="btn btn-ghost">Chat</a>
<a href="#" id="logout" class="btn btn-ghost">Sign out</a>
</div>
</header>
<main class="setup-page">
<div class="setup-head">
<h1>OAuth <em>setup.</em></h1>
<div class="sub">Register vendor apps · paste credentials · users connect with one click</div>
</div>
<p class="setup-lede">Each vendor below needs a one-time <em style="color:var(--gold)">OAuth app registration</em> in their developer portal. Use <code style="font-family:var(--mono);font-size:14px;color:var(--gold)">info@venturaclaw.com</code> as the contact email everywhere. After registering, paste the Client ID + Client Secret here. Users then connect with a single button click.</p>
<div class="contact">
<b>Default contact email →</b>
<code style="font-family:var(--mono);font-size:13px;color:var(--ink)">info@venturaclaw.com</code>
<button onclick="navigator.clipboard.writeText('info@venturaclaw.com')" class="copy" style="font-family:var(--mono);font-size:9px;padding:4px 10px;background:transparent;border:1px solid var(--rule);color:var(--ink-soft);cursor:pointer;border-radius:2px">Copy</button>
</div>
<div id="providers" role="region" aria-label="OAuth providers" aria-live="polite"></div>
</main>
<script>
function esc(s) { return String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
function escAttr(s) { return esc(s); } // for HTML attribute context
async function loadProviders() {
const r = await (await fetch('/api/admin/oauth/providers')).json();
const host = document.getElementById('providers');
host.innerHTML = r.providers.map(p => `
<div class="provider-card ${p.has_credentials ? 'connected' : ''}">
<div class="head">
<div class="lt"><img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/${escAttr(p.icon)}.svg" alt="${escAttr(p.name)}" data-tint="${escAttr(p.tint)}" data-letter="${escAttr(p.name[0] || '?')}" onerror="this.outerHTML='<div class="logo-fb" style="background:'+this.dataset.tint+';color:white;width:100%;height:100%;display:flex;align-items:center;justify-content:center;border-radius:4px;font-family:var(--serif);font-style:italic;font-weight:500">'+this.dataset.letter+'</div>'"></div>
<div style="flex:1">
<div class="name">${esc(p.name)}</div>
${p.registered_at ? `<div style="font-family:var(--mono);font-size:9px;letter-spacing:.10em;color:var(--ink-mute);text-transform:uppercase;margin-top:2px">Registered ${esc(new Date(p.registered_at).toLocaleDateString())}</div>` : ''}
</div>
<span class="status ${p.has_credentials ? 'set' : 'unset'}">${p.has_credentials ? '✓ Live' : 'Not set up'}</span>
</div>
<div class="note">${esc(p.note)}</div>
<div class="steps">
<span class="num">1</span>
<span class="lbl"><b>Open</b> ${esc(p.name)} developer portal as <code>info@venturaclaw.com</code></span>
<span style="display:flex;gap:6px">
<a href="${escAttr(p.register_url)}" target="_blank" rel="noopener" class="btn btn-primary" style="font-family:var(--mono);font-size:9px;padding:6px 14px">Open ↗</a>
<button data-vendor="${escAttr(p.id)}" class="btn launch-bb" style="font-family:var(--mono);font-size:9px;padding:6px 14px;border-color:var(--gold);color:var(--gold)" title="Cloud browser — isolated, no cookie pollution, auto-expires in 1hr">☁ Cloud →</button>
</span>
<span class="num">2</span>
<span class="lbl"><b>Set redirect URI:</b> <code id="ru-${escAttr(p.id)}">${esc(p.redirect_uri)}</code></span>
<button class="copy copy-redirect" data-redirect="${escAttr(p.redirect_uri)}">Copy</button>
<span class="num">3</span>
<span class="lbl"><b>Paste</b> the Client ID + Client Secret here</span>
<span></span>
</div>
<form class="cred-form" data-vendor="${escAttr(p.id)}" aria-label="OAuth credentials for ${escAttr(p.name)}">
<input type="text" name="client_id" placeholder="Client ID${p.has_credentials ? ' (already set — overwrite to change)' : ''}" autocomplete="off">
<input type="password" name="client_secret" placeholder="Client Secret${p.has_credentials ? ' (already set)' : ''}" autocomplete="off">
<button type="submit" class="btn btn-primary" style="font-family:var(--mono);font-size:10px;letter-spacing:.16em">${p.has_credentials ? 'Update' : 'Save'}</button>
<span class="cred-result" role="status" aria-live="polite" style="grid-column:1/-1;font-family:var(--mono);font-size:10px;color:var(--ink-soft);min-height:14px"></span>
</form>
${p.has_credentials ? `<div style="margin-top:10px;font-family:var(--mono);font-size:9px;letter-spacing:.10em;text-transform:uppercase;color:var(--ink-mute)">Users can now: <a href="/oauth/${escAttr(p.id)}/connect" style="color:var(--gold)">Connect ${esc(p.name)} →</a></div>` : ''}
</div>
`).join('');
// Wire Cloud-launch + Copy buttons (delegated, since markup is dynamically generated)
host.querySelectorAll('.launch-bb').forEach(b => b.addEventListener('click', () => launchBrowserbase(b.dataset.vendor, b)));
host.querySelectorAll('.copy-redirect').forEach(b => b.addEventListener('click', () => {
navigator.clipboard.writeText(b.dataset.redirect);
b.textContent = 'copied!'; setTimeout(() => { b.textContent = 'Copy'; }, 1200);
}));
document.querySelectorAll('.cred-form').forEach(f => {
f.addEventListener('submit', async e => {
e.preventDefault();
const vendor = f.dataset.vendor;
const data = Object.fromEntries(new FormData(f));
const result = f.querySelector('.cred-result');
const setMsg = (m, ok) => { result.textContent = m; result.style.color = ok ? 'var(--good)' : 'var(--bad)'; };
if (!data.client_id || !data.client_secret) { setMsg('Both fields required', false); return; }
setMsg('Saving…', true);
try {
const r = await fetch(`/api/admin/oauth/${vendor}/credentials`, {
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(data),
});
const d = await r.json();
if (r.ok) { setMsg('✓ Saved', true); setTimeout(loadProviders, 600); }
else setMsg('Save failed: ' + (d.error || 'unknown'), false);
} catch (err) { setMsg('Network error — check your connection and retry', false); }
});
});
}
async function launchBrowserbase(vendor, btnEl) {
const orig = btnEl.textContent;
btnEl.disabled = true; btnEl.textContent = '☁ launching…';
try {
const r = await fetch(`/api/admin/oauth/${vendor}/sandbox`, { method: 'POST' });
const d = await r.json();
if (!r.ok || !d.live_view) {
btnEl.textContent = '✗ ' + (d.error || 'no live view').slice(0, 40);
setTimeout(() => { btnEl.textContent = orig; btnEl.disabled = false; }, 3500);
return;
}
try { await navigator.clipboard.writeText(d.register_url); } catch {}
const cloudWindow = window.open(d.live_view, '_blank', 'noopener');
if (cloudWindow && d.session_id) {
const watcher = setInterval(() => {
if (cloudWindow.closed) {
clearInterval(watcher);
fetch(`/api/admin/browserbase/release/${d.session_id}`, { method: 'POST' }).catch(() => {});
}
}, 5000);
}
btnEl.textContent = '☁ launched · URL copied';
setTimeout(() => { btnEl.textContent = orig; btnEl.disabled = false; }, 3500);
} catch (e) {
btnEl.textContent = '✗ network error';
setTimeout(() => { btnEl.textContent = orig; btnEl.disabled = false; }, 3500);
}
}
loadProviders();
document.getElementById('logout').addEventListener('click', async e => {
e.preventDefault(); await fetch('/api/auth/logout', { method: 'POST' }); location.href = '/login';
});
</script>
</body></html>