- 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>
127 lines
5.0 KiB
Python
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()
|