Steps 0–10 of the v2 plan, 194 tests passing. Core infrastructure - Shared Redis client (src/redis.ts); all four Redis consumers migrated - Vitest test harness with vitest.config.ts and npm test/test:watch scripts Billing & invoicing (Steps 1–2) - Monthly invoice generation with idempotency (MySQL uq_customer_period unique key) - Cron job with Redis distributed lock (Lua compare-delete, 1-hr TTL) - Invoice emailer via nodemailer (FETCHERPAY SMTP) - Billing middleware: checkLimit gate in handleToolCall; platform attribution fix Email multi-tenancy (Step 3) - EmailCtx = Account | EmailCredentials; imap.ts + smtp.ts accept both - resolveEmailCtx helper in tools.ts; all email tools use customer credentials Analytics + platform health (Steps 4–5) - Chart.js bar charts for platform breakdown and daily activity - Token expiry check in getCredential with dynamic import refresh - platform-health.ts: per-platform health probe with 10-min Redis cache - GET /api/health/platforms; "Token expired" amber badge in dashboard Tool schema filtering (Step 6) - stripAccountParam deep-clones tool schemas; multi-tenant sessions never see the internal account enum OAuth hardening (Step 7) - Atomic auth code consumption: UPDATE SET used=TRUE, check affectedRows - customer_id threaded through oauth_auth_codes → oauth_tokens - getTokenCustomer(); requireAuth resolves req.customer from Bearer token - Consent page requires authenticated session; redirect_uri validated against registered URIs; http://localhost:* loopback wildcard DCR browser flow (Step 8) - ensureOAuthAppRegistered() upserts pre-registered SquareMCP OAuth app on startup with redirect URIs for mcp-callback, localhost:*, claude-desktop, opencode - GET /oauth/connect-mcp → server-side redirect (client_id off frontend) - GET /oauth/mcp-callback → exchanges code, renders config snippet page with copy buttons for Claude Desktop and Codex CLI Webhooks (Step 9) - webhook_url + webhook_secret columns on customers - deliverWebhook(): HMAC-SHA256 signing, 3× exponential retry (1s/4s/16s), Redis DLQ with 7-day TTL on total failure - isValidWebhookUrl(): SSRF protection (blocks RFC-1918, localhost, .local) - POST /api/webhooks/config (secret returned once), GET, DELETE - GET /api/admin/webhooks/dlq/:customerId - WhatsApp POST route uses express.raw() for raw body preservation - Dashboard Webhooks tab with secret-once display and copy button Developer docs (Step 10) - docs/ static HTML site (GitHub Pages, no build pipeline) - index.html: landing page with client + platform overview - getting-started.html: tabbed MCP config for Claude Desktop, Codex CLI, opencode - platforms.html: LinkedIn, TikTok, WhatsApp, Instagram, Twitter, Telegram guides - agent-tutorial.html: complete Node.js agent (Anthropic SDK + MCP SDK), LinkedIn posting loop, extensions for multi-platform + inbound webhook reaction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
276 lines
12 KiB
HTML
276 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>SquareMCP — AI Social Gateway</title>
|
||
<link rel="stylesheet" href="styles.css">
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<!-- Password Reset Request View -->
|
||
<div id="reset-request-view" class="view hidden">
|
||
<div class="auth-card">
|
||
<div class="logo">
|
||
<div class="logo-mark">S</div>
|
||
<h1>Reset Password</h1>
|
||
<p>Enter your email to receive a reset link</p>
|
||
</div>
|
||
<form id="reset-request-form" class="auth-form">
|
||
<input type="email" name="email" placeholder="Email" required>
|
||
<button type="submit" class="btn btn-primary">Send Reset Link</button>
|
||
<p class="error-msg" id="reset-request-error"></p>
|
||
<p class="success-msg" id="reset-request-success"></p>
|
||
<a href="#" id="back-to-login" style="color:#888;font-size:13px;text-align:center;display:block;margin-top:12px;">Back to login</a>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Password Reset Confirm View -->
|
||
<div id="reset-confirm-view" class="view hidden">
|
||
<div class="auth-card">
|
||
<div class="logo">
|
||
<div class="logo-mark">S</div>
|
||
<h1>New Password</h1>
|
||
<p>Enter your new password below</p>
|
||
</div>
|
||
<form id="reset-confirm-form" class="auth-form">
|
||
<input type="password" name="password" placeholder="New password (min 8 chars)" required minlength="8">
|
||
<button type="submit" class="btn btn-primary">Update Password</button>
|
||
<p class="error-msg" id="reset-confirm-error"></p>
|
||
<p class="success-msg" id="reset-confirm-success"></p>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Login View -->
|
||
<div id="login-view" class="view">
|
||
<div class="auth-card">
|
||
<div class="logo">
|
||
<div class="logo-mark">S</div>
|
||
<h1>SquareMCP</h1>
|
||
<p>AI Social Media Gateway</p>
|
||
</div>
|
||
<div class="tabs">
|
||
<button class="tab-btn active" data-tab="login">Sign In</button>
|
||
<button class="tab-btn" data-tab="signup">Create Account</button>
|
||
</div>
|
||
<form id="login-form" class="auth-form">
|
||
<input type="email" name="email" placeholder="Email" required>
|
||
<input type="password" name="password" placeholder="Password" required minlength="8">
|
||
<button type="submit" class="btn btn-primary">Sign In</button>
|
||
<p class="error-msg" id="login-error"></p>
|
||
</form>
|
||
<form id="signup-form" class="auth-form hidden">
|
||
<input type="email" name="email" placeholder="Email" required>
|
||
<input type="password" name="password" placeholder="Password (min 8 chars)" required minlength="8">
|
||
<button type="submit" class="btn btn-primary">Create Account</button>
|
||
<p class="error-msg" id="signup-error"></p>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dashboard View -->
|
||
<div id="dashboard-view" class="view hidden">
|
||
<header class="app-header">
|
||
<div class="header-left">
|
||
<div class="logo-mark small">S</div>
|
||
<span class="app-title">SquareMCP</span>
|
||
</div>
|
||
<div class="header-right">
|
||
<nav class="header-nav">
|
||
<button class="nav-link active" data-view="platforms">Platforms</button>
|
||
<button class="nav-link" data-view="analytics">Analytics</button>
|
||
<button class="nav-link" data-view="invoices">Invoices</button>
|
||
<button class="nav-link" data-view="webhooks">Webhooks</button>
|
||
<button class="nav-link hidden" data-view="admin" id="admin-nav">Admin</button>
|
||
</nav>
|
||
<span id="user-email" class="user-email"></span>
|
||
<button id="logout-btn" class="btn btn-ghost">Logout</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="dashboard">
|
||
<section class="welcome">
|
||
<h2>Connect your platforms</h2>
|
||
<p>Link your social accounts to publish, analyze, and manage content from one place.</p>
|
||
<button id="connect-mcp-btn" class="btn btn-primary" style="margin-top:16px;">Connect MCP Client</button>
|
||
</section>
|
||
|
||
<section class="usage-bar" id="usage-bar">
|
||
<div class="usage-info">
|
||
<span class="plan-badge" id="plan-badge">Free</span>
|
||
<span class="usage-text" id="usage-text">0 / 100 calls this month</span>
|
||
</div>
|
||
<div class="usage-bar-track"><div class="usage-bar-fill" id="usage-bar-fill" style="width:0%"></div></div>
|
||
</section>
|
||
|
||
<section class="platform-grid">
|
||
<!-- TikTok -->
|
||
<div class="platform-card" data-platform="tiktok">
|
||
<div class="platform-icon" style="background:#000;">🎵</div>
|
||
<div class="platform-info">
|
||
<h3>TikTok</h3>
|
||
<p class="platform-desc">Publish videos and view analytics</p>
|
||
<span class="status-badge disconnected">Not connected</span>
|
||
</div>
|
||
<button class="btn btn-connect" data-platform="tiktok">Connect</button>
|
||
</div>
|
||
|
||
<!-- Facebook -->
|
||
<div class="platform-card" data-platform="facebook">
|
||
<div class="platform-icon" style="background:#1877f2;">f</div>
|
||
<div class="platform-info">
|
||
<h3>Facebook</h3>
|
||
<p class="platform-desc">Post to pages and manage content</p>
|
||
<span class="status-badge disconnected">Not connected</span>
|
||
</div>
|
||
<button class="btn btn-connect" data-platform="facebook">Connect</button>
|
||
</div>
|
||
|
||
<!-- Instagram -->
|
||
<div class="platform-card" data-platform="instagram">
|
||
<div class="platform-icon" style="background:linear-gradient(45deg,#f09433,#e6683c,#dc2743,#cc2366,#bc1888);">📷</div>
|
||
<div class="platform-info">
|
||
<h3>Instagram</h3>
|
||
<p class="platform-desc">Publish reels and images</p>
|
||
<span class="status-badge disconnected">Not connected</span>
|
||
</div>
|
||
<button class="btn btn-connect" data-platform="instagram">Connect</button>
|
||
</div>
|
||
|
||
<!-- LinkedIn -->
|
||
<div class="platform-card" data-platform="linkedin">
|
||
<div class="platform-icon" style="background:#0a66c2;">in</div>
|
||
<div class="platform-info">
|
||
<h3>LinkedIn</h3>
|
||
<p class="platform-desc">Share posts, images, and videos</p>
|
||
<span class="status-badge disconnected">Not connected</span>
|
||
</div>
|
||
<button class="btn btn-connect" data-platform="linkedin">Connect</button>
|
||
</div>
|
||
|
||
<!-- Twitter/X -->
|
||
<div class="platform-card" data-platform="twitter">
|
||
<div class="platform-icon" style="background:#000;">𝕏</div>
|
||
<div class="platform-info">
|
||
<h3>Twitter / X</h3>
|
||
<p class="platform-desc">Tweet with media support</p>
|
||
<span class="status-badge disconnected">Not connected</span>
|
||
</div>
|
||
<button class="btn btn-connect" data-platform="twitter">Connect</button>
|
||
</div>
|
||
|
||
<!-- Telegram -->
|
||
<div class="platform-card" data-platform="telegram">
|
||
<div class="platform-icon" style="background:#0088cc;">✈️</div>
|
||
<div class="platform-info">
|
||
<h3>Telegram</h3>
|
||
<p class="platform-desc">Send messages via bot</p>
|
||
<span class="status-badge disconnected">Not connected</span>
|
||
</div>
|
||
<button class="btn btn-connect" data-platform="telegram">Connect</button>
|
||
</div>
|
||
|
||
<!-- Discord -->
|
||
<div class="platform-card" data-platform="discord">
|
||
<div class="platform-icon" style="background:#5865f2;">🎮</div>
|
||
<div class="platform-info">
|
||
<h3>Discord</h3>
|
||
<p class="platform-desc">Send messages to channels</p>
|
||
<span class="status-badge disconnected">Not connected</span>
|
||
</div>
|
||
<button class="btn btn-connect" data-platform="discord">Connect</button>
|
||
</div>
|
||
|
||
<!-- WhatsApp -->
|
||
<div class="platform-card" data-platform="whatsapp">
|
||
<div class="platform-icon" style="background:#25d366;">💬</div>
|
||
<div class="platform-info">
|
||
<h3>WhatsApp</h3>
|
||
<p class="platform-desc">Business messaging</p>
|
||
<span class="status-badge disconnected">Not connected</span>
|
||
</div>
|
||
<button class="btn btn-connect" data-platform="whatsapp">Connect</button>
|
||
</div>
|
||
|
||
<!-- Email -->
|
||
<div class="platform-card" data-platform="email">
|
||
<div class="platform-icon" style="background:#ea4335;">✉️</div>
|
||
<div class="platform-info">
|
||
<h3>Email</h3>
|
||
<p class="platform-desc">IMAP/SMTP accounts</p>
|
||
<span class="status-badge disconnected">Not connected</span>
|
||
</div>
|
||
<button class="btn btn-connect" data-platform="email">Connect</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="analytics-section hidden" id="analytics-section">
|
||
<h3 class="section-title">Analytics</h3>
|
||
<p class="section-subtitle">Tool calls this month, by platform and day</p>
|
||
<div class="charts-grid">
|
||
<div class="chart-card">
|
||
<h4>By platform</h4>
|
||
<div class="chart-container"><canvas id="platform-chart"></canvas></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<h4>Daily activity</h4>
|
||
<div class="chart-container"><canvas id="daily-chart"></canvas></div>
|
||
</div>
|
||
</div>
|
||
<div id="analytics-empty" class="analytics-empty hidden">
|
||
<p>No activity yet this month. Make your first tool call to see data here.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="invoices-section hidden" id="invoices-section">
|
||
<h3>Invoices</h3>
|
||
<div id="invoices-list" class="invoices-list"></div>
|
||
</section>
|
||
|
||
<section class="admin-section hidden" id="admin-section">
|
||
<h3>Admin Panel</h3>
|
||
<div id="admin-customers" class="admin-customers"></div>
|
||
</section>
|
||
|
||
<section class="webhooks-section hidden" id="webhooks-section">
|
||
<h3 class="section-title">Webhook</h3>
|
||
<p class="section-subtitle">Receive real-time events when messages arrive on your connected platforms.</p>
|
||
<div class="webhook-card" id="webhook-card">
|
||
<div id="webhook-status-row" class="webhook-status-row">
|
||
<span id="webhook-url-display" class="webhook-url-display">No webhook configured</span>
|
||
<button id="webhook-delete-btn" class="btn btn-ghost hidden">Remove</button>
|
||
</div>
|
||
<form id="webhook-form" class="webhook-form">
|
||
<input type="url" id="webhook-url-input" placeholder="https://your-server.com/webhook" required>
|
||
<button type="submit" class="btn btn-primary">Save & generate secret</button>
|
||
</form>
|
||
<div id="webhook-secret-box" class="webhook-secret-box hidden">
|
||
<p class="webhook-secret-label">Signing secret — copy now, not shown again:</p>
|
||
<div class="webhook-secret-value" id="webhook-secret-value"></div>
|
||
<button class="btn btn-ghost" onclick="copyWebhookSecret()">Copy secret</button>
|
||
</div>
|
||
<div class="webhook-instructions">
|
||
<p>Verify each request by computing <code>sha256=HMAC-SHA256(secret, rawBody)</code> and comparing to the <code>X-SquareMCP-Signature</code> header.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Connection Modal -->
|
||
<div id="connect-modal" class="modal hidden">
|
||
<div class="modal-backdrop"></div>
|
||
<div class="modal-content">
|
||
<button class="modal-close">×</button>
|
||
<div id="modal-body"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||
<script src="app.js"></script>
|
||
</body>
|
||
</html>
|