feat(saas): full SquareMCP SaaS platform v1

- JWT auth with bcrypt password hashing, cookie sessions, forgot/reset password
- Per-user encrypted credential storage (Redis + AES-256-GCM) for all 9 platforms
- Usage tracking with monthly limits per plan (free/starter/growth/enterprise)
- Invoice generation and retrieval (admin + user views)
- Admin panel with customer listing (role-based access)
- Web app UI at app.squaremcp.com — login, dashboard, connections, usage, invoices
- Unified auth middleware: API key, OAuth Bearer, and JWT cookie support
- Facebook Graph API fixes: published_posts endpoint, photo/video post support
- TikTok sandbox compliance: SELF_ONLY privacy for unaudited apps
- URL verification files for TikTok app review
This commit is contained in:
Garfield
2026-05-13 08:42:33 -04:00
parent 7796de12bf
commit a5e4c55885
46 changed files with 4054 additions and 171 deletions

6
product/app/Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM nginx:1.27-alpine
COPY product/app/nginx-app.conf /etc/nginx/conf.d/default.conf
COPY product/app/index.html /usr/share/nginx/html/index.html
COPY product/app/styles.css /usr/share/nginx/html/styles.css
COPY product/app/app.js /usr/share/nginx/html/app.js
EXPOSE 8080

65
product/app/app-k8s.yaml Normal file
View File

@@ -0,0 +1,65 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: squaremcp-app
namespace: fetcherpay
spec:
replicas: 1
selector:
matchLabels:
app: squaremcp-app
template:
metadata:
labels:
app: squaremcp-app
spec:
containers:
- name: squaremcp-app
image: localhost:32000/squaremcp-app@sha256:45d7adfe10efe727ec1f6c1f5a64ad12d9aa426af90145b5f8d7c1a9dbbe9536
imagePullPolicy: Always
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 3
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: squaremcp-app
namespace: fetcherpay
spec:
selector:
app: squaremcp-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: squaremcp-app-ingress
namespace: fetcherpay
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
rules:
- host: app.squaremcp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: squaremcp-app
port:
number: 80
tls:
- hosts:
- app.squaremcp.com
secretName: squaremcp-app-tls

532
product/app/app.js Normal file
View File

@@ -0,0 +1,532 @@
const API_BASE = 'https://hermes.squaremcp.com';
// State
let currentUser = null;
let isAdmin = false;
// DOM refs
const loginView = document.getElementById('login-view');
const resetRequestView = document.getElementById('reset-request-view');
const resetConfirmView = document.getElementById('reset-confirm-view');
const dashboardView = document.getElementById('dashboard-view');
const loginForm = document.getElementById('login-form');
const signupForm = document.getElementById('signup-form');
const resetRequestForm = document.getElementById('reset-request-form');
const resetConfirmForm = document.getElementById('reset-confirm-form');
const loginError = document.getElementById('login-error');
const signupError = document.getElementById('signup-error');
const tabBtns = document.querySelectorAll('.tab-btn');
const userEmail = document.getElementById('user-email');
const logoutBtn = document.getElementById('logout-btn');
const modal = document.getElementById('connect-modal');
const modalBody = document.getElementById('modal-body');
const modalClose = document.querySelector('.modal-close');
const modalBackdrop = document.querySelector('.modal-backdrop');
const navLinks = document.querySelectorAll('.nav-link');
const platformGrid = document.querySelector('.platform-grid');
const invoicesSection = document.getElementById('invoices-section');
const adminSection = document.getElementById('admin-section');
const adminNav = document.getElementById('admin-nav');
// Platform config
const PLATFORM_CONFIG = {
tiktok: {
name: 'TikTok',
type: 'oauth',
oauthUrl: 'https://tiktok.squaremcp.com/auth/tiktok/start',
scopes: 'user.info.basic,user.info.profile,user.info.stats,video.list,video.publish',
},
facebook: {
name: 'Facebook',
type: 'token',
fields: [
{ key: 'accessToken', label: 'Page Access Token', type: 'password' },
{ key: 'pageId', label: 'Page ID', type: 'text' },
],
help: 'Get your Page Access Token from the Facebook Graph API Explorer. Run GET /me/accounts and copy the access_token for your page.',
},
instagram: {
name: 'Instagram',
type: 'token',
fields: [
{ key: 'accessToken', label: 'Instagram Access Token', type: 'password' },
{ key: 'businessAccountId', label: 'Business Account ID', type: 'text' },
],
help: 'Your Instagram account must be a Business/Creator account connected to a Facebook Page. Get the token from Graph API Explorer with instagram_basic scope.',
},
linkedin: {
name: 'LinkedIn',
type: 'token',
fields: [
{ key: 'accessToken', label: 'LinkedIn Access Token', type: 'password' },
],
help: 'Generate an access token from the LinkedIn Developer Portal with w_member_social scope.',
},
twitter: {
name: 'Twitter / X',
type: 'token',
fields: [
{ key: 'accessToken', label: 'Access Token', type: 'password' },
{ key: 'accessTokenSecret', label: 'Access Token Secret', type: 'password' },
{ key: 'apiKey', label: 'API Key', type: 'password' },
{ key: 'apiSecret', label: 'API Secret', type: 'password' },
],
help: 'Get all four values from the Twitter Developer Portal under Keys and Tokens.',
},
telegram: {
name: 'Telegram',
type: 'token',
fields: [
{ key: 'accessToken', label: 'Bot Token', type: 'password' },
],
help: 'Message @BotFather on Telegram and create a new bot. Copy the token it gives you.',
},
discord: {
name: 'Discord',
type: 'token',
fields: [
{ key: 'accessToken', label: 'Bot Token', type: 'password' },
],
help: 'Go to discord.com/developers/applications → New Application → Bot → Reset Token.',
},
whatsapp: {
name: 'WhatsApp',
type: 'token',
fields: [
{ key: 'accessToken', label: 'Access Token', type: 'password' },
{ key: 'phoneNumberId', label: 'Phone Number ID', type: 'text' },
{ key: 'businessAccountId', label: 'Business Account ID', type: 'text' },
],
help: 'From Meta Business Settings → WhatsApp → API Setup. You need a verified business phone number.',
},
email: {
name: 'Email',
type: 'token',
fields: [
{ key: 'host', label: 'IMAP Host', type: 'text' },
{ key: 'port', label: 'IMAP Port', type: 'text' },
{ key: 'user', label: 'Email Address', type: 'email' },
{ key: 'password', label: 'App Password', type: 'password' },
{ key: 'smtpHost', label: 'SMTP Host', type: 'text' },
{ key: 'smtpPort', label: 'SMTP Port', type: 'text' },
],
help: 'Use an app-specific password (not your login password). Common hosts: Gmail=smtp.gmail.com, Yahoo=smtp.mail.yahoo.com.',
},
};
// Auth helpers
async function apiPost(path, body) {
const res = await fetch(`${API_BASE}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(body),
});
return res.json();
}
async function apiGet(path) {
const res = await fetch(`${API_BASE}${path}`, { credentials: 'include' });
return res.json();
}
// Views
function hideAllViews() {
loginView.classList.add('hidden');
resetRequestView.classList.add('hidden');
resetConfirmView.classList.add('hidden');
dashboardView.classList.add('hidden');
}
function showLogin() {
hideAllViews();
loginView.classList.remove('hidden');
}
function showDashboard() {
hideAllViews();
dashboardView.classList.remove('hidden');
userEmail.textContent = currentUser?.email || '';
updateConnectionStatus();
updateUsage();
loadInvoices();
if (isAdmin) loadAdminPanel();
}
// Tab switching
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
tabBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const tab = btn.dataset.tab;
if (tab === 'login') {
loginForm.classList.remove('hidden');
signupForm.classList.add('hidden');
} else {
loginForm.classList.add('hidden');
signupForm.classList.remove('hidden');
}
});
});
// Navigation
navLinks.forEach(link => {
link.addEventListener('click', () => {
navLinks.forEach(l => l.classList.remove('active'));
link.classList.add('active');
const view = link.dataset.view;
platformGrid.classList.toggle('hidden', view !== 'platforms');
document.querySelector('.welcome').classList.toggle('hidden', view !== 'platforms');
document.querySelector('.usage-bar').classList.toggle('hidden', view !== 'platforms');
invoicesSection.classList.toggle('hidden', view !== 'invoices');
adminSection.classList.toggle('hidden', view !== 'admin');
});
});
// Login
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
loginError.textContent = '';
const fd = new FormData(loginForm);
const data = await apiPost('/api/auth/login', {
email: fd.get('email'),
password: fd.get('password'),
});
if (data.error) {
loginError.textContent = data.error;
return;
}
currentUser = data;
isAdmin = data.plan === 'enterprise'; // simplistic admin check
showDashboard();
});
// Signup
signupForm.addEventListener('submit', async (e) => {
e.preventDefault();
signupError.textContent = '';
const fd = new FormData(signupForm);
const data = await apiPost('/api/auth/signup', {
email: fd.get('email'),
password: fd.get('password'),
});
if (data.error) {
signupError.textContent = data.error;
return;
}
currentUser = data;
isAdmin = false;
showDashboard();
});
// Logout
logoutBtn.addEventListener('click', async () => {
await apiPost('/api/auth/logout', {});
currentUser = null;
isAdmin = false;
showLogin();
});
// Password reset request
resetRequestForm?.addEventListener('submit', async (e) => {
e.preventDefault();
const errorEl = document.getElementById('reset-request-error');
const successEl = document.getElementById('reset-request-success');
errorEl.textContent = '';
successEl.textContent = '';
const fd = new FormData(resetRequestForm);
const data = await apiPost('/api/auth/forgot-password', { email: fd.get('email') });
if (data.resetUrl) {
successEl.textContent = `Reset link: ${data.resetUrl}`;
} else {
successEl.textContent = data.message;
}
});
document.getElementById('back-to-login')?.addEventListener('click', (e) => {
e.preventDefault();
showLogin();
});
// Password reset confirm
resetConfirmForm?.addEventListener('submit', async (e) => {
e.preventDefault();
const errorEl = document.getElementById('reset-confirm-error');
const successEl = document.getElementById('reset-confirm-success');
errorEl.textContent = '';
successEl.textContent = '';
const fd = new FormData(resetConfirmForm);
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (!token) {
errorEl.textContent = 'Missing reset token';
return;
}
const data = await apiPost('/api/auth/reset-password', {
token,
password: fd.get('password'),
});
if (data.error) {
errorEl.textContent = data.error;
return;
}
successEl.textContent = 'Password updated! Redirecting to login...';
setTimeout(() => {
window.location.href = '/';
}, 2000);
});
// Add forgot password link to login form
const forgotLink = document.createElement('a');
forgotLink.href = '#';
forgotLink.textContent = 'Forgot password?';
forgotLink.style.cssText = 'color:#888;font-size:13px;text-align:center;display:block;margin-top:8px;';
forgotLink.addEventListener('click', (e) => {
e.preventDefault();
hideAllViews();
resetRequestView.classList.remove('hidden');
});
loginForm.appendChild(forgotLink);
// Connection status
async function updateConnectionStatus() {
try {
const data = await apiGet('/api/connections');
const connections = data.connections || {};
document.querySelectorAll('.platform-card').forEach(card => {
const platform = card.dataset.platform;
const badge = card.querySelector('.status-badge');
const btn = card.querySelector('.btn-connect');
if (connections[platform]) {
badge.textContent = 'Connected';
badge.classList.remove('disconnected');
badge.classList.add('connected');
btn.textContent = 'Manage';
} else {
badge.textContent = 'Not connected';
badge.classList.remove('connected');
badge.classList.add('disconnected');
btn.textContent = 'Connect';
}
});
} catch {
// ignore
}
}
// Usage
async function updateUsage() {
try {
const data = await apiGet('/api/usage');
const badge = document.getElementById('plan-badge');
const text = document.getElementById('usage-text');
const fill = document.getElementById('usage-bar-fill');
badge.textContent = data.plan;
if (data.monthlyLimit === -1) {
text.textContent = `${data.used} calls (unlimited)`;
fill.style.width = '5%';
fill.className = 'usage-bar-fill';
} else {
text.textContent = `${data.used} / ${data.monthlyLimit} calls this month`;
const pct = Math.min(100, (data.used / data.monthlyLimit) * 100);
fill.style.width = pct + '%';
fill.className = 'usage-bar-fill' + (pct > 90 ? ' danger' : pct > 70 ? ' warning' : '');
}
} catch {
// ignore
}
}
// Invoices
async function loadInvoices() {
try {
const data = await apiGet('/api/invoices');
const list = document.getElementById('invoices-list');
const invoices = data.invoices || [];
if (!invoices.length) {
list.innerHTML = '<p style="color:#888;font-size:14px;">No invoices yet.</p>';
return;
}
list.innerHTML = invoices.map(inv => `
<div class="invoice-item">
<div>
<div class="inv-num">${inv.invoice_number}</div>
<div class="inv-period">${inv.period_start}${inv.period_end}</div>
</div>
<div style="text-align:right;">
<div class="inv-amount">$${inv.amount}</div>
<span class="inv-status ${inv.status}">${inv.status}</span>
</div>
</div>
`).join('');
} catch {
document.getElementById('invoices-list').innerHTML = '<p style="color:#888;font-size:14px;">Failed to load invoices.</p>';
}
}
// Admin panel
async function loadAdminPanel() {
try {
const data = await apiGet('/api/admin/customers');
const container = document.getElementById('admin-customers');
const customers = data.customers || [];
container.innerHTML = `
<table class="admin-table">
<thead><tr><th>Email</th><th>Plan</th><th>Role</th><th>Active</th><th>Joined</th><th>Actions</th></tr></thead>
<tbody>
${customers.map(c => `
<tr>
<td>${c.email}</td>
<td>${c.plan}</td>
<td>${c.role}</td>
<td>${c.active ? 'Yes' : 'No'}</td>
<td>${new Date(c.created_at).toLocaleDateString()}</td>
<td><button class="btn btn-small btn-connect" data-cid="${c.id}" onclick="generateInvoice('${c.id}')">Invoice</button></td>
</tr>
`).join('')}
</tbody>
</table>
`;
} catch {
document.getElementById('admin-customers').innerHTML = '<p style="color:#888;font-size:14px;padding:20px;">Failed to load admin data.</p>';
}
}
window.generateInvoice = async function(customerId) {
try {
const res = await fetch(`${API_BASE}/api/admin/customers/${customerId}/invoice`, {
method: 'POST',
credentials: 'include',
});
const data = await res.json();
if (data.error) {
alert(data.error);
return;
}
alert(`Invoice ${data.invoice_number} created for $${data.amount}`);
loadAdminPanel();
} catch {
alert('Failed to generate invoice');
}
};
// Modal
function openModal(html) {
modalBody.innerHTML = html;
modal.classList.remove('hidden');
}
function closeModal() {
modal.classList.add('hidden');
modalBody.innerHTML = '';
}
modalClose.addEventListener('click', closeModal);
modalBackdrop.addEventListener('click', closeModal);
// Platform connection
function renderConnectForm(platform) {
const cfg = PLATFORM_CONFIG[platform];
if (!cfg) return '';
if (cfg.type === 'oauth') {
const state = btoa(JSON.stringify({ platform, user: currentUser?.id }));
const url = `${cfg.oauthUrl}?state=${encodeURIComponent(state)}&scope=${encodeURIComponent(cfg.scopes)}`;
return `
<div class="connect-form">
<h3>Connect ${cfg.name}</h3>
<p>You'll be redirected to ${cfg.name} to authorize SquareMCP.</p>
<div class="instructions">${cfg.help || ''}</div>
<a href="${url}" target="_blank" class="btn btn-primary" style="text-align:center;text-decoration:none;display:inline-block;">Connect with ${cfg.name}</a>
</div>
`;
}
const fieldsHtml = cfg.fields.map(f => `
<label>${f.label}</label>
<input type="${f.type}" name="${f.key}" placeholder="${f.label}" required>
`).join('');
return `
<form class="connect-form" data-platform="${platform}">
<h3>Connect ${cfg.name}</h3>
<p>Paste your credentials below. They are encrypted and stored securely.</p>
${fieldsHtml}
<div class="instructions">${cfg.help}</div>
<button type="submit" class="btn btn-primary">Save Connection</button>
<p class="error-msg" id="connect-error"></p>
</form>
`;
}
document.querySelectorAll('.btn-connect').forEach(btn => {
btn.addEventListener('click', () => {
const platform = btn.dataset.platform;
openModal(renderConnectForm(platform));
const form = modalBody.querySelector('form.connect-form');
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const errorEl = document.getElementById('connect-error');
errorEl.textContent = '';
errorEl.style.color = '#dc2626';
const fd = new FormData(form);
const credentials = {};
for (const [key, value] of fd.entries()) {
credentials[key] = value;
}
try {
const res = await fetch(`${API_BASE}/api/connect/${platform}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(credentials),
});
const data = await res.json();
if (data.error) {
errorEl.textContent = data.error;
return;
}
errorEl.textContent = 'Connected successfully!';
errorEl.style.color = '#10a37f';
setTimeout(() => {
closeModal();
updateConnectionStatus();
}, 1000);
} catch (err) {
errorEl.textContent = 'Network error. Please try again.';
}
});
}
});
});
// Check session on load
async function checkSession() {
// Check for reset token in URL
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('token')) {
hideAllViews();
resetConfirmView.classList.remove('hidden');
return;
}
try {
const data = await apiGet('/api/auth/me');
if (data.id) {
currentUser = data;
isAdmin = data.plan === 'enterprise';
if (isAdmin) adminNav.classList.remove('hidden');
showDashboard();
} else {
showLogin();
}
} catch {
showLogin();
}
}
// Init
checkSession();

230
product/app/index.html Normal file
View File

@@ -0,0 +1,230 @@
<!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="invoices">Invoices</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>
</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="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>
</main>
</div>
<!-- Connection Modal -->
<div id="connect-modal" class="modal hidden">
<div class="modal-backdrop"></div>
<div class="modal-content">
<button class="modal-close">&times;</button>
<div id="modal-body"></div>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,15 @@
server {
listen 8080;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

610
product/app/styles.css Normal file
View File

@@ -0,0 +1,610 @@
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f0f10;
--surface: #1a1a1b;
--surface-hover: #222223;
--border: #2a2a2b;
--text: #e5e5e5;
--text-secondary: #888;
--accent: #10a37f;
--accent-hover: #0d8c6d;
--danger: #dc2626;
--radius: 12px;
--shadow: 0 4px 24px rgba(0,0,0,0.4);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
line-height: 1.5;
}
.hidden { display: none !important; }
/* Views */
.view {
min-height: 100vh;
}
/* Auth */
#login-view {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.auth-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 40px;
width: 100%;
max-width: 400px;
box-shadow: var(--shadow);
}
.logo {
text-align: center;
margin-bottom: 32px;
}
.logo-mark {
width: 56px;
height: 56px;
background: linear-gradient(135deg, #25f4ee, #fe2c55);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
font-size: 28px;
margin: 0 auto 16px;
}
.logo-mark.small {
width: 36px;
height: 36px;
font-size: 18px;
border-radius: 10px;
margin: 0;
}
.logo h1 {
font-size: 24px;
font-weight: 600;
margin-bottom: 4px;
}
.logo p {
color: var(--text-secondary);
font-size: 14px;
}
.tabs {
display: flex;
gap: 8px;
margin-bottom: 24px;
background: var(--bg);
padding: 4px;
border-radius: 10px;
}
.tab-btn {
flex: 1;
padding: 10px;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 14px;
font-weight: 500;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.tab-btn.active {
background: var(--surface);
color: var(--text);
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.auth-form {
display: flex;
flex-direction: column;
gap: 14px;
}
.auth-form input {
padding: 12px 14px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 15px;
outline: none;
transition: border-color 0.2s;
}
.auth-form input:focus {
border-color: var(--accent);
}
.auth-form input::placeholder {
color: #555;
}
.btn {
padding: 12px 20px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: var(--accent);
color: #fff;
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-ghost {
background: transparent;
color: var(--text-secondary);
padding: 8px 14px;
font-size: 13px;
}
.btn-ghost:hover {
color: var(--text);
}
.btn-connect {
background: var(--bg);
color: var(--text);
border: 1px solid var(--border);
padding: 8px 16px;
font-size: 13px;
white-space: nowrap;
}
.btn-connect:hover {
background: var(--surface-hover);
border-color: var(--accent);
}
.error-msg {
color: var(--danger);
font-size: 13px;
text-align: center;
min-height: 18px;
}
/* Dashboard */
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 24px;
background: var(--surface);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 10;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.app-title {
font-weight: 600;
font-size: 16px;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.user-email {
color: var(--text-secondary);
font-size: 14px;
}
.dashboard {
max-width: 1200px;
margin: 0 auto;
padding: 32px 24px;
}
.welcome {
margin-bottom: 32px;
}
.welcome h2 {
font-size: 28px;
font-weight: 600;
margin-bottom: 8px;
}
.welcome p {
color: var(--text-secondary);
font-size: 16px;
}
.platform-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
.platform-card {
display: flex;
align-items: center;
gap: 16px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
transition: border-color 0.2s, transform 0.2s;
}
.platform-card:hover {
border-color: #3a3a3b;
transform: translateY(-2px);
}
.platform-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
flex-shrink: 0;
}
.platform-info {
flex: 1;
min-width: 0;
}
.platform-info h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 2px;
}
.platform-desc {
color: var(--text-secondary);
font-size: 13px;
margin-bottom: 6px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.status-badge.connected {
background: rgba(16, 163, 127, 0.15);
color: var(--accent);
}
.status-badge.disconnected {
background: rgba(136, 136, 136, 0.1);
color: var(--text-secondary);
}
/* Modal */
.modal {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.modal-backdrop {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.7);
backdrop-filter: blur(4px);
}
.modal-content {
position: relative;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 32px;
width: 100%;
max-width: 480px;
max-height: 80vh;
overflow-y: auto;
box-shadow: var(--shadow);
z-index: 1;
}
.modal-close {
position: absolute;
top: 16px;
right: 16px;
background: none;
border: none;
color: var(--text-secondary);
font-size: 24px;
cursor: pointer;
}
.modal-close:hover {
color: var(--text);
}
/* Connection form in modal */
.connect-form {
display: flex;
flex-direction: column;
gap: 14px;
}
.connect-form h3 {
margin-bottom: 8px;
}
.connect-form p {
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 8px;
}
.connect-form label {
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
}
.connect-form input {
padding: 12px 14px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 14px;
outline: none;
}
.connect-form input:focus {
border-color: var(--accent);
}
.connect-form .btn-primary {
margin-top: 8px;
}
.instructions {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
}
.instructions code {
background: #2a2a2b;
padding: 2px 6px;
border-radius: 4px;
font-family: 'SF Mono', monospace;
font-size: 12px;
}
@media (max-width: 640px) {
.platform-grid {
grid-template-columns: 1fr;
}
.auth-card {
padding: 28px 20px;
}
}
/* Usage bar */
.usage-bar {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px 20px;
margin-bottom: 24px;
}
.usage-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.plan-badge {
background: var(--accent);
color: #fff;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.usage-text {
color: var(--text-secondary);
font-size: 13px;
}
.usage-bar-track {
height: 6px;
background: var(--bg);
border-radius: 3px;
overflow: hidden;
}
.usage-bar-fill {
height: 100%;
background: var(--accent);
border-radius: 3px;
transition: width 0.5s ease;
}
.usage-bar-fill.warning {
background: #f59e0b;
}
.usage-bar-fill.danger {
background: #dc2626;
}
/* Header nav */
.header-nav {
display: flex;
gap: 4px;
margin-right: 16px;
}
.nav-link {
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 13px;
font-weight: 500;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.nav-link:hover {
color: var(--text);
}
.nav-link.active {
background: var(--bg);
color: var(--text);
}
/* Invoices */
.invoices-section, .admin-section {
margin-top: 24px;
}
.invoices-section h3, .admin-section h3 {
font-size: 18px;
margin-bottom: 16px;
}
.invoices-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.invoice-item {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px 20px;
}
.invoice-item .inv-num {
font-weight: 600;
font-size: 14px;
}
.invoice-item .inv-period {
color: var(--text-secondary);
font-size: 12px;
}
.invoice-item .inv-amount {
font-weight: 700;
font-size: 16px;
}
.invoice-item .inv-status {
padding: 3px 10px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.inv-status.draft { background: rgba(136,136,136,0.1); color: #888; }
.inv-status.sent { background: rgba(245,158,11,0.15); color: #f59e0b; }
.inv-status.paid { background: rgba(16,163,127,0.15); color: var(--accent); }
.inv-status.overdue { background: rgba(220,38,38,0.15); color: #dc2626; }
/* Admin */
.admin-customers {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.admin-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.admin-table th {
text-align: left;
padding: 12px 16px;
color: var(--text-secondary);
font-weight: 500;
border-bottom: 1px solid var(--border);
}
.admin-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
.admin-table tr:last-child td {
border-bottom: none;
}
.admin-table .btn-small {
padding: 4px 10px;
font-size: 11px;
}
/* Password reset */
.success-msg {
color: var(--accent);
font-size: 13px;
text-align: center;
min-height: 18px;
}

View File

@@ -6,5 +6,12 @@ COPY product/site/styles.css /usr/share/nginx/html/styles.css
COPY product/site/script.js /usr/share/nginx/html/script.js
COPY product/site/squaremcp-logo.svg /usr/share/nginx/html/squaremcp-logo.svg
COPY product/site/squaremcp-hero-loop.mp4 /usr/share/nginx/html/squaremcp-hero-loop.mp4
COPY product/site/squaremcp-tiktok-launch.mp4 /usr/share/nginx/html/squaremcp-tiktok-launch.mp4
COPY product/site/tiktok /usr/share/nginx/html/tiktok
COPY product/site/tiktokkFNJHjzDuzvGIlXnK4MaGw3MSluybOih.txt /usr/share/nginx/html/tiktokkFNJHjzDuzvGIlXnK4MaGw3MSluybOih.txt
COPY product/site/privacy.html /usr/share/nginx/html/privacy.html
COPY product/site/privacy /usr/share/nginx/html/privacy
COPY product/site/terms.html /usr/share/nginx/html/terms.html
COPY product/site/terms /usr/share/nginx/html/terms
COPY product/site/tiktok/tiktokIIxGZsCAoYNJVObSA8cYj8FyWrAu1nIW.txt /usr/share/nginx/html/tiktokIIxGZsCAoYNJVObSA8cYj8FyWrAu1nIW.txt
COPY product/site/tiktok/tiktokwJMUoUxgRBFO9T8p08qlMgtsez0Ktr9y.txt /usr/share/nginx/html/tiktokwJMUoUxgRBFO9T8p08qlMgtsez0Ktr9y.txt

View File

@@ -1,6 +1,6 @@
server {
listen 8080;
server_name squaremcp.com www.squaremcp.com;
server_name squaremcp.com www.squaremcp.com tiktok.squaremcp.com;
root /usr/share/nginx/html;
index index.html;

View File

@@ -0,0 +1 @@
tiktok-developers-site-verification=XbaTJRvDkwUNzhEXou9SsogGyiDkQshF

View File

@@ -11,3 +11,4 @@ spec:
dnsNames:
- squaremcp.com
- www.squaremcp.com
- tiktok.squaremcp.com

View File

@@ -15,7 +15,7 @@ spec:
spec:
containers:
- name: squaremcp-site
image: localhost:32000/squaremcp-site:latest
image: localhost:32000/squaremcp-site@sha256:395e736f1899ce0f2402e34caa95359e2eb54b5424318cf8139982e66b35a974
imagePullPolicy: Always
ports:
- containerPort: 8080
@@ -90,8 +90,33 @@ spec:
name: squaremcp-site
port:
number: 80
- host: tiktok.squaremcp.com
http:
paths:
- path: /auth/tiktok
pathType: Prefix
backend:
service:
name: hermes-mcp
port:
number: 3456
- path: /api/pilot-request
pathType: Prefix
backend:
service:
name: hermes-mcp
port:
number: 3456
- path: /
pathType: Prefix
backend:
service:
name: squaremcp-site
port:
number: 80
tls:
- hosts:
- squaremcp.com
- www.squaremcp.com
- tiktok.squaremcp.com
secretName: squaremcp-tls

Binary file not shown.

View File

@@ -0,0 +1 @@
tiktok-developers-site-verification=JLZJ5wt0TEQdq2RTHGAZtY9ofrtPFBeM

Binary file not shown.

View File

@@ -0,0 +1,414 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SquareMCP - TikTok Integration Demo</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #f7f7f8;
color: #1a1a1a;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: #fff;
border-bottom: 1px solid #e5e5e5;
padding: 14px 24px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.header img { width: 32px; height: 32px; border-radius: 6px; }
.header h1 { font-size: 16px; font-weight: 600; color: #1a1a1a; }
.header .badge {
margin-left: auto;
background: #e3f2fd;
color: #1976d2;
font-size: 11px;
font-weight: 600;
padding: 4px 10px;
border-radius: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.chat {
flex: 1;
overflow-y: auto;
padding: 24px;
max-width: 800px;
width: 100%;
margin: 0 auto;
}
.message {
display: flex;
gap: 16px;
margin-bottom: 24px;
opacity: 0;
transform: translateY(12px);
transition: all 0.5s ease;
}
.message.visible { opacity: 1; transform: translateY(0); }
.avatar {
width: 32px;
height: 32px;
border-radius: 50%;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: #fff;
}
.user .avatar { background: #10a37f; }
.assistant .avatar { background: #1a1a1a; }
.bubble {
background: #fff;
border: 1px solid #e5e5e5;
border-radius: 12px;
padding: 16px 20px;
max-width: 640px;
line-height: 1.6;
font-size: 15px;
}
.user .bubble { background: #f0f9f6; border-color: #c8e6d5; }
.bubble strong { color: #1a1a1a; }
.bubble code {
background: #f4f4f5;
padding: 2px 6px;
border-radius: 4px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 13px;
}
.bubble pre {
background: #1a1a1a;
color: #e5e5e5;
padding: 14px;
border-radius: 8px;
overflow-x: auto;
font-size: 13px;
margin-top: 10px;
}
.bubble .btn {
display: inline-block;
background: #1a1a1a;
color: #fff;
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
font-size: 14px;
font-weight: 500;
margin-top: 10px;
cursor: pointer;
border: none;
}
.bubble .btn:hover { background: #333; }
.bubble .btn.tiktok {
background: #000;
background-image: linear-gradient(135deg, #25f4ee, #fe2c55);
color: #fff;
}
.bubble .success { color: #059669; font-weight: 600; }
.bubble .error { color: #dc2626; font-weight: 600; }
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #e5e5e5;
border-top-color: #1a1a1a;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
.typing {
display: flex;
gap: 4px;
padding: 8px 0;
}
.typing span {
width: 8px;
height: 8px;
background: #999;
border-radius: 50%;
animation: bounce 1.2s infinite;
}
.typing span:nth-child(2) { animation-delay: 0.2s; }
.typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-6px); }
}
.profile-card {
display: flex;
align-items: center;
gap: 16px;
margin-top: 12px;
padding: 14px;
background: #fafafa;
border-radius: 10px;
}
.profile-card img {
width: 56px;
height: 56px;
border-radius: 50%;
object-fit: cover;
}
.profile-card .info h3 { font-size: 16px; margin-bottom: 4px; }
.profile-card .info p { font-size: 13px; color: #666; }
.stats {
display: flex;
gap: 24px;
margin-top: 12px;
}
.stat { text-align: center; }
.stat .num { font-size: 20px; font-weight: 700; color: #1a1a1a; }
.stat .lbl { font-size: 12px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
.publish-form {
margin-top: 12px;
}
.publish-form input {
width: 100%;
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
margin-bottom: 10px;
}
.video-preview {
margin-top: 12px;
border-radius: 10px;
overflow: hidden;
max-width: 320px;
}
.video-preview video { width: 100%; display: block; }
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: #ecfdf5;
color: #059669;
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
margin-top: 10px;
}
.status-badge::before {
content: '';
width: 8px;
height: 8px;
background: #059669;
border-radius: 50%;
}
</style>
</head>
<body>
<div class="header">
<div style="width:32px;height:32px;background:linear-gradient(135deg,#25f4ee,#fe2c55);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:18px;">S</div>
<h1>SquareMCP</h1>
<span class="badge">TikTok Integration Demo</span>
</div>
<div class="chat" id="chat"></div>
<script>
const API_KEY = '114521f3f9e6858480d6269446a446ef';
const API_BASE = 'https://hermes.squaremcp.com';
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function addMessage(role, html) {
const chat = document.getElementById('chat');
const div = document.createElement('div');
div.className = `message ${role}`;
const avatar = role === 'user' ? 'U' : 'S';
const bg = role === 'user' ? '#10a37f' : '#1a1a1a';
div.innerHTML = `
<div class="avatar" style="background:${bg}">${avatar}</div>
<div class="bubble">${html}</div>
`;
chat.appendChild(div);
chat.scrollTop = chat.scrollHeight;
requestAnimationFrame(() => div.classList.add('visible'));
}
function addTyping() {
const chat = document.getElementById('chat');
const div = document.createElement('div');
div.className = 'message assistant';
div.id = 'typing-indicator';
div.innerHTML = `
<div class="avatar" style="background:#1a1a1a">S</div>
<div class="bubble">
<div class="typing"><span></span><span></span><span></span></div>
</div>
`;
chat.appendChild(div);
chat.scrollTop = chat.scrollHeight;
requestAnimationFrame(() => div.classList.add('visible'));
}
function removeTyping() {
const t = document.getElementById('typing-indicator');
if (t) t.remove();
}
async function apiGet(path) {
const res = await fetch(`${API_BASE}${path}`, { headers: { 'x-api-key': API_KEY } });
return res.json();
}
async function apiPost(path, body) {
const res = await fetch(`${API_BASE}${path}`, {
method: 'POST',
headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return res.json();
}
async function runDemo() {
await sleep(1500);
// Step 1: Connect
addMessage('user', 'Connect my TikTok account to SquareMCP');
await sleep(800);
addTyping();
await sleep(1500);
removeTyping();
addMessage('assistant', `
I'll help you connect your TikTok account. Click the button below to authorize SquareMCP.
<br><br>
<button class="btn tiktok" onclick="this.textContent='Connecting...'">🎵 Connect with TikTok</button>
`);
await sleep(2500);
addTyping();
await sleep(2000);
removeTyping();
addMessage('assistant', `
<span class="success">✅ Account connected successfully!</span><br>
Welcome, <strong>Garfield Heron</strong>. Your TikTok account is now linked to SquareMCP.
`);
await sleep(2000);
// Step 2: Profile
addMessage('user', 'Show my TikTok profile and stats');
await sleep(800);
addTyping();
await sleep(1500);
removeTyping();
const profile = await apiGet('/api/tiktok/profile?account=tiktok');
addMessage('assistant', `
Here's your TikTok profile:<br>
<div class="profile-card">
<img src="${profile.avatar_url || ''}" onerror="this.style.display='none'" alt="avatar">
<div class="info">
<h3>${profile.display_name || 'Garfield Heron'}</h3>
<p>@${profile.username || 'garfieldheron'} ${profile.is_verified ? '✓' : ''}</p>
</div>
</div>
<div class="stats">
<div class="stat"><div class="num">${profile.follower_count ?? 0}</div><div class="lbl">Followers</div></div>
<div class="stat"><div class="num">${profile.following_count ?? 0}</div><div class="lbl">Following</div></div>
<div class="stat"><div class="num">${profile.likes_count ?? 0}</div><div class="lbl">Likes</div></div>
<div class="stat"><div class="num">${profile.video_count ?? 0}</div><div class="lbl">Videos</div></div>
</div>
`);
await sleep(2500);
// Step 3: Creator Info
addMessage('user', 'What are my publishing options?');
await sleep(800);
addTyping();
await sleep(1500);
removeTyping();
const creator = await apiGet('/api/tiktok/creator-info?account=tiktok');
addMessage('assistant', `
Here are your creator publishing settings:<br><br>
<strong>Creator:</strong> ${creator.creator_nickname || 'Garfield Heron'} (@${creator.creator_username || 'garfieldheron'})<br>
<strong>Max video duration:</strong> ${creator.max_video_post_duration_sec || 0} seconds<br>
<strong>Privacy options:</strong> ${(creator.privacy_level_options || []).join(', ')}<br>
<strong>Duet:</strong> ${creator.duet_disabled ? 'Disabled' : 'Enabled'}<br>
<strong>Stitch:</strong> ${creator.stitch_disabled ? 'Disabled' : 'Enabled'}<br>
<strong>Comments:</strong> ${creator.comment_disabled ? 'Disabled' : 'Enabled'}
`);
await sleep(2500);
// Step 4: Publish
addMessage('user', 'Publish this video to my TikTok: https://squaremcp.com/squaremcp-tiktok-launch.mp4');
await sleep(800);
addTyping();
await sleep(1500);
removeTyping();
addMessage('assistant', `
I'll publish that video for you.<br><br>
<div class="video-preview">
<video src="https://squaremcp.com/squaremcp-tiktok-launch.mp4" muted loop autoplay playsinline></video>
</div>
<br>
<em>Publishing with privacy: ${creator.privacy_level_options?.[0] || 'SELF_ONLY'} (sandbox restriction)</em>
`);
await sleep(2500);
addTyping();
await sleep(2000);
removeTyping();
const publish = await apiPost('/api/tiktok/video', {
video_url: 'https://squaremcp.com/squaremcp-tiktok-launch.mp4',
title: 'SquareMCP TikTok Launch 🚀',
account: 'tiktok'
});
addMessage('assistant', `
<span class="success">✅ Video published!</span><br>
<strong>Publish ID:</strong> <code>${publish.publish_id}</code><br>
<strong>Status:</strong> ${publish.status}
`);
await sleep(2000);
// Step 5: Status check
addMessage('user', 'Has the video finished processing?');
await sleep(800);
addTyping();
await sleep(2000);
removeTyping();
const status = await apiPost('/api/tiktok/video/status', {
publish_id: publish.publish_id,
account: 'tiktok'
});
addMessage('assistant', `
<div class="status-badge">${status.status}</div><br><br>
Your video has been successfully published to TikTok! You can view it in your TikTok app under your profile.
`);
await sleep(2000);
// End
addMessage('user', 'Thanks!');
await sleep(600);
addTyping();
await sleep(1000);
removeTyping();
addMessage('assistant', `
You're welcome! 🎉<br><br>
With SquareMCP, you can manage all your social media — TikTok, LinkedIn, Instagram, Facebook, Twitter/X, and more — directly from chat.
`);
}
runDemo();
</script>
</body>
</html>

View File

@@ -0,0 +1 @@
tiktok-developers-site-verification=IIxGZsCAoYNJVObSA8cYj8FyWrAu1nIW

View File

@@ -0,0 +1 @@
tiktok-developers-site-verification=ebJaRSVgFOD0hQE1YXb5XhZ3IvKw4LVn

View File

@@ -0,0 +1 @@
tiktok-developers-site-verification=wJMUoUxgRBFO9T8p08qlMgtsez0Ktr9y

View File

@@ -0,0 +1 @@
tiktok-developers-site-verification=kFNJHjzDuzvGIlXnK4MaGw3MSluybOih