Files
PromptTech/backend/models.py
Kristen Hercules 9a7b00649b feat: Implement comprehensive OAuth and email verification authentication system
- Add email verification with token-based validation
- Integrate Google, Facebook, and Yahoo OAuth providers
- Add OAuth configuration and email service modules
- Update User model with email_verified, oauth_provider, oauth_id fields
- Implement async password hashing/verification to prevent blocking
- Add database migration script for new user fields
- Create email verification page with professional UI
- Update login page with social login buttons (Google, Facebook, Yahoo)
- Add OAuth callback token handling
- Implement scroll-to-top navigation component
- Add 5-second real-time polling for Products and Services pages
- Enhance About page with Apple-style scroll animations
- Update Home and Contact pages with branding and business info
- Optimize API cache with prefix-based clearing
- Create comprehensive setup documentation and quick start guide
- Fix login performance with ThreadPoolExecutor for bcrypt operations

Performance improvements:
- Login time optimized to ~220ms with async password verification
- Real-time data updates every 5 seconds
- Non-blocking password operations

Security enhancements:
- Email verification required for new accounts
- OAuth integration for secure social login
- Verification tokens expire after 24 hours
- Password field nullable for OAuth users
2026-02-04 00:41:16 -06:00

360 lines
16 KiB
Python

from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, Text, ForeignKey, Enum as SQLEnum, JSON
from sqlalchemy.orm import relationship, declarative_base
from sqlalchemy.sql import func
from datetime import datetime, timezone
import enum
import uuid
Base = declarative_base()
def generate_uuid():
return str(uuid.uuid4())
class OrderStatus(enum.Enum):
PENDING = "pending"
PROCESSING = "processing"
LAYAWAY = "layaway"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
REFUNDED = "refunded"
ON_HOLD = "on_hold"
class UserRole(enum.Enum):
USER = "user"
ADMIN = "admin"
EMPLOYEE = "employee"
ACCOUNTANT = "accountant"
SALES_MANAGER = "sales_manager"
class User(Base):
__tablename__ = "users"
id = Column(String(36), primary_key=True, default=generate_uuid)
email = Column(String(255), unique=True, nullable=False, index=True)
name = Column(String(255), nullable=False)
password = Column(String(255), nullable=True) # Nullable for OAuth users
role = Column(SQLEnum(UserRole), default=UserRole.USER)
is_active = Column(Boolean, default=True, nullable=False)
# Email verification fields
email_verified = Column(Boolean, default=False, nullable=False)
verification_token = Column(String(500), nullable=True)
# OAuth fields
oauth_provider = Column(String(50), nullable=True) # google, facebook, yahoo, or None for email
oauth_id = Column(String(255), nullable=True) # User ID from OAuth provider
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
cart_items = relationship("CartItem", back_populates="user", cascade="all, delete-orphan")
orders = relationship("Order", back_populates="user")
reviews = relationship("Review", back_populates="user")
bookings = relationship("Booking", back_populates="user")
class Category(Base):
__tablename__ = "categories"
id = Column(String(36), primary_key=True, default=generate_uuid)
name = Column(String(100), unique=True, nullable=False)
slug = Column(String(100), unique=True, nullable=False)
description = Column(Text)
type = Column(String(50), default="product") # product or service
created_at = Column(DateTime(timezone=True), server_default=func.now())
products = relationship("Product", back_populates="category_rel")
services = relationship("Service", back_populates="category_rel")
class Product(Base):
__tablename__ = "products"
id = Column(String(36), primary_key=True, default=generate_uuid)
name = Column(String(255), nullable=False)
description = Column(Text) # Now supports HTML from rich text editor
price = Column(Float, nullable=False)
category = Column(String(100), nullable=False)
category_id = Column(String(36), ForeignKey("categories.id"), nullable=True)
image_url = Column(String(500)) # Deprecated - kept for backwards compatibility
stock = Column(Integer, default=10)
low_stock_threshold = Column(Integer, default=5)
brand = Column(String(100))
specs = Column(JSON, default={})
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
category_rel = relationship("Category", back_populates="products")
cart_items = relationship("CartItem", back_populates="product")
order_items = relationship("OrderItem", back_populates="product")
reviews = relationship("Review", back_populates="product", cascade="all, delete-orphan")
inventory_logs = relationship("InventoryLog", back_populates="product", cascade="all, delete-orphan")
images = relationship("ProductImage", back_populates="product", cascade="all, delete-orphan", order_by="ProductImage.display_order")
class ProductImage(Base):
__tablename__ = "product_images"
id = Column(String(36), primary_key=True, default=generate_uuid)
product_id = Column(String(36), ForeignKey("products.id"), nullable=False)
image_url = Column(String(500), nullable=False)
display_order = Column(Integer, default=0)
is_primary = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
product = relationship("Product", back_populates="images")
class ServiceImage(Base):
__tablename__ = "service_images"
id = Column(String(36), primary_key=True, default=generate_uuid)
service_id = Column(String(36), ForeignKey("services.id"), nullable=False)
image_url = Column(String(500), nullable=False)
display_order = Column(Integer, default=0)
is_primary = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
service = relationship("Service", back_populates="images")
class Service(Base):
__tablename__ = "services"
id = Column(String(36), primary_key=True, default=generate_uuid)
name = Column(String(255), nullable=False)
description = Column(Text) # Now supports HTML from rich text editor
price = Column(Float, nullable=False)
duration = Column(String(50))
image_url = Column(String(500)) # Deprecated - kept for backwards compatibility
category = Column(String(100), nullable=False)
category_id = Column(String(36), ForeignKey("categories.id"), nullable=True)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
category_rel = relationship("Category", back_populates="services")
bookings = relationship("Booking", back_populates="service")
reviews = relationship("Review", back_populates="service", cascade="all, delete-orphan")
images = relationship("ServiceImage", back_populates="service", cascade="all, delete-orphan", order_by="ServiceImage.display_order")
class CartItem(Base):
__tablename__ = "cart_items"
id = Column(String(36), primary_key=True, default=generate_uuid)
user_id = Column(String(36), ForeignKey("users.id"), nullable=False)
product_id = Column(String(36), ForeignKey("products.id"), nullable=False)
quantity = Column(Integer, default=1)
created_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", back_populates="cart_items")
product = relationship("Product", back_populates="cart_items")
class Order(Base):
__tablename__ = "orders"
id = Column(String(36), primary_key=True, default=generate_uuid)
user_id = Column(String(36), ForeignKey("users.id"), nullable=False)
status = Column(SQLEnum(OrderStatus), default=OrderStatus.PENDING)
subtotal = Column(Float, default=0)
tax = Column(Float, default=0)
shipping = Column(Float, default=0)
total = Column(Float, default=0)
shipping_address = Column(JSON, default={})
notes = Column(Text)
tracking_number = Column(String(100))
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
user = relationship("User", back_populates="orders")
items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")
status_history = relationship("OrderStatusHistory", back_populates="order", cascade="all, delete-orphan")
class OrderItem(Base):
__tablename__ = "order_items"
id = Column(String(36), primary_key=True, default=generate_uuid)
order_id = Column(String(36), ForeignKey("orders.id"), nullable=False)
product_id = Column(String(36), ForeignKey("products.id"), nullable=False)
quantity = Column(Integer, default=1)
price = Column(Float, nullable=False)
product_name = Column(String(255))
product_image = Column(String(500))
order = relationship("Order", back_populates="items")
product = relationship("Product", back_populates="order_items")
class OrderStatusHistory(Base):
__tablename__ = "order_status_history"
id = Column(String(36), primary_key=True, default=generate_uuid)
order_id = Column(String(36), ForeignKey("orders.id"), nullable=False)
status = Column(SQLEnum(OrderStatus), nullable=False)
notes = Column(Text)
created_at = Column(DateTime(timezone=True), server_default=func.now())
created_by = Column(String(36))
order = relationship("Order", back_populates="status_history")
class Review(Base):
__tablename__ = "reviews"
id = Column(String(36), primary_key=True, default=generate_uuid)
user_id = Column(String(36), ForeignKey("users.id"), nullable=False)
product_id = Column(String(36), ForeignKey("products.id"), nullable=True)
service_id = Column(String(36), ForeignKey("services.id"), nullable=True)
rating = Column(Integer, nullable=False) # 1-5
title = Column(String(255))
comment = Column(Text)
is_verified_purchase = Column(Boolean, default=False)
is_approved = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", back_populates="reviews")
product = relationship("Product", back_populates="reviews")
service = relationship("Service", back_populates="reviews")
class Booking(Base):
__tablename__ = "bookings"
id = Column(String(36), primary_key=True, default=generate_uuid)
service_id = Column(String(36), ForeignKey("services.id"), nullable=False)
user_id = Column(String(36), ForeignKey("users.id"), nullable=True)
name = Column(String(255), nullable=False)
email = Column(String(255), nullable=False)
phone = Column(String(50))
preferred_date = Column(String(50))
notes = Column(Text)
status = Column(String(50), default="pending")
service_name = Column(String(255))
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Completion fields
completed_at = Column(DateTime(timezone=True), nullable=True)
diagnosis = Column(Text, nullable=True) # Initial diagnosis/issue description
work_performed = Column(Text, nullable=True) # What was done to fix it
technician_notes = Column(Text, nullable=True) # Internal technician notes
service_cost = Column(Float, nullable=True) # Final cost if different from base price
# Payment fields
paid = Column(Boolean, default=False)
paid_at = Column(DateTime(timezone=True), nullable=True)
# Device information fields
device_model = Column(String(255), nullable=True) # e.g., "Dell Latitude 5520"
serial_number = Column(String(255), nullable=True)
product_number = Column(String(255), nullable=True)
screen_size = Column(String(50), nullable=True) # e.g., "15-inch", "13-inch"
service = relationship("Service", back_populates="bookings")
user = relationship("User", back_populates="bookings")
class Contact(Base):
__tablename__ = "contacts"
id = Column(String(36), primary_key=True, default=generate_uuid)
name = Column(String(255), nullable=False)
email = Column(String(255), nullable=False)
subject = Column(String(255))
message = Column(Text, nullable=False)
status = Column(String(50), default="pending")
created_at = Column(DateTime(timezone=True), server_default=func.now())
class InventoryLog(Base):
__tablename__ = "inventory_logs"
id = Column(String(36), primary_key=True, default=generate_uuid)
product_id = Column(String(36), ForeignKey("products.id"), nullable=False)
action = Column(String(50), nullable=False) # add, remove, adjust, sale
quantity_change = Column(Integer, nullable=False)
previous_stock = Column(Integer)
new_stock = Column(Integer)
notes = Column(Text)
created_by = Column(String(36))
created_at = Column(DateTime(timezone=True), server_default=func.now())
product = relationship("Product", back_populates="inventory_logs")
class SalesReport(Base):
__tablename__ = "sales_reports"
id = Column(String(36), primary_key=True, default=generate_uuid)
report_type = Column(String(50), nullable=False) # daily, weekly, monthly
report_date = Column(DateTime(timezone=True), nullable=False)
start_date = Column(DateTime(timezone=True))
end_date = Column(DateTime(timezone=True))
total_orders = Column(Integer, default=0)
total_revenue = Column(Float, default=0)
total_products_sold = Column(Integer, default=0)
total_services_booked = Column(Integer, default=0)
report_data = Column(JSON, default={})
created_at = Column(DateTime(timezone=True), server_default=func.now())
class AboutContent(Base):
__tablename__ = "about_content"
id = Column(String(36), primary_key=True, default=generate_uuid)
section = Column(String(50), nullable=False, unique=True) # 'hero', 'story', 'stats'
title = Column(String(255))
subtitle = Column(Text)
content = Column(Text) # HTML content from rich text editor
image_url = Column(String(500))
data = Column(JSON, default={}) # For flexible content like stats
is_active = Column(Boolean, default=True)
display_order = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
class TeamMember(Base):
__tablename__ = "team_members"
id = Column(String(36), primary_key=True, default=generate_uuid)
name = Column(String(255), nullable=False)
role = Column(String(255), nullable=False)
bio = Column(Text) # HTML content from rich text editor
image_url = Column(String(500))
email = Column(String(255))
linkedin = Column(String(500))
display_order = Column(Integer, default=0)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
class CompanyValue(Base):
__tablename__ = "company_values"
id = Column(String(36), primary_key=True, default=generate_uuid)
title = Column(String(255), nullable=False)
description = Column(Text)
icon = Column(String(50)) # Icon name (e.g., 'Target', 'Users', 'Award', 'Heart')
display_order = Column(Integer, default=0)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
class MediaType(enum.Enum):
IMAGE = "image"
DOCUMENT = "document"
VIDEO = "video"
OTHER = "other"
class Media(Base):
__tablename__ = "media"
id = Column(String(36), primary_key=True, default=generate_uuid)
filename = Column(String(255), nullable=False)
original_filename = Column(String(255), nullable=False)
file_path = Column(String(500), nullable=False)
file_url = Column(String(500), nullable=False)
file_size = Column(Integer, default=0) # Size in bytes
mime_type = Column(String(100))
media_type = Column(SQLEnum(MediaType), default=MediaType.IMAGE)
alt_text = Column(String(255))
title = Column(String(255))
description = Column(Text)
width = Column(Integer) # For images
height = Column(Integer) # For images
uploaded_by = Column(String(36), ForeignKey("users.id"), nullable=True)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
uploader = relationship("User", foreign_keys=[uploaded_by])