← back to Rentv 2026

public/admin/pr-intelligence/credentials.html

105 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Account Credentials — PR Intelligence</title>
<link rel="stylesheet" href="/admin/pr-intelligence/pr-shared.css">
<style>
  :root{--ink:#1a2230;--mut:#6b7686;--line:#e4e8ee;--brand:#0b6cff;--ok:#0a8a4a;--bad:#c0392b;--bg:#f6f8fb}
  *{box-sizing:border-box} body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:var(--ink);background:var(--bg);margin:0}
  .wrap{max-width:820px;margin:0 auto;padding:28px 20px 80px}
  h1{font-size:22px;margin:0 0 4px} .sub{color:var(--mut);margin:0 0 24px}
  .card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:22px 24px;margin:0 0 20px;box-shadow:0 1px 2px rgba(20,30,50,.04)}
  .card h2{font-size:17px;margin:0 0 2px;display:flex;align-items:center;gap:8px}
  .card .hint{color:var(--mut);font-size:13px;margin:0 0 18px}
  label{display:block;font-weight:600;font-size:13px;margin:14px 0 5px}
  input{width:100%;padding:10px 12px;border:1px solid var(--line);border-radius:8px;font:14px inherit;background:#fbfcfe}
  input:focus{outline:none;border-color:var(--brand);background:#fff}
  .row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
  .btn{appearance:none;border:0;border-radius:8px;padding:10px 18px;font:600 14px inherit;cursor:pointer}
  .btn.primary{background:var(--brand);color:#fff} .btn.ghost{background:#eef2f8;color:var(--ink)}
  .btns{display:flex;gap:10px;margin-top:20px;align-items:center}
  .status{margin-left:auto;font-weight:600;font-size:13px}
  .status.ok{color:var(--ok)} .status.bad{color:var(--bad)} .status.mut{color:var(--mut)}
  ol.steps{margin:0 0 6px;padding-left:20px;color:#3a4452;font-size:13.5px} ol.steps li{margin:7px 0}
  ol.steps a{color:var(--brand)} code{background:#eef2f8;padding:1px 6px;border-radius:5px;font-size:12.5px}
  .pill{font-size:11px;font-weight:700;padding:2px 9px;border-radius:20px;background:#eef2f8;color:var(--mut)}
  .pill.on{background:#e4f6ec;color:var(--ok)}
  details{border-top:1px solid var(--line);margin-top:16px;padding-top:12px} summary{cursor:pointer;font-weight:600;font-size:13px;color:var(--brand)}
</style>
</head>
<body>
<div class="wrap">
  <h1>Account Credentials</h1>
  <p class="sub">Connect your integrations. Everything you enter is saved securely under your account — <b id="who">your tenant</b>.</p>

  <div class="card">
    <h2>📧 Gmail integration <span class="pill" id="gmailPill">not connected</span></h2>
    <p class="hint">Sync your email so every message to/from a contact is recorded on their timeline (dates &amp; times of all correspondence). Read-only — we never send from your account without your action.</p>

    <details open>
      <summary>How to get these 4 values (about 3 minutes)</summary>
      <ol class="steps">
        <li>Go to <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener">Google Cloud Console → Credentials</a>. Create (or pick) a project.</li>
        <li><b>Enable the Gmail API:</b> APIs &amp; Services → Library → search “Gmail API” → Enable.</li>
        <li>Credentials → <b>Create Credentials → OAuth client ID</b> → type <b>Web application</b>. Under “Authorized redirect URIs” add <code>https://developers.google.com/oauthplayground</code>. Copy the <b>Client ID</b> and <b>Client Secret</b> below.</li>
        <li>Open the <a href="https://developers.google.com/oauthplayground" target="_blank" rel="noopener">OAuth 2.0 Playground</a> → gear ⚙ (top right) → check <b>“Use your own OAuth credentials”</b> → paste your Client ID + Secret.</li>
        <li>In the left list, under <b>Gmail API v1</b> select the scope <code>https://www.googleapis.com/auth/gmail.readonly</code> → <b>Authorize APIs</b> → sign in with the Gmail account you want to sync → <b>Exchange authorization code for tokens</b> → copy the <b>Refresh token</b> below.</li>
      </ol>
    </details>

    <label>Gmail address to sync</label>
    <input id="gmail_user" type="email" placeholder="steve.bloom@gmail.com" autocomplete="off">
    <div class="row">
      <div><label>Client ID</label><input id="client_id" placeholder="…apps.googleusercontent.com" autocomplete="off"></div>
      <div><label>Client Secret</label><input id="client_secret" type="password" placeholder="GOCSPX-…" autocomplete="off"></div>
    </div>
    <label>Refresh Token</label>
    <input id="refresh_token" type="password" placeholder="1//0g…" autocomplete="off">

    <div class="btns">
      <button class="btn primary" id="save">Save credentials</button>
      <button class="btn ghost" id="test">Test connection</button>
      <span class="status mut" id="msg"></span>
    </div>
  </div>

  <p class="sub" style="font-size:12.5px">Secrets are write-only from this page — once saved they are never shown again, only their connection status. To change them, re-enter and save.</p>
</div>

<script>
const $=id=>document.getElementById(id);
const J=(u,o={})=>fetch(u,{credentials:'same-origin',headers:{'Content-Type':'application/json'},...o}).then(r=>r.json());
function setPill(on){const p=$('gmailPill');p.textContent=on?'connected':'not connected';p.className='pill'+(on?' on':'');}
function msg(t,cls){const m=$('msg');m.textContent=t;m.className='status '+(cls||'mut');}

async function load(){
  try{
    const t=await J('/api/pr/tenant');
    if(t&&t.name) $('who').textContent=t.name;
    if(t&&t.gmail_user) $('gmail_user').value=t.gmail_user;
    setPill(!!(t&&t.gmail_configured));
  }catch(e){}
}
$('save').onclick=async()=>{
  const body={gmail_user:$('gmail_user').value,client_id:$('client_id').value,client_secret:$('client_secret').value,refresh_token:$('refresh_token').value};
  if(!body.gmail_user||!body.client_id||!body.client_secret||!body.refresh_token){msg('Fill in all four fields.','bad');return;}
  msg('Saving…','mut');
  try{const r=await J('/api/pr/tenant/gmail',{method:'POST',body:JSON.stringify(body)});
    if(r.ok){setPill(true);$('client_secret').value='';$('refresh_token').value='';msg('Saved ✓ — now test the connection.','ok');}
    else msg(r.error||'Save failed','bad');
  }catch(e){msg('Save failed: '+e.message,'bad');}
};
$('test').onclick=async()=>{
  msg('Testing…','mut');
  try{const r=await J('/api/pr/tenant/gmail/test',{method:'POST'});
    if(r.ok&&r.connected){setPill(true);msg('Connected ✓ '+(r.gmail_user||''),'ok');}
    else msg('Not connected: '+(r.error||'unknown'),'bad');
  }catch(e){msg('Test failed: '+e.message,'bad');}
};
load();
</script>
</body>
</html>