Files
Priceflow/app/models.py
T
Claude 1dd6d4dae1 feat: Add authentication system and admin panel
- Add User model with authentication
- Create JWT-based auth service
- Add login page with admin/admin default credentials
- Create admin panel with user management, settings, and site configuration
- Move settings to admin section (admin only)
- Change search page from multi-site checkboxes to single site selector
- Add logout functionality to layout
- Protect routes with authentication
2025-11-23 09:15:56 +00:00

112 lines
5.5 KiB
Python

from datetime import UTC, datetime
from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Text
from sqlalchemy.orm import relationship
from .database import Base
class User(Base):
"""User model for authentication"""
__tablename__ = "users"
id: int = Column(Integer, primary_key=True, index=True) # type: ignore
username: str = Column(String, unique=True, nullable=False, index=True) # type: ignore
password_hash: str = Column(String, nullable=False) # type: ignore
is_admin: bool = Column(Boolean, default=False) # type: ignore
is_active: bool = Column(Boolean, default=True) # type: ignore
created_at: datetime = Column(DateTime, default=lambda: datetime.now(UTC)) # type: ignore
last_login: datetime | None = Column(DateTime, nullable=True) # type: ignore
class NotificationProfile(Base):
__tablename__ = "notification_profiles"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True)
apprise_url = Column(String)
notify_on_price_drop = Column(Boolean, default=True)
notify_on_target_price = Column(Boolean, default=True)
price_drop_threshold_percent = Column(Float, default=10.0)
notify_on_stock_change = Column(Boolean, default=True)
check_interval_minutes = Column(Integer, default=60)
items = relationship("Item", back_populates="notification_profile")
class Item(Base):
__tablename__ = "items"
id: int = Column(Integer, primary_key=True, index=True) # type: ignore
url: str = Column(String, index=True) # type: ignore
name: str = Column(String) # type: ignore
selector: str | None = Column(String, nullable=True) # type: ignore
target_price: float | None = Column(Float, nullable=True) # type: ignore
check_interval_minutes: int = Column(Integer, default=60) # type: ignore
# New fields
current_price: float | None = Column(Float, nullable=True) # type: ignore
in_stock: bool | None = Column(Boolean, nullable=True) # type: ignore
tags: str | None = Column(String, nullable=True) # type: ignore
description: str | None = Column(String, nullable=True) # type: ignore
# Confidence scores for latest extraction
current_price_confidence: float | None = Column(Float, nullable=True) # type: ignore
in_stock_confidence: float | None = Column(Float, nullable=True) # type: ignore
is_active: bool = Column(Boolean, default=True) # type: ignore
last_checked: datetime | None = Column(DateTime, nullable=True) # type: ignore
is_refreshing: bool = Column(Boolean, default=False) # type: ignore
last_error: str | None = Column(String, nullable=True) # type: ignore
notification_profile_id: int | None = Column(Integer, ForeignKey("notification_profiles.id"), nullable=True) # type: ignore
notification_profile = relationship("NotificationProfile", back_populates="items")
price_history = relationship("PriceHistory", back_populates="item", cascade="all, delete-orphan")
class PriceHistory(Base):
__tablename__ = "price_history"
id: int = Column(Integer, primary_key=True, index=True) # type: ignore
item_id: int = Column(Integer, ForeignKey("items.id")) # type: ignore
price: float = Column(Float) # type: ignore
timestamp: datetime = Column(DateTime, default=lambda: datetime.now(UTC)) # type: ignore
screenshot_path: str | None = Column(String, nullable=True) # type: ignore
# Confidence scores and AI metadata
price_confidence: float | None = Column(Float, nullable=True) # type: ignore
in_stock_confidence: float | None = Column(Float, nullable=True) # type: ignore
ai_model: str | None = Column(String, nullable=True) # type: ignore
ai_provider: str | None = Column(String, nullable=True) # type: ignore
prompt_version: str | None = Column(String, nullable=True) # type: ignore
repair_used: bool | None = Column(Boolean, nullable=True, default=False) # type: ignore
item = relationship("Item", back_populates="price_history")
class Settings(Base):
__tablename__ = "settings"
key = Column(String, primary_key=True, index=True)
value = Column(Text)
class SearchSite(Base):
"""Sites configurés pour la recherche de produits"""
__tablename__ = "search_sites"
id: int = Column(Integer, primary_key=True, index=True) # type: ignore
name: str = Column(String, nullable=False) # type: ignore # "Amazon France"
domain: str = Column(String, unique=True, nullable=False, index=True) # type: ignore # "amazon.fr"
logo_url: str | None = Column(String, nullable=True) # type: ignore
category: str | None = Column(String, nullable=True) # type: ignore # "Tech", "Déco", "Généraliste"
is_active: bool = Column(Boolean, default=True) # type: ignore
priority: int = Column(Integer, default=0) # type: ignore # Ordre d'affichage
requires_js: bool = Column(Boolean, default=False) # type: ignore # Force Browserless si True
price_selector: str | None = Column(String, nullable=True) # type: ignore # Sélecteur CSS pour le prix
search_url: str | None = Column(String, nullable=True) # type: ignore # URL de recherche avec {query} placeholder
product_link_selector: str | None = Column(String, nullable=True) # type: ignore # Sélecteur CSS pour les liens produits
created_at: datetime = Column(DateTime, default=lambda: datetime.now(UTC)) # type: ignore
updated_at: datetime = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)) # type: ignore