feat: v3.1.0 — Live Platform Foundation (Track E.1-E.3)

Major release introducing the Live Platform architecture:

- SQLite database backend with async operations and migrations (E.1)
- AES-256-GCM credential encryption with HKDF key derivation (E.1)
- OAuth Social Login with GitHub and Google (E.2)
- Site management API with encrypted credential storage (E.3)
- Per-user MCP endpoints at /u/{user_id}/{alias}/mcp (E.3)
- User API keys with bcrypt hashing (E.3)
- Auto-generated config snippets for 5 MCP clients (E.3)
- Dashboard: dark/light mode, RBAC, My Sites, Connect page
- Active background health checks
- 452 tests (up from 303), all passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-23 23:20:03 +03:30
parent 42ea75bcb3
commit 9e6f06d933
53 changed files with 8654 additions and 1820 deletions

View File

@@ -1,46 +1,61 @@
<!DOCTYPE html>
<html lang="{{ lang|default('en') }}" {% if lang == 'fa' %}dir="rtl"{% endif %}>
<html lang="{{ lang|default('en') }}" {% if lang=='fa' %}dir="rtl" {% endif %}>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ t.login_title }} - MCP Hub</title>
<link rel="icon" type="image/svg+xml" href="/static/logo.svg">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
{% include "dashboard/partials/head_assets.html" %}
<style>
.login-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.card-shadow {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.input-focus:focus {
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.3);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
}
</style>
</head>
<body class="bg-gray-900 min-h-screen flex items-center justify-center p-4">
<!-- Background decoration -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute -top-40 -right-40 w-80 h-80 bg-purple-500/20 rounded-full blur-3xl animate-float"></div>
<div class="absolute -bottom-40 -left-40 w-80 h-80 bg-blue-500/20 rounded-full blur-3xl animate-float" style="animation-delay: -3s;"></div>
<div class="absolute -bottom-40 -left-40 w-80 h-80 bg-blue-500/20 rounded-full blur-3xl animate-float"
style="animation-delay: -3s;"></div>
</div>
<!-- Login Card -->
@@ -49,20 +64,22 @@
<!-- Logo -->
<div class="text-center mb-8">
<div class="mx-auto w-16 h-16 login-gradient rounded-2xl flex items-center justify-center mb-4">
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
<img src="/static/logo.svg" alt="MCP Hub Logo"
class="w-10 h-10 object-contain drop-shadow-[0_0_8px_rgba(255,255,255,0.8)]">
</div>
<h1 class="text-2xl font-bold text-white">MCP Hub</h1>
<p class="text-gray-400 mt-2" {% if lang == 'fa' %}dir="rtl"{% endif %}>{{ t.login_subtitle }}</p>
<p class="text-gray-400 mt-2" {% if lang=='fa' %}dir="rtl" {% endif %}>{{ t.login_subtitle }}</p>
</div>
<!-- Error Message -->
{% if error %}
<div class="mb-6 p-4 bg-red-500/20 border border-red-500/50 rounded-lg" {% if lang == 'fa' %}dir="rtl"{% endif %}>
<div class="mb-6 p-4 bg-red-500/20 border border-red-500/50 rounded-lg" {% if lang=='fa' %}dir="rtl" {%
endif %}>
<div class="flex items-center">
<svg class="w-5 h-5 text-red-400 {% if lang == 'fa' %}ml-2{% else %}mr-2{% endif %}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
<svg class="w-5 h-5 text-red-400 {% if lang == 'fa' %}ml-2{% else %}mr-2{% endif %}" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-red-400 text-sm">
{% if error == 'rate_limit' %}
@@ -77,48 +94,49 @@
<!-- Login Form -->
<form method="POST" action="/dashboard/login" class="space-y-6">
<input type="hidden" name="csrf_token" value="{{ request.state.csrf_token }}">
<input type="hidden" name="next" value="{{ next_url }}">
<div {% if lang == 'fa' %}dir="rtl"{% endif %}>
<div {% if lang=='fa' %}dir="rtl" {% endif %}>
<label for="api_key" class="block text-sm font-medium text-gray-300 mb-2">
{{ t.api_key_label }}
</label>
<input
type="password"
id="api_key"
name="api_key"
required
autocomplete="off"
<input type="password" id="api_key" name="api_key" required autocomplete="off"
placeholder="{{ t.api_key_placeholder }}"
class="input-focus w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-purple-500 transition-colors"
{% if lang == 'fa' %}dir="ltr" style="text-align: right;"{% endif %}
>
{% if lang=='fa' %}dir="ltr" style="text-align: right;" {% endif %}>
</div>
<button
type="submit"
class="btn-primary w-full py-3 px-4 text-white font-semibold rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 focus:ring-offset-gray-800"
>
<button type="submit"
class="btn-primary w-full py-3 px-4 text-white font-semibold rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 focus:ring-offset-gray-800">
{{ t.login_button }}
</button>
</form>
<!-- Security Note -->
<div class="mt-6 p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg" {% if lang == 'fa' %}dir="rtl"{% endif %}>
<div class="mt-6 p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg" {% if lang=='fa' %}dir="rtl" {%
endif %}>
<div class="flex items-start">
<svg class="w-5 h-5 text-blue-400 {% if lang == 'fa' %}ml-2{% else %}mr-2{% endif %} mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
<svg class="w-5 h-5 text-blue-400 {% if lang == 'fa' %}ml-2{% else %}mr-2{% endif %} mt-0.5 flex-shrink-0"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div class="text-sm text-blue-300">
{% if lang == 'fa' %}
<p>برای دسترسی به داشبورد به Master API Key نیاز دارید.</p>
<p class="mt-2 text-blue-400">کلید API خود را در متغیر محیطی <code class="bg-gray-700 px-1 rounded">MASTER_API_KEY</code> تنظیم کنید.</p>
<p class="mt-2 text-blue-400">کلید API خود را در متغیر محیطی <code
class="bg-gray-700 px-1 rounded">MASTER_API_KEY</code> تنظیم کنید.</p>
{% else %}
<p>Enter your <code class="bg-gray-700 px-1 rounded">MASTER_API_KEY</code> to access the dashboard.</p>
<p class="mt-2 text-blue-400">Don't have one? Set it in your <code class="bg-gray-700 px-1 rounded">.env</code> file, or check the server logs for the temporary key.</p>
<p>Enter your <code class="bg-gray-700 px-1 rounded">MASTER_API_KEY</code> to access the
dashboard.</p>
<p class="mt-2 text-blue-400">Don't have one? Set it in your <code
class="bg-gray-700 px-1 rounded">.env</code> file, or check the server logs for the
temporary key.</p>
{% endif %}
<p class="mt-2">
<a href="https://github.com/airano-ir/mcphub#quick-start" target="_blank" class="text-purple-400 hover:text-purple-300 underline">
<a href="https://github.com/airano-ir/mcphub#quick-start" target="_blank"
class="text-purple-400 hover:text-purple-300 underline">
{% if lang == 'fa' %}راهنمای شروع{% else %}Setup Guide{% endif %}
</a>
</p>
@@ -128,11 +146,9 @@
<!-- Language Toggle -->
<div class="mt-6 text-center">
<a
href="?lang={% if lang == 'fa' %}en{% else %}fa{% endif %}{% if next_url %}&next={{ next_url }}{% endif %}"
class="text-sm text-gray-400 hover:text-white transition-colors"
>
{% if lang == 'fa' %}English{% else %}فارسی{% endif %}
<a href="?lang={% if lang == 'fa' %}en{% else %}fa{% endif %}{% if next_url %}&next={{ next_url }}{% endif %}"
class="text-sm text-gray-400 hover:text-white transition-colors">
{% if lang == 'fa' %}English{% else %}<span lang="fa" dir="rtl">فارسی</span>{% endif %}
</a>
</div>
</div>
@@ -143,4 +159,5 @@
</p>
</div>
</body>
</html>
</html>