Add Auth0 authentication and user dashboard
- Add Auth0 SPA SDK integration for authentication - Create login/logout flow with Auth0 - Add user dashboard page with: - Profile display and editing - Credits balance and transaction history - API key management (create/revoke) - Account settings - Add API client for backend communication - Update navbar with Login/Dashboard buttons - Preserve existing landing page design Configuration: - Auth0 Domain: dev-t13zhs74oltgqtfxf.auth0.com - API base: /api (proxied through Caddy)
This commit is contained in:
parent
2ea2f28143
commit
2e693de7fa
488
dashboard.html
Normal file
488
dashboard.html
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dashboard • Moxiegen</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;600&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--dark-background: #0F172A;
|
||||||
|
--dark-surface: #1E293B;
|
||||||
|
--dark-primary-text: #F8FAFC;
|
||||||
|
--dark-secondary-text: #94A3B8;
|
||||||
|
--dark-accent-primary: #38BDF8;
|
||||||
|
--dark-accent-neural: #A5B4FC;
|
||||||
|
--dark-border: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--dark-background);
|
||||||
|
color: var(--dark-primary-text);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient {
|
||||||
|
background: linear-gradient(135deg, var(--dark-accent-primary), var(--dark-accent-neural));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--dark-accent-primary), var(--dark-accent-neural));
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 30px -10px rgba(56, 189, 248, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--dark-surface);
|
||||||
|
border: 1px solid var(--dark-border);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
border-color: var(--dark-accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-skeleton {
|
||||||
|
background: linear-gradient(90deg, var(--dark-surface) 0%, var(--dark-border) 50%, var(--dark-surface) 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeleton 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes skeleton {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-mask {
|
||||||
|
font-family: monospace;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: var(--dark-accent-primary);
|
||||||
|
color: var(--dark-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
animation: slideIn 0.3s ease, fadeOut 0.3s ease 2.7s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen">
|
||||||
|
<!-- Navigation -->
|
||||||
|
<nav class="border-b border-slate-800 bg-slate-950/80 backdrop-blur-lg sticky top-0 z-50">
|
||||||
|
<div class="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<a href="index.html">
|
||||||
|
<img src="logo.svg" alt="Moxiegen" class="h-10 w-auto" onerror="this.src='logo.png'">
|
||||||
|
</a>
|
||||||
|
<span class="font-semibold text-2xl" style="font-family: 'Space Grotesk', sans-serif;">Moxiegen</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div id="userInfo" class="flex items-center gap-3">
|
||||||
|
<div class="text-right">
|
||||||
|
<div id="userName" class="font-medium"></div>
|
||||||
|
<div id="userEmail" class="text-sm text-slate-400"></div>
|
||||||
|
</div>
|
||||||
|
<div id="userAvatar" class="w-10 h-10 rounded-full bg-gradient-to-br from-[#38BDF8] to-[#A5B4FC] flex items-center justify-center font-semibold text-slate-950"></div>
|
||||||
|
</div>
|
||||||
|
<button onclick="logout()" class="px-4 py-2 text-sm font-medium text-slate-400 hover:text-white border border-slate-700 hover:border-slate-500 rounded-lg transition-all">
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="max-w-7xl mx-auto px-6 py-8">
|
||||||
|
<!-- Welcome Section -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-3xl font-semibold mb-2">Welcome back, <span id="welcomeName" class="text-gradient">User</span></h1>
|
||||||
|
<p class="text-slate-400">Manage your API keys, credits, and account settings.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||||
|
<!-- Credits -->
|
||||||
|
<div class="card rounded-2xl p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<span class="text-slate-400 text-sm font-medium uppercase tracking-wider">Credits</span>
|
||||||
|
<span class="text-2xl">💰</span>
|
||||||
|
</div>
|
||||||
|
<div id="creditsBalance" class="text-4xl font-bold text-gradient">--</div>
|
||||||
|
<p class="text-slate-400 text-sm mt-2">Available balance</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Keys -->
|
||||||
|
<div class="card rounded-2xl p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<span class="text-slate-400 text-sm font-medium uppercase tracking-wider">API Keys</span>
|
||||||
|
<span class="text-2xl">🔑</span>
|
||||||
|
</div>
|
||||||
|
<div id="apiKeyCount" class="text-4xl font-bold text-gradient">--</div>
|
||||||
|
<p class="text-slate-400 text-sm mt-2">Active keys</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Subscription -->
|
||||||
|
<div class="card rounded-2xl p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<span class="text-slate-400 text-sm font-medium uppercase tracking-wider">Plan</span>
|
||||||
|
<span class="text-2xl">⚡</span>
|
||||||
|
</div>
|
||||||
|
<div id="subscriptionPlan" class="text-4xl font-bold text-gradient capitalize">--</div>
|
||||||
|
<p id="subscriptionStatus" class="text-slate-400 text-sm mt-2">--</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Panels -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<!-- API Keys Panel -->
|
||||||
|
<div class="card rounded-2xl p-6">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h2 class="text-xl font-semibold">API Keys</h2>
|
||||||
|
<button onclick="createApiKey()" class="btn-primary px-4 py-2 text-sm font-medium text-slate-950 rounded-lg flex items-center gap-2">
|
||||||
|
<span>+</span> Create Key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="apiKeysList" class="space-y-3">
|
||||||
|
<div class="loading-skeleton h-16 rounded-lg"></div>
|
||||||
|
<div class="loading-skeleton h-16 rounded-lg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="noApiKeys" class="hidden text-center py-8 text-slate-400">
|
||||||
|
<p class="mb-4">No API keys yet</p>
|
||||||
|
<button onclick="createApiKey()" class="text-[#38BDF8] hover:underline">Create your first API key</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Activity Panel -->
|
||||||
|
<div class="card rounded-2xl p-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-6">Recent Activity</h2>
|
||||||
|
|
||||||
|
<div id="activityList" class="space-y-3">
|
||||||
|
<div class="loading-skeleton h-14 rounded-lg"></div>
|
||||||
|
<div class="loading-skeleton h-14 rounded-lg"></div>
|
||||||
|
<div class="loading-skeleton h-14 rounded-lg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="noActivity" class="hidden text-center py-8 text-slate-400">
|
||||||
|
<p>No recent activity</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Account Settings -->
|
||||||
|
<div class="card rounded-2xl p-6 mt-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-6">Account Settings</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-slate-400 mb-2">Display Name</label>
|
||||||
|
<input type="text" id="settingsName" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 outline-none focus:border-[#38BDF8] transition-colors" placeholder="Your name">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-slate-400 mb-2">Email</label>
|
||||||
|
<input type="email" id="settingsEmail" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 outline-none focus:border-[#38BDF8] transition-colors" disabled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4 mt-6">
|
||||||
|
<button onclick="saveSettings()" class="btn-primary px-6 py-2 text-sm font-medium text-slate-950 rounded-lg">
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
<button onclick="deactivateAccount()" class="px-6 py-2 text-sm font-medium text-red-400 hover:text-red-300 border border-red-400/30 hover:border-red-400 rounded-lg transition-colors">
|
||||||
|
Deactivate Account
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Toast Container -->
|
||||||
|
<div id="toastContainer" class="fixed bottom-6 right-6 z-50 space-y-2"></div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="js/config.js"></script>
|
||||||
|
<script src="js/auth.js"></script>
|
||||||
|
<script src="js/api.js"></script>
|
||||||
|
<script>
|
||||||
|
// Initialize on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
try {
|
||||||
|
// Initialize auth
|
||||||
|
await moxieAuth.init();
|
||||||
|
|
||||||
|
// Check if authenticated
|
||||||
|
if (!moxieAuth.isAuthenticated) {
|
||||||
|
// Redirect to login
|
||||||
|
await moxieAuth.login(window.location.href);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load user data
|
||||||
|
await loadDashboard();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Initialization error:', error);
|
||||||
|
showToast('Failed to initialize. Please refresh.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadDashboard() {
|
||||||
|
try {
|
||||||
|
// Get user profile from API
|
||||||
|
const profileResponse = await moxieAPI.getProfile();
|
||||||
|
const user = profileResponse.data.user;
|
||||||
|
|
||||||
|
// Update UI with user info
|
||||||
|
updateUserUI(user);
|
||||||
|
|
||||||
|
// Load credits
|
||||||
|
const creditsResponse = await moxieAPI.getCredits(1, 5);
|
||||||
|
document.getElementById('creditsBalance').textContent = creditsResponse.data.credits.toLocaleString();
|
||||||
|
|
||||||
|
// Update activity list
|
||||||
|
updateActivityList(creditsResponse.data.transactions);
|
||||||
|
|
||||||
|
// Load API keys
|
||||||
|
const keysResponse = await moxieAPI.getApiKeys();
|
||||||
|
document.getElementById('apiKeyCount').textContent = keysResponse.data.keys.length;
|
||||||
|
updateApiKeysList(keysResponse.data.keys);
|
||||||
|
|
||||||
|
// Update subscription
|
||||||
|
document.getElementById('subscriptionPlan').textContent = user.subscription_tier || 'Free';
|
||||||
|
document.getElementById('subscriptionStatus').textContent = user.subscription_status || 'No active subscription';
|
||||||
|
|
||||||
|
// Update settings form
|
||||||
|
document.getElementById('settingsName').value = user.name || '';
|
||||||
|
document.getElementById('settingsEmail').value = user.email || '';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load dashboard:', error);
|
||||||
|
showToast('Failed to load dashboard data', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUserUI(user) {
|
||||||
|
document.getElementById('userName').textContent = user.name || 'User';
|
||||||
|
document.getElementById('userEmail').textContent = user.email || '';
|
||||||
|
document.getElementById('welcomeName').textContent = user.name || 'User';
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
const avatar = document.getElementById('userAvatar');
|
||||||
|
if (user.picture) {
|
||||||
|
avatar.innerHTML = `<img src="${user.picture}" class="w-full h-full rounded-full object-cover">`;
|
||||||
|
} else {
|
||||||
|
avatar.textContent = (user.name || 'U').charAt(0).toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateApiKeysList(keys) {
|
||||||
|
const container = document.getElementById('apiKeysList');
|
||||||
|
const noKeys = document.getElementById('noApiKeys');
|
||||||
|
|
||||||
|
if (keys.length === 0) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
noKeys.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
noKeys.classList.add('hidden');
|
||||||
|
|
||||||
|
container.innerHTML = keys.map(key => `
|
||||||
|
<div class="flex items-center justify-between bg-slate-800/50 rounded-lg p-4">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">${escapeHtml(key.name)}</div>
|
||||||
|
<div class="text-sm text-slate-400">
|
||||||
|
Created ${new Date(key.created_at).toLocaleDateString()}
|
||||||
|
${key.last_used ? ` • Last used ${new Date(key.last_used).toLocaleDateString()}` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onclick="revokeKey('${key.id}')" class="px-3 py-1 text-sm text-red-400 hover:text-red-300 hover:bg-red-400/10 rounded transition-colors">
|
||||||
|
Revoke
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateActivityList(transactions) {
|
||||||
|
const container = document.getElementById('activityList');
|
||||||
|
const noActivity = document.getElementById('noActivity');
|
||||||
|
|
||||||
|
if (!transactions || transactions.length === 0) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
noActivity.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
noActivity.classList.add('hidden');
|
||||||
|
|
||||||
|
container.innerHTML = transactions.slice(0, 5).map(tx => `
|
||||||
|
<div class="flex items-center justify-between bg-slate-800/50 rounded-lg p-4">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">${escapeHtml(tx.description || 'Credit transaction')}</div>
|
||||||
|
<div class="text-sm text-slate-400">${new Date(tx.created_at).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
<div class="font-mono ${tx.amount > 0 ? 'text-green-400' : 'text-red-400'}">
|
||||||
|
${tx.amount > 0 ? '+' : ''}${tx.amount}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createApiKey() {
|
||||||
|
const name = prompt('Enter a name for this API key:', 'My API Key');
|
||||||
|
if (!name) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await moxieAPI.createApiKey(name);
|
||||||
|
const key = response.data.key;
|
||||||
|
|
||||||
|
// Show the key (only time it will be visible)
|
||||||
|
showToast('API key created! Copy it now - it won\'t be shown again.', 'success');
|
||||||
|
|
||||||
|
// Show modal with key
|
||||||
|
showApiKeyModal(key);
|
||||||
|
|
||||||
|
// Refresh list
|
||||||
|
const keysResponse = await moxieAPI.getApiKeys();
|
||||||
|
document.getElementById('apiKeyCount').textContent = keysResponse.data.keys.length;
|
||||||
|
updateApiKeysList(keysResponse.data.keys);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showToast('Failed to create API key: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showApiKeyModal(key) {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="bg-slate-800 rounded-2xl p-6 max-w-lg w-full">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">API Key Created</h3>
|
||||||
|
<p class="text-slate-400 mb-4">Copy this key now. It won't be shown again.</p>
|
||||||
|
<div class="bg-slate-900 rounded-lg p-4 mb-4">
|
||||||
|
<code class="text-[#38BDF8] break-all text-sm">${escapeHtml(key.key)}</code>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button onclick="navigator.clipboard.writeText('${escapeHtml(key.key)}'); this.textContent='Copied!';"
|
||||||
|
class="btn-primary px-4 py-2 text-sm font-medium text-slate-950 rounded-lg">
|
||||||
|
Copy to Clipboard
|
||||||
|
</button>
|
||||||
|
<button onclick="this.closest('.fixed').remove()"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-slate-400 hover:text-white border border-slate-700 rounded-lg">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) modal.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revokeKey(keyId) {
|
||||||
|
if (!confirm('Are you sure you want to revoke this API key? This action cannot be undone.')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await moxieAPI.revokeApiKey(keyId);
|
||||||
|
showToast('API key revoked', 'success');
|
||||||
|
|
||||||
|
// Refresh list
|
||||||
|
const keysResponse = await moxieAPI.getApiKeys();
|
||||||
|
document.getElementById('apiKeyCount').textContent = keysResponse.data.keys.length;
|
||||||
|
updateApiKeysList(keysResponse.data.keys);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showToast('Failed to revoke API key: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSettings() {
|
||||||
|
const name = document.getElementById('settingsName').value.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await moxieAPI.updateProfile({ name });
|
||||||
|
showToast('Settings saved', 'success');
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
document.getElementById('userName').textContent = name || 'User';
|
||||||
|
document.getElementById('welcomeName').textContent = name || 'User';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showToast('Failed to save settings: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deactivateAccount() {
|
||||||
|
if (!confirm('Are you sure you want to deactivate your account? This action cannot be undone.')) return;
|
||||||
|
if (!confirm('This will permanently delete your account and all associated data. Continue?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await moxieAPI.deactivateAccount();
|
||||||
|
showToast('Account deactivated', 'success');
|
||||||
|
await logout();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showToast('Failed to deactivate account: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
try {
|
||||||
|
await moxieAuth.logout();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
window.location.href = 'index.html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
const container = document.getElementById('toastContainer');
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
|
||||||
|
const bgColor = type === 'error' ? 'bg-red-500/20 border-red-500/50' :
|
||||||
|
type === 'success' ? 'bg-green-500/20 border-green-500/50' :
|
||||||
|
'bg-slate-800 border-slate-700';
|
||||||
|
|
||||||
|
toast.className = `toast ${bgColor} border rounded-lg px-4 py-3 text-sm max-w-sm`;
|
||||||
|
toast.textContent = message;
|
||||||
|
|
||||||
|
container.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => toast.remove(), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
61
index.html
61
index.html
@ -724,21 +724,35 @@
|
|||||||
<nav id="navbar" class="border-b border-slate-800 bg-slate-950/80 backdrop-blur-lg sticky top-0 z-50">
|
<nav id="navbar" class="border-b border-slate-800 bg-slate-950/80 backdrop-blur-lg sticky top-0 z-50">
|
||||||
<div class="max-w-screen-2xl mx-auto px-8 py-5 flex items-center justify-between">
|
<div class="max-w-screen-2xl mx-auto px-8 py-5 flex items-center justify-between">
|
||||||
<!-- Logo + Brand -->
|
<!-- Logo + Brand -->
|
||||||
<div class="flex items-center gap-x-3">
|
<a href="index.html" class="flex items-center gap-x-3">
|
||||||
<img src="logo.svg"
|
<img src="logo.svg"
|
||||||
alt="Moxiegen"
|
alt="Moxiegen"
|
||||||
class="h-11 w-auto logo-glow"
|
class="h-11 w-auto logo-glow"
|
||||||
onerror="this.onerror=null; this.src='logo.png';">
|
onerror="this.onerror=null; this.src='logo.png';">
|
||||||
<span class="font-semibold text-3xl tracking-[-1px] text-white" style="font-family: 'Space Grotesk', sans-serif;">Moxiegen</span>
|
<span class="font-semibold text-3xl tracking-[-1px] text-white" style="font-family: 'Space Grotesk', sans-serif;">Moxiegen</span>
|
||||||
</div>
|
</a>
|
||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<div class="flex items-center gap-x-8 text-sm font-medium">
|
<div class="flex items-center gap-x-8 text-sm font-medium">
|
||||||
<a href="#benefits" class="nav-link hover:text-[#38BDF8]">How It Works</a>
|
<a href="#benefits" class="nav-link hover:text-[#38BDF8]">How It Works</a>
|
||||||
<a href="#moxy" class="nav-link hover:text-[#38BDF8]">See Moxy Go</a>
|
<a href="#moxy" class="nav-link hover:text-[#38BDF8]">See Moxy Go</a>
|
||||||
<a href="#licensing" class="nav-link hover:text-[#38BDF8]">Licensing</a>
|
<a href="#licensing" class="nav-link hover:text-[#38BDF8]">Licensing</a>
|
||||||
|
|
||||||
|
<!-- Auth buttons -->
|
||||||
|
<div id="authButtons" class="flex items-center gap-x-4">
|
||||||
|
<!-- Login button (shown when not authenticated) -->
|
||||||
|
<button id="loginBtn" onclick="login()" class="hidden ripple px-6 py-2.5 bg-white text-slate-950 font-semibold rounded-3xl hover:bg-[#38BDF8] hover:text-white btn-primary">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Dashboard button (shown when authenticated) -->
|
||||||
|
<a id="dashboardBtn" href="dashboard.html" class="hidden ripple px-6 py-2.5 bg-white text-slate-950 font-semibold rounded-3xl hover:bg-[#38BDF8] hover:text-white btn-primary">
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button onclick="document.getElementById('contact').scrollIntoView({ behavior: 'smooth' })"
|
<button onclick="document.getElementById('contact').scrollIntoView({ behavior: 'smooth' })"
|
||||||
class="ripple px-6 py-2.5 bg-white text-slate-950 font-semibold rounded-3xl hover:bg-[#38BDF8] hover:text-white btn-primary">
|
class="ripple px-6 py-2.5 border border-white/30 hover:border-[#38BDF8] font-semibold rounded-3xl">
|
||||||
Contact Us
|
Contact Us
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -1196,7 +1210,48 @@
|
|||||||
initTiltEffect();
|
initTiltEffect();
|
||||||
initSmoothScroll();
|
initSmoothScroll();
|
||||||
animateCounters();
|
animateCounters();
|
||||||
|
initAuth();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// AUTHENTICATION
|
||||||
|
// ============================================
|
||||||
|
async function initAuth() {
|
||||||
|
try {
|
||||||
|
await moxieAuth.init();
|
||||||
|
updateAuthUI();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth init error:', error);
|
||||||
|
// Show login button on error
|
||||||
|
document.getElementById('loginBtn').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAuthUI() {
|
||||||
|
const loginBtn = document.getElementById('loginBtn');
|
||||||
|
const dashboardBtn = document.getElementById('dashboardBtn');
|
||||||
|
|
||||||
|
if (moxieAuth.isAuthenticated) {
|
||||||
|
loginBtn.classList.add('hidden');
|
||||||
|
dashboardBtn.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
loginBtn.classList.remove('hidden');
|
||||||
|
dashboardBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
try {
|
||||||
|
await moxieAuth.login(window.location.origin + '/dashboard.html');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
alert('Failed to login. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Auth Scripts -->
|
||||||
|
<script src="js/config.js"></script>
|
||||||
|
<script src="js/auth.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
177
js/api.js
Normal file
177
js/api.js
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
/**
|
||||||
|
* Moxiegen API Client
|
||||||
|
* Handles all API calls to the backend with authentication
|
||||||
|
*/
|
||||||
|
|
||||||
|
class MoxieAPI {
|
||||||
|
constructor() {
|
||||||
|
this.baseUrl = CONFIG.api.baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make authenticated API request
|
||||||
|
*/
|
||||||
|
async request(endpoint, options = {}) {
|
||||||
|
const token = await moxieAuth.getToken();
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = endpoint.startsWith('http') ? endpoint : this.baseUrl + endpoint;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(data.message || 'Request failed', response.status, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof APIError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new APIError(error.message, 0, { error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET request
|
||||||
|
*/
|
||||||
|
async get(endpoint) {
|
||||||
|
return this.request(endpoint, { method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST request
|
||||||
|
*/
|
||||||
|
async post(endpoint, data) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT request
|
||||||
|
*/
|
||||||
|
async put(endpoint, data) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE request
|
||||||
|
*/
|
||||||
|
async delete(endpoint, data) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'DELETE',
|
||||||
|
body: data ? JSON.stringify(data) : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// User Endpoints
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user profile
|
||||||
|
*/
|
||||||
|
async getProfile() {
|
||||||
|
return this.get(CONFIG.api.endpoints.me);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user profile
|
||||||
|
*/
|
||||||
|
async updateProfile(data) {
|
||||||
|
return this.put(CONFIG.api.endpoints.me, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivate account
|
||||||
|
*/
|
||||||
|
async deactivateAccount() {
|
||||||
|
return this.delete(CONFIG.api.endpoints.me);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Credits Endpoints
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get credits balance and history
|
||||||
|
*/
|
||||||
|
async getCredits(page = 1, limit = 10) {
|
||||||
|
return this.get(`${CONFIG.api.endpoints.credits}?page=${page}&limit=${limit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// API Keys Endpoints
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all API keys
|
||||||
|
*/
|
||||||
|
async getApiKeys() {
|
||||||
|
return this.get(CONFIG.api.endpoints.apiKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new API key
|
||||||
|
*/
|
||||||
|
async createApiKey(name = 'API Key') {
|
||||||
|
return this.post(CONFIG.api.endpoints.apiKeys, { name });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke API key
|
||||||
|
*/
|
||||||
|
async revokeApiKey(keyId) {
|
||||||
|
return this.delete(`${CONFIG.api.endpoints.apiKeys}/${keyId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Health Check
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check API health
|
||||||
|
*/
|
||||||
|
async checkHealth() {
|
||||||
|
return this.get(CONFIG.api.endpoints.health);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom API Error class
|
||||||
|
*/
|
||||||
|
class APIError extends Error {
|
||||||
|
constructor(message, status, data) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'APIError';
|
||||||
|
this.status = status;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create singleton instance
|
||||||
|
const moxieAPI = new MoxieAPI();
|
||||||
|
|
||||||
|
// Export for use in other modules
|
||||||
|
window.moxieAPI = moxieAPI;
|
||||||
|
window.APIError = APIError;
|
||||||
176
js/auth.js
Normal file
176
js/auth.js
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
/**
|
||||||
|
* Moxiegen Authentication Module
|
||||||
|
* Handles Auth0 authentication, login/logout, and token management
|
||||||
|
*/
|
||||||
|
|
||||||
|
class MoxieAuth {
|
||||||
|
constructor() {
|
||||||
|
this.auth0Client = null;
|
||||||
|
this.isAuthenticated = false;
|
||||||
|
this.user = null;
|
||||||
|
this.token = null;
|
||||||
|
this.isInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Auth0 client
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
if (this.isInitialized) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Dynamically load Auth0 SPA SDK if not already loaded
|
||||||
|
if (typeof auth0 === 'undefined') {
|
||||||
|
await this.loadScript('https://cdn.auth0.com/js/auth0-spa-js/2.0/auth0-spa-js.production.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Auth0 client
|
||||||
|
this.auth0Client = await auth0.createAuth0Client({
|
||||||
|
domain: CONFIG.auth0.domain,
|
||||||
|
clientId: CONFIG.auth0.clientId,
|
||||||
|
authorizationParams: {
|
||||||
|
audience: CONFIG.auth0.audience,
|
||||||
|
redirect_uri: CONFIG.auth0.redirectUri
|
||||||
|
},
|
||||||
|
cacheLocation: 'localstorage',
|
||||||
|
useRefreshTokens: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if coming back from Auth0 redirect
|
||||||
|
const query = window.location.search;
|
||||||
|
if (query.includes('code=') && query.includes('state=')) {
|
||||||
|
await this.auth0Client.handleRedirectCallback();
|
||||||
|
// Remove query params from URL
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check authentication status
|
||||||
|
this.isAuthenticated = await this.auth0Client.isAuthenticated();
|
||||||
|
|
||||||
|
if (this.isAuthenticated) {
|
||||||
|
this.user = await this.auth0Client.getUser();
|
||||||
|
this.token = await this.auth0Client.getTokenSilently();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
console.log('Auth0 initialized. Authenticated:', this.isAuthenticated);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize Auth0:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load external script
|
||||||
|
*/
|
||||||
|
loadScript(src) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = src;
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = reject;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login - redirect to Auth0
|
||||||
|
*/
|
||||||
|
async login(returnTo = null) {
|
||||||
|
if (!this.auth0Client) {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.auth0Client.loginWithRedirect({
|
||||||
|
authorizationParams: {
|
||||||
|
audience: CONFIG.auth0.audience,
|
||||||
|
redirect_uri: returnTo || CONFIG.auth0.redirectUri
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout
|
||||||
|
*/
|
||||||
|
async logout() {
|
||||||
|
if (!this.auth0Client) {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear local state
|
||||||
|
this.isAuthenticated = false;
|
||||||
|
this.user = null;
|
||||||
|
this.token = null;
|
||||||
|
|
||||||
|
// Logout from Auth0
|
||||||
|
await this.auth0Client.logout({
|
||||||
|
logoutParams: {
|
||||||
|
returnTo: CONFIG.auth0.logoutUri
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get access token (silently refreshes if needed)
|
||||||
|
*/
|
||||||
|
async getToken() {
|
||||||
|
if (!this.auth0Client) {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isAuthenticated) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.token = await this.auth0Client.getTokenSilently();
|
||||||
|
return this.token;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get token:', error);
|
||||||
|
// Token might be expired, try to re-authenticate
|
||||||
|
this.isAuthenticated = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user
|
||||||
|
*/
|
||||||
|
async getUser() {
|
||||||
|
if (!this.auth0Client) {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
return this.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is authenticated
|
||||||
|
*/
|
||||||
|
async checkAuth() {
|
||||||
|
if (!this.auth0Client) {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
this.isAuthenticated = await this.auth0Client.isAuthenticated();
|
||||||
|
if (this.isAuthenticated) {
|
||||||
|
this.user = await this.auth0Client.getUser();
|
||||||
|
this.token = await this.auth0Client.getTokenSilently();
|
||||||
|
}
|
||||||
|
return this.isAuthenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has specific role
|
||||||
|
*/
|
||||||
|
hasRole(role) {
|
||||||
|
if (!this.user) return false;
|
||||||
|
const roles = this.user['https://moxiegen.client.guacamolebox.net/roles'] || [];
|
||||||
|
return roles.includes(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create singleton instance
|
||||||
|
const moxieAuth = new MoxieAuth();
|
||||||
|
|
||||||
|
// Export for use in other modules
|
||||||
|
window.moxieAuth = moxieAuth;
|
||||||
38
js/config.js
Normal file
38
js/config.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Moxiegen Frontend Configuration
|
||||||
|
* Update these values to match your Auth0 and API settings
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
// Auth0 Configuration
|
||||||
|
auth0: {
|
||||||
|
domain: 'dev-t13zhs74oltgqtfxf.auth0.com',
|
||||||
|
clientId: 'YOUR_CLIENT_ID', // Replace with your Auth0 Client ID
|
||||||
|
audience: 'https://dev-t13zhs74oltgqtfxf.auth0.com/api/v2/',
|
||||||
|
redirectUri: window.location.origin + '/dashboard.html',
|
||||||
|
logoutUri: window.location.origin
|
||||||
|
},
|
||||||
|
|
||||||
|
// API Configuration
|
||||||
|
api: {
|
||||||
|
baseUrl: '/api', // Proxied through Caddy
|
||||||
|
endpoints: {
|
||||||
|
me: '/users/me',
|
||||||
|
credits: '/users/credits',
|
||||||
|
apiKeys: '/users/api-keys',
|
||||||
|
health: '/health'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// App Configuration
|
||||||
|
app: {
|
||||||
|
name: 'Moxiegen',
|
||||||
|
version: '2.0.0'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Freeze config to prevent modifications
|
||||||
|
Object.freeze(CONFIG);
|
||||||
|
Object.freeze(CONFIG.auth0);
|
||||||
|
Object.freeze(CONFIG.api);
|
||||||
|
Object.freeze(CONFIG.app);
|
||||||
Loading…
Reference in New Issue
Block a user