- 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
360 lines
16 KiB
Python
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]) |