Spaces:
Running
Running
| """Author RAG Chatbot SaaS β User Model. | |
| Represents both Author (client) and SuperAdmin accounts. | |
| Role is enforced at the middleware and service layers. | |
| """ | |
| from datetime import datetime | |
| from sqlalchemy import Boolean, DateTime, Integer, String | |
| from sqlalchemy.orm import Mapped, mapped_column, relationship | |
| from app.models.base import Base, TimestampMixin, generate_uuid | |
| class User(Base, TimestampMixin): | |
| """User account β authors (clients) and super admins.""" | |
| __tablename__ = "users" | |
| id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) | |
| email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) | |
| password_hash: Mapped[str] = mapped_column(String(255), nullable=False) | |
| role: Mapped[str] = mapped_column( | |
| String(20), | |
| nullable=False, | |
| default="author", | |
| ) | |
| is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) | |
| failed_login_attempts: Mapped[int] = mapped_column(Integer, default=0, nullable=False) | |
| locked_until: Mapped[str | None] = mapped_column(String(50), nullable=True) | |
| totp_secret: Mapped[str | None] = mapped_column(String(64), nullable=True) # R-134 | |
| # Author profile fields | |
| full_name: Mapped[str | None] = mapped_column(String(255), nullable=True) | |
| website_url: Mapped[str | None] = mapped_column(String(500), nullable=True) | |
| bio: Mapped[str | None] = mapped_column(String(2000), nullable=True) | |
| logo_path: Mapped[str | None] = mapped_column(String(500), nullable=True) | |
| timezone: Mapped[str] = mapped_column(String(100), default="America/New_York", nullable=False) | |
| # Chatbot config | |
| bot_name: Mapped[str] = mapped_column(String(100), default="BookBot", nullable=False) | |
| bot_avatar_path: Mapped[str | None] = mapped_column(String(500), nullable=True) | |
| welcome_message: Mapped[str] = mapped_column( | |
| String(500), | |
| default="Hello!", | |
| nullable=False, | |
| ) | |
| fallback_message: Mapped[str] = mapped_column( | |
| String(500), | |
| default="I'm not sure about that specific detail β but I can tell you plenty about the books. What would you like to know?", | |
| nullable=False, | |
| ) | |
| response_style: Mapped[str] = mapped_column( | |
| String(20), | |
| default="balanced", | |
| nullable=False, | |
| ) | |
| out_of_scope_message: Mapped[str] = mapped_column( | |
| String(500), | |
| default="I can only help with questions about the author's books. Could you ask something related?", | |
| nullable=False, | |
| ) | |
| chatbot_is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) | |
| widget_theme: Mapped[str] = mapped_column(String(50), default="indigo", nullable=False) | |
| widget_position: Mapped[str] = mapped_column(String(20), default="bottom-right", nullable=False) | |
| widget_auto_open_delay: Mapped[int] = mapped_column(Integer, default=0, nullable=False) | |
| # Email notification preferences | |
| notify_weekly_digest: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) | |
| notify_token_alerts: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) | |
| notify_new_conversation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) | |
| notify_subscription_expiry: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) | |
| # ββ Stripe Billing ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| stripe_customer_id: Mapped[str | None] = mapped_column( | |
| String(255), nullable=True, unique=True, index=True | |
| ) # Set on first Stripe Checkout session | |
| stripe_subscription_id: Mapped[str | None] = mapped_column( | |
| String(255), nullable=True, unique=True, index=True | |
| ) | |
| stripe_plan_id: Mapped[str | None] = mapped_column( | |
| String(50), nullable=True | |
| ) # FK to PricingPlan.id (string FK β no hard FK constraint for flexibility) | |
| payment_grace_until: Mapped[datetime | None] = mapped_column( | |
| DateTime(timezone=True), nullable=True | |
| ) # Set when payment fails β access continues until this datetime | |
| # ββ White-Label (Phase 3) βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| remove_branding: Mapped[bool] = mapped_column( | |
| Boolean, default=False, nullable=False | |
| ) # Hide "Powered by Author RAG" footer (Pro/Enterprise only) | |
| custom_widget_css: Mapped[str | None] = mapped_column( | |
| String(5000), nullable=True | |
| ) # Author-supplied CSS β sanitized server-side before storage | |
| # ββ Phase 4 β Agency / Connect / GDPR βββββββββββββββββββββββββββββββββ | |
| stripe_connect_account_id: Mapped[str | None] = mapped_column( | |
| String(255), nullable=True | |
| ) # Stripe Express account for referral commission payouts | |
| notes: Mapped[str | None] = mapped_column( | |
| String(500), nullable=True | |
| ) # Internal flag; e.g. GDPR_DELETE_REQUESTED:<iso_date> | |
| # Relationships | |
| books: Mapped[list] = relationship("Book", back_populates="author", cascade="all, delete-orphan") | |
| documents: Mapped[list] = relationship("Document", back_populates="author", cascade="all, delete-orphan") | |
| access_records: Mapped[list] = relationship( | |
| "ClientAccess", | |
| back_populates="author", | |
| cascade="all, delete-orphan", | |
| foreign_keys="ClientAccess.author_id", | |
| ) | |
| def __repr__(self) -> str: | |
| return f"<User id={self.id} email={self.email} role={self.role}>" | |