← back to Designer Wallcoverings

mailers/cc-api/oauth-setup.js

102 lines

#!/usr/bin/env node
/**
 * Constant Contact v3 — one-time OAuth setup helper.
 *
 * Confidential auth-code flow (uses the client secret; no PKCE needed).
 * Mints the long-lived CC_REFRESH_TOKEN that cc-client.js then auto-refreshes.
 *
 * Env required: CC_CLIENT_ID, CC_CLIENT_SECRET  (route via the `secrets` skill)
 * Env optional: CC_REDIRECT_URI  (default below — MUST match the URI registered
 *               in the Constant Contact app exactly)
 *
 * Usage:
 *   node oauth-setup.js authorize          # prints the URL Steve clicks
 *   node oauth-setup.js token "<code>"     # exchanges the ?code=... for tokens
 *   node oauth-setup.js token "<full redirected URL>"   # also accepts the whole URL
 */
const crypto = require('crypto');
const path = require('path');
// Auto-load the saved creds (CC_CLIENT_ID/SECRET) from the gitignored local .env.
try { require('dotenv').config({ path: path.join(__dirname, '.env') }); } catch { /* dotenv optional */ }

const AUTHORIZE_URL = 'https://authz.constantcontact.com/oauth2/default/v1/authorize';
const TOKEN_URL = 'https://authz.constantcontact.com/oauth2/default/v1/token';
const REDIRECT_URI = process.env.CC_REDIRECT_URI || 'https://localhost';
// contact_data = read/manage lists (set recipients via API); campaign_data = create/send; offline_access = refresh token.
const SCOPE = process.env.CC_SCOPE || 'contact_data campaign_data offline_access';

function need(name) {
  const v = process.env[name];
  if (!v) {
    console.error(`Missing env ${name}. Set CC_CLIENT_ID + CC_CLIENT_SECRET (route via /secrets), then re-run.`);
    process.exit(1);
  }
  return v;
}

function buildAuthorizeUrl() {
  const clientId = need('CC_CLIENT_ID');
  const state = crypto.randomBytes(8).toString('hex');
  const u = new URL(AUTHORIZE_URL);
  u.searchParams.set('client_id', clientId);
  u.searchParams.set('redirect_uri', REDIRECT_URI);
  u.searchParams.set('response_type', 'code');
  u.searchParams.set('scope', SCOPE);
  u.searchParams.set('state', state);
  return u.toString();
}

function extractCode(arg) {
  if (!arg) { console.error('Pass the ?code=... value (or the full redirected URL).'); process.exit(1); }
  if (arg.includes('code=')) {
    try { return new URL(arg).searchParams.get('code'); }
    catch { const m = arg.match(/code=([^&\s]+)/); return m && m[1]; }
  }
  return arg.trim();
}

async function exchangeCode(code) {
  const clientId = need('CC_CLIENT_ID');
  const clientSecret = need('CC_CLIENT_SECRET');
  const basic = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
  const body = new URLSearchParams({
    code,
    redirect_uri: REDIRECT_URI,
    grant_type: 'authorization_code',
  });
  console.error('POST', TOKEN_URL, '(authorization_code grant)');
  const res = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { Authorization: `Basic ${basic}`, 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  });
  const json = await res.json().catch(() => ({}));
  if (!res.ok) {
    console.error(`Token exchange failed (${res.status}):`, JSON.stringify(json));
    process.exit(1);
  }
  return json;
}

(async () => {
  const cmd = process.argv[2];
  if (cmd === 'authorize') {
    console.log('\n=== Open this URL in your browser, log into Constant Contact, and Allow: ===\n');
    console.log(buildAuthorizeUrl());
    console.log(`\nAfter approving you land on ${REDIRECT_URI}/?code=...&state=...`);
    console.log('(a "can\'t connect" page is fine — the code is in the address bar). Copy that and run:');
    console.log('  node oauth-setup.js token "<paste the code or the whole URL>"\n');
  } else if (cmd === 'token') {
    const code = extractCode(process.argv[3]);
    const tok = await exchangeCode(code);
    console.log('\n=== SUCCESS — tokens minted ===');
    console.log('access_token  (short-lived):', (tok.access_token || '').slice(0, 12) + '…');
    console.log('refresh_token (save this!)  :', tok.refresh_token);
    console.log('\nRoute this via the secrets skill so cc-client.js can auto-refresh:');
    console.log('  CC_REFRESH_TOKEN=' + tok.refresh_token);
  } else {
    console.error('Usage: node oauth-setup.js authorize | token "<code-or-url>"');
    process.exit(1);
  }
})();