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
+91
View File
@@ -0,0 +1,91 @@
{% extends "base.html" %}
{% block title %}Users · API Gateway{% endblock %}
{% macro trash(label) %}
<button class="icon" title="{{ label }}" aria-label="{{ label }}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
</button>
{% endmacro %}
{% macro pencil(label, modal) %}
<button class="icon edit" data-modal="{{ modal }}" title="{{ label }}" aria-label="{{ label }}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>
</button>
{% endmacro %}
{% block content %}
<div class="row" style="margin-bottom:20px">
<h1 style="margin:0">Users</h1>
<button class="right" data-modal="modal-new-user">+ Add user</button>
</div>
<div class="card">
<table>
<thead><tr><th>Username</th><th>API keys</th><th>Created</th><th>On</th><th></th></tr></thead>
<tbody>
{% for u in users %}
<tr>
<td class="strong">{{ u.username }}</td>
<td>{{ u.api_keys | length }}</td>
<td>{{ u.created_at.strftime("%Y-%m-%d") }}</td>
<td>
{% if u.id != user.id %}
<form class="inline" method="post" action="/admin/users/{{ u.id }}/toggle">
<label class="switch" title="{{ 'Disable' if u.is_active else 'Enable' }}">
<input type="checkbox" {% if u.is_active %}checked{% endif %} onchange="this.form.submit()">
<span class="slider"></span>
</label>
</form>
{% else %}
<label class="switch" title="You cannot deactivate your own account">
<input type="checkbox" checked disabled><span class="slider" style="opacity:.5"></span>
</label>
{% endif %}
</td>
<td style="white-space:nowrap">
{{ pencil('Edit ' ~ u.username, 'modal-user-' ~ u.id) }}
{% if u.id != user.id %}
<form class="inline" method="post" action="/admin/users/{{ u.id }}/delete"
data-confirm="Deletes {{ u.username }} and all their API keys">
{{ trash('Delete ' ~ u.username) }}
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="hint">Every user can sign in to this console and own API keys.
Gateway access itself is granted per key on the <a href="/admin/keys">API Keys</a> page.</div>
</div>
{% for u in users %}
<div class="modal-overlay" id="modal-user-{{ u.id }}" role="dialog" aria-modal="true" aria-label="Edit {{ u.username }}">
<div class="modal">
<h2>Edit {{ u.username }}</h2>
<button type="button" class="icon modal-close" title="Close" aria-label="Close">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
<form method="post" action="/admin/users/{{ u.id }}/password">
<label>New password</label>
<input type="password" name="password" required>
<div style="margin-top:14px"><button>Reset password</button></div>
</form>
</div>
</div>
{% endfor %}
<div class="modal-overlay" id="modal-new-user" role="dialog" aria-modal="true" aria-label="Add a user">
<div class="modal">
<h2>Add a user</h2>
<button type="button" class="icon modal-close" title="Close" aria-label="Close">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
<form method="post" action="/admin/users" class="grid cols-2">
<div><label>Username</label><input type="text" name="username" required></div>
<div><label>Password</label><input type="password" name="password" required></div>
<div><button>Create user</button></div>
</form>
</div>
</div>
{% endblock %}