← 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }
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=&quot;logo-fb&quot; style=&quot;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&quot;>'+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>