Spaces:
Running
Running
| """Author RAG Chatbot SaaS β Client Access (Subscription) Model. | |
| Records every subscription grant from SuperAdmin to an author. | |
| The token_hash is stored (never the raw token) for security. | |
| Revocation is instant via Redis blacklist + this record. | |
| """ | |
| from datetime import datetime | |
| from sqlalchemy import Boolean, DateTime, Integer, String, Text | |
| from sqlalchemy import ForeignKey | |
| from sqlalchemy.orm import Mapped, mapped_column, relationship | |
| from app.models.base import Base, TimestampMixin, generate_uuid | |
| class ClientAccess(Base, TimestampMixin): | |
| """Subscription access record β one row per grant from SuperAdmin.""" | |
| __tablename__ = "client_access" | |
| id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) | |
| author_id: Mapped[str] = mapped_column( | |
| String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True | |
| ) | |
| granted_by: Mapped[str | None] = mapped_column( | |
| String(36), ForeignKey("users.id"), nullable=True | |
| ) # NULL for Stripe-auto-provisioned subscriptions (no human granter) | |
| plan: Mapped[str] = mapped_column( | |
| String(20), | |
| nullable=False, | |
| ) # Legacy plan string β also see plan_id FK below | |
| plan_id: Mapped[str | None] = mapped_column( | |
| String(50), nullable=True, index=True | |
| ) # FK to PricingPlan.id β set for Stripe-provisioned subscriptions | |
| stripe_subscription_id: Mapped[str | None] = mapped_column( | |
| String(255), nullable=True, unique=True, index=True | |
| ) # Links this access record to the Stripe subscription for webhook updates | |
| granted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) | |
| expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True) | |
| token_hash: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) | |
| # Token budget | |
| token_budget: Mapped[int] = mapped_column(Integer, nullable=False) | |
| tokens_used: Mapped[int] = mapped_column(Integer, default=0, nullable=False) | |
| bonus_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False) | |
| # Revocation | |
| is_revoked: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True) | |
| revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) | |
| revoked_by: Mapped[str | None] = mapped_column(String(36), nullable=True) | |
| revoke_reason: Mapped[str | None] = mapped_column(String(500), nullable=True) | |
| # Auto-renewal | |
| auto_renew: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) | |
| # Admin notes | |
| notes: Mapped[str | None] = mapped_column(Text, nullable=True) | |
| # Relationships | |
| author: Mapped["User"] = relationship("User", foreign_keys=[author_id], back_populates="access_records") | |
| def total_token_budget(self) -> int: | |
| """Total available tokens including any bonus grants.""" | |
| return self.token_budget + self.bonus_tokens | |
| def __repr__(self) -> str: | |
| return f"<ClientAccess author={self.author_id} plan={self.plan} expires={self.expires_at}>" | |