Files
Samuel AmarandClaude Haiku 4.5 77d7a50fa9 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>
2026-07-29 14:00:22 +02:00

127 lines
5.0 KiB
Python

from datetime import datetime, timezone
from sqlalchemy import (
Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Table, Text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
def utcnow() -> datetime:
return datetime.now(timezone.utc)
key_endpoint_access = Table(
"key_endpoint_access",
Base.metadata,
Column("api_key_id", ForeignKey("api_keys.id", ondelete="CASCADE"), primary_key=True),
Column("endpoint_id", ForeignKey("endpoints.id", ondelete="CASCADE"), primary_key=True),
)
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(64), unique=True, index=True)
password_hash: Mapped[str] = mapped_column(String(256))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
api_keys: Mapped[list["ApiKey"]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)
class Service(Base):
__tablename__ = "services"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128))
slug: Mapped[str] = mapped_column(String(64), unique=True, index=True)
base_url: Mapped[str] = mapped_column(String(512))
description: Mapped[str] = mapped_column(Text, default="")
timeout_seconds: Mapped[float] = mapped_column(Float, default=30.0)
# Disable for upstreams with self-signed / internal-CA certificates.
verify_tls: Mapped[bool] = mapped_column(Boolean, default=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
endpoints: Mapped[list["Endpoint"]] = relationship(
back_populates="service", cascade="all, delete-orphan",
order_by="Endpoint.path",
)
class Endpoint(Base):
"""A callable route of an upstream service.
`method` is an HTTP verb or '*' (any). `path` starts with '/' and may use
'{param}' (one segment), '*' (any within a segment) and '**' (any depth),
e.g. '/orders/{id}', '/reports/**'.
"""
__tablename__ = "endpoints"
id: Mapped[int] = mapped_column(primary_key=True)
service_id: Mapped[int] = mapped_column(ForeignKey("services.id", ondelete="CASCADE"))
method: Mapped[str] = mapped_column(String(10), default="*")
path: Mapped[str] = mapped_column(String(512))
description: Mapped[str] = mapped_column(Text, default="")
service: Mapped[Service] = relationship(back_populates="endpoints")
api_keys: Mapped[list["ApiKey"]] = relationship(
secondary=key_endpoint_access, back_populates="endpoints"
)
@property
def label(self) -> str:
return f"{self.method} {self.path}"
class ApiKey(Base):
__tablename__ = "api_keys"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
name: Mapped[str] = mapped_column(String(128))
prefix: Mapped[str] = mapped_column(String(16), index=True) # first chars, for display
key_hash: Mapped[str] = mapped_column(String(128), unique=True, index=True)
rate_limit_per_minute: Mapped[int] = mapped_column(Integer, default=60) # 0 = unlimited
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
user: Mapped[User] = relationship(back_populates="api_keys")
endpoints: Mapped[list[Endpoint]] = relationship(
secondary=key_endpoint_access, back_populates="api_keys"
)
class RequestLog(Base):
__tablename__ = "request_logs"
id: Mapped[int] = mapped_column(primary_key=True)
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, index=True)
api_key_id: Mapped[int | None] = mapped_column(
ForeignKey("api_keys.id", ondelete="SET NULL"), nullable=True, index=True
)
service_id: Mapped[int | None] = mapped_column(
ForeignKey("services.id", ondelete="SET NULL"), nullable=True, index=True
)
endpoint_id: Mapped[int | None] = mapped_column(
ForeignKey("endpoints.id", ondelete="SET NULL"), nullable=True, index=True
)
method: Mapped[str] = mapped_column(String(10))
path: Mapped[str] = mapped_column(String(1024))
query_string: Mapped[str] = mapped_column(String(2048), default="")
status_code: Mapped[int] = mapped_column(Integer, index=True)
latency_ms: Mapped[float] = mapped_column(Float)
client_ip: Mapped[str] = mapped_column(String(64), default="")
request_body: Mapped[str] = mapped_column(Text, default="")
response_body: Mapped[str] = mapped_column(Text, default="")
api_key: Mapped[ApiKey | None] = relationship()
service: Mapped[Service | None] = relationship()
endpoint: Mapped[Endpoint | None] = relationship()