/* Auth, roles, branding + Login ===================================== "Relay" — multi-platform ad command. Authentication and all team / connection data now live in the MySQL backend (see /api). This file keeps the role matrix (mirrored server-side), the login screen, and shared identity UI. Theme + branding still persist to localStorage (cosmetic only). =================================================================== */ /* ---------- localStorage (cosmetic prefs only) -------------------- */ const LS = { theme: 'relay_theme_v1', brand: 'relay_brand_v1', }; function loadLS(key, fallback){ try { const v = localStorage.getItem(key); return v ? JSON.parse(v) : fallback; } catch(e){ return fallback; } } function saveLS(key, val){ try { localStorage.setItem(key, JSON.stringify(val)); } catch(e){} } /* ---------- Roles & permissions (UI mirror of server rbac.php) ----- */ const ROLES = { super_admin: { label:'Super Admin', tone:'#8351ff', rank:5, blurb:'Full control. Manage users, roles, integrations and every account.', perms:['*'], }, admin: { label:'Admin', tone:'#2f7bff', rank:4, blurb:'Manage integrations & accounts and act on campaigns. Cannot manage users.', perms:['integrations.manage','accounts.manage','campaigns.edit','automations.manage','reallocate.apply','columns.build','settings.manage','mediaplan.manage','reports.build','view'], }, manager: { label:'Manager', tone:'#10b981', rank:3, blurb:'Edit campaigns, run automations and reallocate budget on assigned accounts.', perms:['campaigns.edit','automations.manage','reallocate.apply','columns.build','reports.build','view'], }, analyst: { label:'Analyst', tone:'#06b6d4', rank:2, blurb:'View performance and build reports / custom columns. No live edits.', perms:['columns.build','reports.build','view'], }, viewer: { label:'Viewer', tone:'#8b8b80', rank:1, blurb:'Read-only access to assigned accounts.', perms:['view'], }, reporter: { label:'Reporter', tone:'#f59e0b', rank:1, blurb:'Report Builder only — build, export and import report data for assigned accounts. No dashboards or settings.', perms:['reports.build','data.import'], }, }; const ROLE_ORDER = ['super_admin','admin','manager','analyst','viewer','reporter']; function can(user, perm){ if (!user || user.status === 'suspended') return false; const perms = (ROLES[user.role] || {}).perms || []; return perms.includes('*') || perms.includes(perm); } /* Accounts a user may see. '*' = all. */ function accessibleAccounts(user){ if (!user) return []; if (user.accounts === '*' || (Array.isArray(user.accounts) && user.accounts.includes('*'))) return ACCOUNTS; return ACCOUNTS.filter(a => (user.accounts||[]).includes(a.id)); } const AVATAR_TONES = ['#8351ff','#2f7bff','#10b981','#06b6d4','#f59e0b','#ec4899','#ef4444','#6366f1']; /* ---------- Audit metadata (icons/verbs for the activity log) ------ */ const AUDIT_META = { 'auth.signin': { icon:'zap', tone:'#06b6d4', verb:'signed in' }, 'auth.signout': { icon:'link-break', tone:'#8b8b80', verb:'signed out' }, 'user.create': { icon:'plus', tone:'#10b981', verb:'invited' }, 'user.update': { icon:'settings', tone:'#2f7bff', verb:'updated' }, 'user.delete': { icon:'x', tone:'#ef4444', verb:'removed' }, 'account.create': { icon:'plus', tone:'#10b981', verb:'added account' }, 'account.update': { icon:'settings', tone:'#2f7bff', verb:'updated account' }, 'account.delete': { icon:'x', tone:'#ef4444', verb:'removed account' }, 'integration.update': { icon:'settings', tone:'#8351ff', verb:'updated connection' }, 'integration.connect': { icon:'zap', tone:'#10b981', verb:'connected' }, 'integration.disconnect': { icon:'link-break', tone:'#f59e0b', verb:'disconnected' }, 'integration.test': { icon:'play', tone:'#06b6d4', verb:'tested' }, 'data.import': { icon:'plus', tone:'#10b981', verb:'imported data to' }, }; /* ---------- Branding (cosmetic, localStorage) --------------------- */ const BRAND_DEFAULT = { name:'Relay', tag:'MCP', accent:'#8351ff', logo:null }; function loadBranding(){ return { ...BRAND_DEFAULT, ...(loadLS(LS.brand, {})||{}) }; } function saveBranding(b){ saveLS(LS.brand, b); return b; } /* Which platforms have a live (connected) feed, per integrations. */ function connectedPlatforms(integrations){ const set = {}; (integrations||[]).forEach(i => { if (i.status === 'connected') (i.platforms||[]).forEach(p => { set[p] = i.health || 'healthy'; }); }); return set; } /* ---------- Avatar ------------------------------------------------- */ function initialsOf(name){ return String(name||'?').trim().split(/\s+/).map(s=>s[0]).slice(0,2).join('').toUpperCase(); } function Avatar({ user, size=36, ring=false }){ if (!user) return null; const tone = user.tone || '#8351ff'; return ( {initialsOf(user.name)} {user.status==='suspended' && ( )} ); } function RoleBadge({ role, size='md' }){ const r = ROLES[role] || ROLES.viewer; const cls = size==='sm' ? 'text-[10px] px-1.5 py-0.5' : 'text-[11px] px-2 py-0.5'; return ( {r.label} ); } /* =================================================================== Login screen =================================================================== */ function RelayMark({ size=34, tone='#8351ff' }){ const s = size; return ( ); } function BrandLogo({ brand, size=32 }){ const b = brand || loadBranding(); if (b.logo) return logo; return ; } function Wordmark({ light=false, size=18, brand }){ const b = brand || loadBranding(); const accent = b.accent || '#8351ff'; return ( {b.name || 'Relay'} {b.tag && {b.tag} } ); } /* onSignIn(email, password, remember) → Promise (rejects with {message} on failure). */ function LoginScreen({ onSignIn, brand }){ const _brand = brand || loadBranding(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [show, setShow] = useState(false); const [error, setError] = useState(''); const [busy, setBusy] = useState(false); const [remember, setRemember] = useState(true); const submit = (e) => { e && e.preventDefault(); setError(''); if (!email.trim() || !password) { setError('Enter your email and password.'); return; } setBusy(true); Promise.resolve(onSignIn(email.trim(), password, remember)) .catch(err => { setError((err && err.message) || 'Sign-in failed. Try again.'); }) .finally(() => setBusy(false)); }; return (
{/* Left — brand / atmosphere */}
MCP PIPELINE · LIVE

Every ad platform,
wired through
one relay.

Meta, Google, TikTok, Snap and X stream into a single command surface — via MCP, direct API, or manual import. Drill, reallocate and automate without leaving the page.

{['facebook','instagram','google','ga','tiktok','snapchat','x'].map((g,i)=>( ))}
{_brand.name} · secured workspace
{/* Right — auth card */}

Sign in to your workspace

Authenticate to open the live dashboard.

{error && (
{error}
)}
Forgot password?
Secured workspace · access provisioned by your admin
); } Object.assign(window, { LS, loadLS, saveLS, ROLES, ROLE_ORDER, AVATAR_TONES, can, accessibleAccounts, AUDIT_META, connectedPlatforms, initialsOf, Avatar, RoleBadge, RelayMark, Wordmark, BrandLogo, LoginScreen, loadBranding, saveBranding, BRAND_DEFAULT, });