diff --git a/src/proxy_pool/accounts/models.py b/src/proxy_pool/accounts/models.py new file mode 100644 index 0000000..c57ea6b --- /dev/null +++ b/src/proxy_pool/accounts/models.py @@ -0,0 +1,78 @@ +import enum +import uuid +from datetime import datetime + +from sqlalchemy import ForeignKey, Index, Integer, String, Text, func +from sqlalchemy.dialects.postgresql import ENUM +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from proxy_pool.db.base import Base, TimestampMixin, UUIDPrimaryKeyMixin + + +class CreditTxType(str, enum.Enum): + PURCHASE = "purchase" + ACQUIRE = "acquire" + REFUND = "refund" + ADMIN_ADJUST = "admin_adjust" + + +class User(UUIDPrimaryKeyMixin, TimestampMixin, Base): + __tablename__ = "users" + + email: Mapped[str] = mapped_column(String(320), unique=True) + display_name: Mapped[str | None] = mapped_column(String(128)) + is_active: Mapped[bool] = mapped_column(default=True) + + api_keys: Mapped[list["ApiKey"]] = relationship(back_populates="user") + credit_entries: Mapped[list["CreditLedger"]] = relationship(back_populates="user") + + +class ApiKey(UUIDPrimaryKeyMixin, TimestampMixin, Base): + __tablename__ = "api_keys" + __table_args__ = ( + Index("ix_api_keys_hash", "key_hash", unique=True), + Index("ix_api_keys_prefix", "prefix"), + ) + + user_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), + ) + key_hash: Mapped[str] = mapped_column(String(128)) + prefix: Mapped[str] = mapped_column(String(8)) + label: Mapped[str | None] = mapped_column(String(128)) + is_active: Mapped[bool] = mapped_column(default=True) + last_used_at: Mapped[datetime | None] = mapped_column() + expires_at: Mapped[datetime | None] = mapped_column() + + user: Mapped["User"] = relationship(back_populates="api_keys") + + +class CreditLedger(UUIDPrimaryKeyMixin, TimestampMixin, Base): + __tablename__ = "credit_ledger" + __table_args__ = (Index("ix_ledger_user_created", "user_id", "created_at"),) + + user_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), + ) + amount: Mapped[int] = mapped_column(Integer) + tx_type: Mapped[CreditTxType] = mapped_column( + ENUM(CreditTxType, name="credit_tx_type"), + ) + description: Mapped[str | None] = mapped_column(Text) + reference_id: Mapped[uuid.UUID | None] = mapped_column() + + user: Mapped["User"] = relationship(back_populates="credit_entries") + + +class ProxyLease(UUIDPrimaryKeyMixin, Base): + __tablename__ = "proxy_leases" + __table_args__ = ( + Index("ix_leases_user", "user_id"), + Index("ix_leases_proxy_active", "proxy_id", "is_released"), + ) + + user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id")) + proxy_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("proxies.id")) + acquired_at: Mapped[datetime] = mapped_column(server_default=func.now()) + expires_at: Mapped[datetime] = mapped_column() + is_released: Mapped[bool] = mapped_column(default=False)