Initial commit: API gateway with admin console

- FastAPI async gateway with httpx proxying to multiple upstreams
- SQLite database with SQLAlchemy ORM
- Admin console: manage services, users, API keys, endpoint access
- Per-key, per-endpoint granular access control
- OpenAPI document sync and caching (5-minute TTL)
- Request/response logging with full transaction inspection
- In-memory rate limiting (per-key, fixed-window)
- Tiered log retention (7d payloads, 90d rows, incremental vacuum)
- TLS verification toggle per service (for self-signed certificates)
- Service connectivity validation with automatic endpoint refresh
- Request browser with filters and deep-link inspection
- Docker setup with persistent volume
- Modal forms for create/edit flows

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Samuel Amar
2026-07-29 14:00:22 +02:00
co-authored by Claude Haiku 4.5
commit 77d7a50fa9
31 changed files with 2927 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}API Gateway{% endblock %}</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="layout">
<nav class="sidebar">
<div class="brand">API <span>Gateway</span></div>
<a class="nav-item {% if active == 'dashboard' %}active{% endif %}" href="/admin">Dashboard</a>
<a class="nav-item {% if active == 'services' %}active{% endif %}" href="/admin/services">Services</a>
<a class="nav-item {% if active == 'users' %}active{% endif %}" href="/admin/users">Users</a>
<a class="nav-item {% if active == 'keys' %}active{% endif %}" href="/admin/keys">API Keys</a>
<a class="nav-item {% if active == 'requests' %}active{% endif %}" href="/admin/requests">Requests</a>
<a class="nav-item {% if active == 'monitoring' %}active{% endif %}" href="/admin/monitoring">Monitoring</a>
<div class="spacer"></div>
{% if user %}
<div class="whoami">{{ user.username }}</div>
<a class="nav-item" href="/admin/logout">Log out</a>
{% endif %}
</nav>
<main class="main">
{% block content %}{% endblock %}
</main>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script>
if (window.Chart) {
Chart.defaults.color = '#898781';
Chart.defaults.borderColor = '#2c2c2a';
Chart.defaults.font.family = 'system-ui, -apple-system, "Segoe UI", sans-serif';
Chart.defaults.plugins.legend.labels.boxWidth = 12;
Chart.defaults.plugins.legend.labels.boxHeight = 12;
Chart.defaults.animation.duration = 300;
}
const GW = {
blue: '#3987e5',
red: '#e66767',
ink2: '#c3c2b7',
grid: '#2c2c2a',
async fetchJSON(url) { const r = await fetch(url); return r.json(); },
};
// Destructive forms carry data-confirm. First click arms the button
// ("Confirm?"), a second click within 5s submits. No native dialogs —
// confirm() is unreliable in embedded browsers.
document.addEventListener('submit', e => {
const form = e.target;
if (!form.dataset.confirm) return;
if (form.dataset.armed === '1') return; // second click: let it through
e.preventDefault();
const btn = form.querySelector('button');
form.dataset.armed = '1';
if (!btn.dataset.orig) btn.dataset.orig = btn.innerHTML;
btn.innerHTML = 'Confirm?';
btn.classList.add('armed');
btn.title = form.dataset.confirm;
setTimeout(() => {
form.dataset.armed = '';
btn.innerHTML = btn.dataset.orig;
btn.classList.remove('armed');
}, 5000);
}, true);
// Modal overlays: [data-modal] opens the overlay with that id; the × button,
// a click on the backdrop, or Escape closes it.
document.addEventListener('click', e => {
const opener = e.target.closest('[data-modal]');
if (opener) {
e.preventDefault();
document.getElementById(opener.dataset.modal).classList.add('open');
return;
}
if (e.target.closest('.modal-close')) {
e.target.closest('.modal-overlay').classList.remove('open');
return;
}
if (e.target.classList.contains('modal-overlay')) e.target.classList.remove('open');
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape')
document.querySelectorAll('.modal-overlay.open').forEach(m => m.classList.remove('open'));
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>