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()