Arag / app /models /user.py
AuthorBot
Add email list sync UI, Phase 4 user fields, and startup migrations.
59ba43e
Raw
History Blame Contribute Delete
5.72 kB
"""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}>"