diff --git a/.gitignore b/.gitignore
index bf38fa5..a9f2576 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ backend/*.pyc
backend/.pytest_cache/
backend/.vscode/
backend/instance/
+uploads/
# Frontend
frontend/node_modules/
diff --git a/backend/.env.example b/backend/.env.example
index a742e65..44eeb90 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -1,4 +1,4 @@
-DATABASE_URL=postgresql://user:password@localhost:5432/ecommerce_db
+DATABASE_URL=postgresql://ecommerce_user:password@localhost:5432/ecommerce_db
JWT_SECRET_KEY=your-secret-key-here-change-this-in-production
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
diff --git a/backend/app/__pycache__/__init__.cpython-314.pyc b/backend/app/__pycache__/__init__.cpython-314.pyc
new file mode 100644
index 0000000..5c6f4bf
Binary files /dev/null and b/backend/app/__pycache__/__init__.cpython-314.pyc differ
diff --git a/backend/app/__pycache__/config.cpython-314.pyc b/backend/app/__pycache__/config.cpython-314.pyc
new file mode 100644
index 0000000..2593fb5
Binary files /dev/null and b/backend/app/__pycache__/config.cpython-314.pyc differ
diff --git a/backend/app/__pycache__/main.cpython-314.pyc b/backend/app/__pycache__/main.cpython-314.pyc
new file mode 100644
index 0000000..6d50a86
Binary files /dev/null and b/backend/app/__pycache__/main.cpython-314.pyc differ
diff --git a/backend/app/__pycache__/utils.cpython-314.pyc b/backend/app/__pycache__/utils.cpython-314.pyc
new file mode 100644
index 0000000..47b6246
Binary files /dev/null and b/backend/app/__pycache__/utils.cpython-314.pyc differ
diff --git a/backend/app/config.py b/backend/app/config.py
index c8b6539..8ea8a61 100644
--- a/backend/app/config.py
+++ b/backend/app/config.py
@@ -1,4 +1,5 @@
from pydantic_settings import BaseSettings
+from pathlib import Path
class Settings(BaseSettings):
@@ -9,7 +10,7 @@ class Settings(BaseSettings):
frontend_url: str = "http://localhost:5173"
class Config:
- env_file = ".env"
+ env_file = str(Path(__file__).parent.parent / ".env")
settings = Settings()
diff --git a/backend/app/database/__pycache__/__init__.cpython-314.pyc b/backend/app/database/__pycache__/__init__.cpython-314.pyc
new file mode 100644
index 0000000..fe7c06f
Binary files /dev/null and b/backend/app/database/__pycache__/__init__.cpython-314.pyc differ
diff --git a/backend/app/database/__pycache__/database.cpython-314.pyc b/backend/app/database/__pycache__/database.cpython-314.pyc
new file mode 100644
index 0000000..38fb7ba
Binary files /dev/null and b/backend/app/database/__pycache__/database.cpython-314.pyc differ
diff --git a/backend/app/main.py b/backend/app/main.py
index acb583f..aabb977 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -1,9 +1,23 @@
+import sys
+from pathlib import Path
+
+# Add backend directory to Python path
+backend_dir = Path(__file__).resolve().parent.parent
+if str(backend_dir) not in sys.path:
+ sys.path.insert(0, str(backend_dir))
+
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
+from fastapi.staticfiles import StaticFiles
+import uvicorn
from app.database.database import engine, Base
from app.config import settings
-from app.routers import auth, users, products, categories, cart, orders, wishlist, contact
-
+from app.routers import auth, users, products, categories, cart, orders, wishlist, contact, models, brands
+
+# Create uploads directory if it doesn't exist
+uploads_dir = Path("uploads")
+uploads_dir.mkdir(exist_ok=True)
+
# Create tables
Base.metadata.create_all(bind=engine)
@@ -27,11 +41,16 @@ app.include_router(auth.router)
app.include_router(users.router)
app.include_router(products.router)
app.include_router(categories.router)
+app.include_router(models.router)
+app.include_router(brands.router)
app.include_router(cart.router)
app.include_router(orders.router)
app.include_router(wishlist.router)
app.include_router(contact.router)
+# Mount static files for uploads
+app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
+
@app.get("/")
def read_root():
@@ -45,3 +64,7 @@ def read_root():
@app.get("/health")
def health_check():
return {"status": "healthy"}
+
+
+if __name__ == "__main__":
+ uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
\ No newline at end of file
diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py
index 16c3ceb..5152d9c 100644
--- a/backend/app/models/__init__.py
+++ b/backend/app/models/__init__.py
@@ -1,6 +1,7 @@
from .user import User
from .product import Product
from .category import Category
+from .model import Model
from .cart import Cart, CartItem
from .order import Order, OrderItem
from .wishlist import Wishlist
@@ -10,6 +11,7 @@ __all__ = [
"User",
"Product",
"Category",
+ "Model",
"Cart",
"CartItem",
"Order",
diff --git a/backend/app/models/__pycache__/__init__.cpython-314.pyc b/backend/app/models/__pycache__/__init__.cpython-314.pyc
new file mode 100644
index 0000000..c569d7b
Binary files /dev/null and b/backend/app/models/__pycache__/__init__.cpython-314.pyc differ
diff --git a/backend/app/models/__pycache__/brand.cpython-314.pyc b/backend/app/models/__pycache__/brand.cpython-314.pyc
new file mode 100644
index 0000000..acee02e
Binary files /dev/null and b/backend/app/models/__pycache__/brand.cpython-314.pyc differ
diff --git a/backend/app/models/__pycache__/cart.cpython-314.pyc b/backend/app/models/__pycache__/cart.cpython-314.pyc
new file mode 100644
index 0000000..0310ab8
Binary files /dev/null and b/backend/app/models/__pycache__/cart.cpython-314.pyc differ
diff --git a/backend/app/models/__pycache__/category.cpython-314.pyc b/backend/app/models/__pycache__/category.cpython-314.pyc
new file mode 100644
index 0000000..ccd0420
Binary files /dev/null and b/backend/app/models/__pycache__/category.cpython-314.pyc differ
diff --git a/backend/app/models/__pycache__/contact_message.cpython-314.pyc b/backend/app/models/__pycache__/contact_message.cpython-314.pyc
new file mode 100644
index 0000000..fe230bc
Binary files /dev/null and b/backend/app/models/__pycache__/contact_message.cpython-314.pyc differ
diff --git a/backend/app/models/__pycache__/model.cpython-314.pyc b/backend/app/models/__pycache__/model.cpython-314.pyc
new file mode 100644
index 0000000..0632483
Binary files /dev/null and b/backend/app/models/__pycache__/model.cpython-314.pyc differ
diff --git a/backend/app/models/__pycache__/order.cpython-314.pyc b/backend/app/models/__pycache__/order.cpython-314.pyc
new file mode 100644
index 0000000..979fb28
Binary files /dev/null and b/backend/app/models/__pycache__/order.cpython-314.pyc differ
diff --git a/backend/app/models/__pycache__/product.cpython-314.pyc b/backend/app/models/__pycache__/product.cpython-314.pyc
new file mode 100644
index 0000000..f6cedc7
Binary files /dev/null and b/backend/app/models/__pycache__/product.cpython-314.pyc differ
diff --git a/backend/app/models/__pycache__/user.cpython-314.pyc b/backend/app/models/__pycache__/user.cpython-314.pyc
new file mode 100644
index 0000000..50d2931
Binary files /dev/null and b/backend/app/models/__pycache__/user.cpython-314.pyc differ
diff --git a/backend/app/models/__pycache__/wishlist.cpython-314.pyc b/backend/app/models/__pycache__/wishlist.cpython-314.pyc
new file mode 100644
index 0000000..57e090a
Binary files /dev/null and b/backend/app/models/__pycache__/wishlist.cpython-314.pyc differ
diff --git a/backend/app/models/brand.py b/backend/app/models/brand.py
new file mode 100644
index 0000000..8006783
--- /dev/null
+++ b/backend/app/models/brand.py
@@ -0,0 +1,8 @@
+from sqlalchemy import Column, Integer, String
+from ..database.database import Base
+
+class Brand(Base):
+ __tablename__ = "brand"
+
+ id = Column(Integer, primary_key=True, index=True)
+ name = Column(String(100), unique=True, nullable=False, index=True)
diff --git a/backend/app/models/category.py b/backend/app/models/category.py
index 73ce1ef..32ae02d 100644
--- a/backend/app/models/category.py
+++ b/backend/app/models/category.py
@@ -9,3 +9,4 @@ class Category(Base):
name = Column(String, unique=True, index=True)
slug = Column(String, unique=True, index=True)
description = Column(String, nullable=True)
+ image = Column(String, nullable=True)
diff --git a/backend/app/models/model.py b/backend/app/models/model.py
new file mode 100644
index 0000000..05f1aa7
--- /dev/null
+++ b/backend/app/models/model.py
@@ -0,0 +1,21 @@
+from sqlalchemy import Column, Integer, String, DECIMAL, ForeignKey, TIMESTAMP, JSON
+from sqlalchemy.orm import relationship
+from datetime import datetime
+from app.database.database import Base
+
+
+class Model(Base):
+ __tablename__ = "model"
+
+ id = Column(Integer, primary_key=True, index=True)
+ name = Column(String(255), nullable=False)
+ category_id = Column(Integer, ForeignKey("category.id", ondelete="CASCADE"), nullable=False)
+ brand = Column(String(100), nullable=False)
+ base_price = Column(DECIMAL(10, 2), nullable=True)
+ sizes = Column(JSON, nullable=True)
+ description = Column(String, nullable=True)
+ created_at = Column(TIMESTAMP, default=datetime.utcnow)
+
+ # Relationships
+ category = relationship("Category", backref="models")
+ products = relationship("Product", back_populates="model")
diff --git a/backend/app/models/product.py b/backend/app/models/product.py
index fd79036..e068d9a 100644
--- a/backend/app/models/product.py
+++ b/backend/app/models/product.py
@@ -1,4 +1,4 @@
-from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, Text, ForeignKey, JSON
+from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, Text, ForeignKey, JSON, DECIMAL
from sqlalchemy.orm import relationship
from datetime import datetime
from app.database.database import Base
@@ -9,10 +9,12 @@ class Product(Base):
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
+ slug = Column(String, unique=True, index=True, nullable=True)
description = Column(Text)
price = Column(Float)
discount_price = Column(Float, nullable=True)
category_id = Column(Integer, ForeignKey("category.id"))
+ model_id = Column(Integer, ForeignKey("model.id", ondelete="SET NULL"), nullable=True)
gender = Column(String) # men, women
brand = Column(String)
sizes = Column(JSON) # ["S", "M", "L", "XL", ...]
@@ -21,8 +23,11 @@ class Product(Base):
images = Column(JSON) # Array of image URLs
is_featured = Column(Boolean, default=False)
is_on_sale = Column(Boolean, default=False)
+ override_price = Column(DECIMAL(10, 2), nullable=True)
+ override_sizes = Column(JSON, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
category = relationship("Category")
+ model = relationship("Model", back_populates="products")
cart_items = relationship("CartItem", back_populates="product")
order_items = relationship("OrderItem", back_populates="product")
diff --git a/backend/app/models/user.py b/backend/app/models/user.py
index 2678d2e..2d0c403 100644
--- a/backend/app/models/user.py
+++ b/backend/app/models/user.py
@@ -24,6 +24,7 @@ class User(Base):
postal_code = Column(String, nullable=True)
country = Column(String, nullable=True)
is_active = Column(Boolean, default=True)
+ is_admin = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
cart = relationship("Cart", back_populates="user", uselist=False)
diff --git a/backend/app/routers/__pycache__/__init__.cpython-314.pyc b/backend/app/routers/__pycache__/__init__.cpython-314.pyc
new file mode 100644
index 0000000..bf08c63
Binary files /dev/null and b/backend/app/routers/__pycache__/__init__.cpython-314.pyc differ
diff --git a/backend/app/routers/__pycache__/auth.cpython-314.pyc b/backend/app/routers/__pycache__/auth.cpython-314.pyc
new file mode 100644
index 0000000..4fe2a58
Binary files /dev/null and b/backend/app/routers/__pycache__/auth.cpython-314.pyc differ
diff --git a/backend/app/routers/__pycache__/brands.cpython-314.pyc b/backend/app/routers/__pycache__/brands.cpython-314.pyc
new file mode 100644
index 0000000..27900a3
Binary files /dev/null and b/backend/app/routers/__pycache__/brands.cpython-314.pyc differ
diff --git a/backend/app/routers/__pycache__/cart.cpython-314.pyc b/backend/app/routers/__pycache__/cart.cpython-314.pyc
new file mode 100644
index 0000000..c29d2bc
Binary files /dev/null and b/backend/app/routers/__pycache__/cart.cpython-314.pyc differ
diff --git a/backend/app/routers/__pycache__/categories.cpython-314.pyc b/backend/app/routers/__pycache__/categories.cpython-314.pyc
new file mode 100644
index 0000000..dc2d0e9
Binary files /dev/null and b/backend/app/routers/__pycache__/categories.cpython-314.pyc differ
diff --git a/backend/app/routers/__pycache__/contact.cpython-314.pyc b/backend/app/routers/__pycache__/contact.cpython-314.pyc
new file mode 100644
index 0000000..906e587
Binary files /dev/null and b/backend/app/routers/__pycache__/contact.cpython-314.pyc differ
diff --git a/backend/app/routers/__pycache__/models.cpython-314.pyc b/backend/app/routers/__pycache__/models.cpython-314.pyc
new file mode 100644
index 0000000..74556f0
Binary files /dev/null and b/backend/app/routers/__pycache__/models.cpython-314.pyc differ
diff --git a/backend/app/routers/__pycache__/orders.cpython-314.pyc b/backend/app/routers/__pycache__/orders.cpython-314.pyc
new file mode 100644
index 0000000..057319b
Binary files /dev/null and b/backend/app/routers/__pycache__/orders.cpython-314.pyc differ
diff --git a/backend/app/routers/__pycache__/products.cpython-314.pyc b/backend/app/routers/__pycache__/products.cpython-314.pyc
new file mode 100644
index 0000000..ad066b6
Binary files /dev/null and b/backend/app/routers/__pycache__/products.cpython-314.pyc differ
diff --git a/backend/app/routers/__pycache__/users.cpython-314.pyc b/backend/app/routers/__pycache__/users.cpython-314.pyc
new file mode 100644
index 0000000..73554f4
Binary files /dev/null and b/backend/app/routers/__pycache__/users.cpython-314.pyc differ
diff --git a/backend/app/routers/__pycache__/wishlist.cpython-314.pyc b/backend/app/routers/__pycache__/wishlist.cpython-314.pyc
new file mode 100644
index 0000000..65e3836
Binary files /dev/null and b/backend/app/routers/__pycache__/wishlist.cpython-314.pyc differ
diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py
index 8aebd1f..a70ab97 100644
--- a/backend/app/routers/auth.py
+++ b/backend/app/routers/auth.py
@@ -47,7 +47,7 @@ def login(email: str, password: str, db: Session = Depends(get_db)):
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
access_token = create_access_token(
- data={"sub": user.id}, expires_delta=access_token_expires
+ data={"sub": str(user.id)}, expires_delta=access_token_expires
)
return {
diff --git a/backend/app/routers/brands.py b/backend/app/routers/brands.py
new file mode 100644
index 0000000..c8a9314
--- /dev/null
+++ b/backend/app/routers/brands.py
@@ -0,0 +1,89 @@
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.orm import Session
+from typing import List
+from ..database.database import get_db
+from ..models.brand import Brand
+from ..models.user import User
+from ..schemas.brand import BrandCreate, BrandUpdate, BrandResponse
+from ..services.auth import get_current_user
+
+router = APIRouter(prefix="/api/brands", tags=["brands"])
+
+@router.get("", response_model=List[BrandResponse])
+def list_brands(db: Session = Depends(get_db)):
+ """Get all brands"""
+ brands = db.query(Brand).order_by(Brand.name).all()
+ return brands
+
+@router.get("/{brand_id}", response_model=BrandResponse)
+def get_brand(brand_id: int, db: Session = Depends(get_db)):
+ """Get a specific brand"""
+ brand = db.query(Brand).filter(Brand.id == brand_id).first()
+ if not brand:
+ raise HTTPException(status_code=404, detail="Brand not found")
+ return brand
+
+@router.post("", response_model=BrandResponse)
+def create_brand(
+ brand_data: BrandCreate,
+ db: Session = Depends(get_db),
+ current_user: User = Depends(get_current_user)
+):
+ """Create a new brand (admin only)"""
+ if not current_user.is_admin:
+ raise HTTPException(status_code=403, detail="Admin access required")
+
+ # Check if brand already exists
+ existing = db.query(Brand).filter(Brand.name == brand_data.name).first()
+ if existing:
+ raise HTTPException(status_code=400, detail="Brand already exists")
+
+ brand = Brand(name=brand_data.name)
+ db.add(brand)
+ db.commit()
+ db.refresh(brand)
+ return brand
+
+@router.put("/{brand_id}", response_model=BrandResponse)
+def update_brand(
+ brand_id: int,
+ brand_data: BrandUpdate,
+ db: Session = Depends(get_db),
+ current_user: User = Depends(get_current_user)
+):
+ """Update a brand (admin only)"""
+ if not current_user.is_admin:
+ raise HTTPException(status_code=403, detail="Admin access required")
+
+ brand = db.query(Brand).filter(Brand.id == brand_id).first()
+ if not brand:
+ raise HTTPException(status_code=404, detail="Brand not found")
+
+ # Check if new name conflicts with existing brand
+ if brand_data.name != brand.name:
+ existing = db.query(Brand).filter(Brand.name == brand_data.name).first()
+ if existing:
+ raise HTTPException(status_code=400, detail="Brand name already exists")
+
+ brand.name = brand_data.name
+ db.commit()
+ db.refresh(brand)
+ return brand
+
+@router.delete("/{brand_id}")
+def delete_brand(
+ brand_id: int,
+ db: Session = Depends(get_db),
+ current_user: User = Depends(get_current_user)
+):
+ """Delete a brand (admin only)"""
+ if not current_user.is_admin:
+ raise HTTPException(status_code=403, detail="Admin access required")
+
+ brand = db.query(Brand).filter(Brand.id == brand_id).first()
+ if not brand:
+ raise HTTPException(status_code=404, detail="Brand not found")
+
+ db.delete(brand)
+ db.commit()
+ return {"message": "Brand deleted successfully"}
diff --git a/backend/app/routers/cart.py b/backend/app/routers/cart.py
index 4ab6d38..66f6b95 100644
--- a/backend/app/routers/cart.py
+++ b/backend/app/routers/cart.py
@@ -9,42 +9,40 @@ from app.services.cart import (
remove_from_cart,
clear_cart,
)
-from app.services.auth import verify_token
+from app.services.auth import get_current_user
+from app.models import User
router = APIRouter(prefix="/api/cart", tags=["cart"])
-def get_user_id_from_token(token: str) -> int:
- user_id = verify_token(token)
- if user_id is None:
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid token",
- )
- return user_id
-
-
@router.get("", response_model=CartResponse)
-def get_user_cart(token: str, db: Session = Depends(get_db)):
- user_id = get_user_id_from_token(token)
- cart = get_cart(db, user_id)
+def get_user_cart(
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ cart = get_cart(db, current_user.id)
if not cart:
raise HTTPException(status_code=404, detail="Cart not found")
return cart
@router.post("/add", response_model=dict)
-def add_item_to_cart(token: str, item: CartItemCreate, db: Session = Depends(get_db)):
- user_id = get_user_id_from_token(token)
- cart_item = add_to_cart(db, user_id, item)
+def add_item_to_cart(
+ item: CartItemCreate,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ cart_item = add_to_cart(db, current_user.id, item)
return {"message": "Item added to cart", "item_id": cart_item.id}
@router.put("/{cart_item_id}", response_model=dict)
def update_item(
- cart_item_id: int, token: str, update: CartItemUpdate, db: Session = Depends(get_db)
+ cart_item_id: int,
+ update: CartItemUpdate,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
):
- user_id = get_user_id_from_token(token)
cart_item = update_cart_item(db, cart_item_id, update)
if not cart_item:
raise HTTPException(status_code=404, detail="Cart item not found")
@@ -52,16 +50,21 @@ def update_item(
@router.delete("/{cart_item_id}")
-def remove_item(cart_item_id: int, token: str, db: Session = Depends(get_db)):
- user_id = get_user_id_from_token(token)
+def remove_item(
+ cart_item_id: int,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
if not remove_from_cart(db, cart_item_id):
raise HTTPException(status_code=404, detail="Cart item not found")
return {"message": "Item removed from cart"}
@router.delete("")
-def clear_user_cart(token: str, db: Session = Depends(get_db)):
- user_id = get_user_id_from_token(token)
- if not clear_cart(db, user_id):
+def clear_user_cart(
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ if not clear_cart(db, current_user.id):
raise HTTPException(status_code=404, detail="Cart not found")
return {"message": "Cart cleared"}
diff --git a/backend/app/routers/categories.py b/backend/app/routers/categories.py
index 898f384..ef8cb73 100644
--- a/backend/app/routers/categories.py
+++ b/backend/app/routers/categories.py
@@ -2,8 +2,9 @@ from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
from app.database.database import get_db
-from app.models import Category
+from app.models import Category, User
from app.schemas.category import CategoryCreate, CategoryResponse, CategoryUpdate
+from app.services.auth import get_current_admin_user
router = APIRouter(prefix="/api/categories", tags=["categories"])
@@ -22,8 +23,11 @@ def get_category(category_id: int, db: Session = Depends(get_db)):
@router.post("", response_model=CategoryResponse)
-def create_category(category: CategoryCreate, db: Session = Depends(get_db)):
- # TODO: Add admin check
+def create_category(
+ category: CategoryCreate,
+ db: Session = Depends(get_db),
+ admin: User = Depends(get_current_admin_user)
+):
db_category = Category(**category.dict())
db.add(db_category)
db.commit()
@@ -33,9 +37,11 @@ def create_category(category: CategoryCreate, db: Session = Depends(get_db)):
@router.put("/{category_id}", response_model=CategoryResponse)
def update_category(
- category_id: int, category_update: CategoryUpdate, db: Session = Depends(get_db)
+ category_id: int,
+ category_update: CategoryUpdate,
+ db: Session = Depends(get_db),
+ admin: User = Depends(get_current_admin_user)
):
- # TODO: Add admin check
category = db.query(Category).filter(Category.id == category_id).first()
if not category:
raise HTTPException(status_code=404, detail="Category not found")
@@ -49,8 +55,11 @@ def update_category(
@router.delete("/{category_id}")
-def delete_category(category_id: int, db: Session = Depends(get_db)):
- # TODO: Add admin check
+def delete_category(
+ category_id: int,
+ db: Session = Depends(get_db),
+ admin: User = Depends(get_current_admin_user)
+):
category = db.query(Category).filter(Category.id == category_id).first()
if not category:
raise HTTPException(status_code=404, detail="Category not found")
diff --git a/backend/app/routers/models.py b/backend/app/routers/models.py
new file mode 100644
index 0000000..6ee78d4
--- /dev/null
+++ b/backend/app/routers/models.py
@@ -0,0 +1,105 @@
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.orm import Session
+from typing import List, Optional
+from app.database.database import get_db
+from app.models import Model, User
+from app.schemas.model import ModelCreate, ModelUpdate, ModelResponse
+from app.services.auth import get_current_admin_user
+
+router = APIRouter(prefix="/api/models", tags=["models"])
+
+
+@router.get("", response_model=List[ModelResponse])
+def get_models(
+ category_id: Optional[int] = None,
+ brand: Optional[str] = None,
+ db: Session = Depends(get_db)
+):
+ """Get all models with optional filtering by category and brand"""
+ query = db.query(Model)
+
+ if category_id:
+ query = query.filter(Model.category_id == category_id)
+ if brand:
+ query = query.filter(Model.brand.ilike(f"%{brand}%"))
+
+ return query.all()
+
+
+@router.get("/{model_id}", response_model=ModelResponse)
+def get_model(model_id: int, db: Session = Depends(get_db)):
+ """Get a specific model by ID"""
+ model = db.query(Model).filter(Model.id == model_id).first()
+ if not model:
+ raise HTTPException(status_code=404, detail="Model not found")
+ return model
+
+
+@router.post("", response_model=ModelResponse)
+def create_model(
+ model_data: ModelCreate,
+ db: Session = Depends(get_db),
+ admin: User = Depends(get_current_admin_user)
+):
+ """Create a new model (admin only)"""
+ # Check if model with same name, category, and brand already exists
+ existing = db.query(Model).filter(
+ Model.name == model_data.name,
+ Model.category_id == model_data.category_id,
+ Model.brand == model_data.brand
+ ).first()
+
+ if existing:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Model '{model_data.name}' already exists for brand '{model_data.brand}' in this category"
+ )
+
+ db_model = Model(**model_data.dict())
+ db.add(db_model)
+ db.commit()
+ db.refresh(db_model)
+ return db_model
+
+
+@router.put("/{model_id}", response_model=ModelResponse)
+def update_model(
+ model_id: int,
+ model_update: ModelUpdate,
+ db: Session = Depends(get_db),
+ admin: User = Depends(get_current_admin_user)
+):
+ """Update a model (admin only)"""
+ model = db.query(Model).filter(Model.id == model_id).first()
+ if not model:
+ raise HTTPException(status_code=404, detail="Model not found")
+
+ for field, value in model_update.dict(exclude_unset=True).items():
+ setattr(model, field, value)
+
+ db.commit()
+ db.refresh(model)
+ return model
+
+
+@router.delete("/{model_id}")
+def delete_model(
+ model_id: int,
+ db: Session = Depends(get_db),
+ admin: User = Depends(get_current_admin_user)
+):
+ """Delete a model (admin only)"""
+ model = db.query(Model).filter(Model.id == model_id).first()
+ if not model:
+ raise HTTPException(status_code=404, detail="Model not found")
+
+ db.delete(model)
+ db.commit()
+ return {"message": "Model deleted successfully"}
+
+
+@router.get("/brands/list")
+def get_brands(db: Session = Depends(get_db)):
+ """Get list of unique brands"""
+ brands = db.query(Model.brand).distinct().all()
+ return [brand[0] for brand in brands if brand[0]]
diff --git a/backend/app/routers/orders.py b/backend/app/routers/orders.py
index a3de381..446ed80 100644
--- a/backend/app/routers/orders.py
+++ b/backend/app/routers/orders.py
@@ -9,25 +9,19 @@ from app.services.order import (
get_user_orders,
update_order_status,
)
-from app.services.auth import verify_token
+from app.services.auth import get_current_user
+from app.models import User
router = APIRouter(prefix="/api/orders", tags=["orders"])
-def get_user_id_from_token(token: str) -> int:
- user_id = verify_token(token)
- if user_id is None:
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid token",
- )
- return user_id
-
-
@router.post("", response_model=OrderResponse)
-def create_new_order(token: str, order_data: OrderCreate, db: Session = Depends(get_db)):
- user_id = get_user_id_from_token(token)
- order = create_order(db, user_id, order_data)
+def create_new_order(
+ order_data: OrderCreate,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ order = create_order(db, current_user.id, order_data)
if not order:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -36,16 +30,21 @@ def create_new_order(token: str, order_data: OrderCreate, db: Session = Depends(
return order
-@router.get("/user/orders", response_model=List[OrderResponse])
-def get_user_order_history(token: str, db: Session = Depends(get_db)):
- user_id = get_user_id_from_token(token)
- return get_user_orders(db, user_id)
+@router.get("", response_model=List[OrderResponse])
+def get_user_order_history(
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ return get_user_orders(db, current_user.id)
@router.get("/{order_id}", response_model=OrderResponse)
-def get_order(order_id: int, token: str, db: Session = Depends(get_db)):
- user_id = get_user_id_from_token(token)
+def get_order(
+ order_id: int,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
order = get_order_by_id(db, order_id)
- if not order or order.user_id != user_id:
+ if not order or order.user_id != current_user.id:
raise HTTPException(status_code=404, detail="Order not found")
return order
diff --git a/backend/app/routers/products.py b/backend/app/routers/products.py
index 1915c1b..db359bc 100644
--- a/backend/app/routers/products.py
+++ b/backend/app/routers/products.py
@@ -1,8 +1,10 @@
-from fastapi import APIRouter, Depends, HTTPException, status
+from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
+from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from typing import List, Optional
+from pathlib import Path
from app.database.database import get_db
-from app.models import Product, Category
+from app.models import Product, Category, User
from app.schemas.product import (
ProductCreate,
ProductResponse,
@@ -16,6 +18,8 @@ from app.services.product import (
delete_product,
search_products,
)
+from app.services.auth import get_current_admin_user
+from app.utils import save_upload_file, generate_slug
router = APIRouter(prefix="/api/products", tags=["products"])
@@ -23,22 +27,37 @@ router = APIRouter(prefix="/api/products", tags=["products"])
@router.get("", response_model=List[ProductResponse])
def list_products(
category_id: Optional[int] = None,
+ model_id: Optional[int] = None,
+ brand: Optional[str] = None,
gender: Optional[str] = None,
+ min_price: Optional[float] = None,
+ max_price: Optional[float] = None,
on_sale: Optional[bool] = None,
featured: Optional[bool] = None,
skip: int = 0,
limit: int = 20,
db: Session = Depends(get_db),
):
- return get_products(
- db,
- category_id=category_id,
- gender=gender,
- on_sale=on_sale,
- featured=featured,
- skip=skip,
- limit=limit,
- )
+ query = db.query(Product)
+
+ if category_id:
+ query = query.filter(Product.category_id == category_id)
+ if model_id:
+ query = query.filter(Product.model_id == model_id)
+ if brand:
+ query = query.filter(Product.brand.ilike(f"%{brand}%"))
+ if gender:
+ query = query.filter(Product.gender == gender)
+ if min_price is not None:
+ query = query.filter(Product.price >= min_price)
+ if max_price is not None:
+ query = query.filter(Product.price <= max_price)
+ if on_sale is not None:
+ query = query.filter(Product.is_on_sale == on_sale)
+ if featured is not None:
+ query = query.filter(Product.is_featured == featured)
+
+ return query.offset(skip).limit(limit).all()
@router.get("/search", response_model=List[ProductResponse])
@@ -55,16 +74,32 @@ def get_product(product_id: int, db: Session = Depends(get_db)):
@router.post("", response_model=ProductResponse)
-def create_new_product(product: ProductCreate, db: Session = Depends(get_db)):
- # TODO: Add admin check
+def create_new_product(
+ product: ProductCreate,
+ db: Session = Depends(get_db),
+ admin: User = Depends(get_current_admin_user),
+):
+ # Auto-generate slug if not provided
+ if not product.slug:
+ base_slug = generate_slug(f"{product.brand} {product.name}")
+ # Check if slug exists and make it unique
+ slug = base_slug
+ counter = 1
+ while db.query(Product).filter(Product.slug == slug).first():
+ slug = f"{base_slug}-{counter}"
+ counter += 1
+ product.slug = slug
+
return create_product(db, product)
@router.put("/{product_id}", response_model=ProductResponse)
def update_existing_product(
- product_id: int, product_update: ProductUpdate, db: Session = Depends(get_db)
+ product_id: int,
+ product_update: ProductUpdate,
+ db: Session = Depends(get_db),
+ admin: User = Depends(get_current_admin_user),
):
- # TODO: Add admin check
product = update_product(db, product_id, product_update)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
@@ -72,8 +107,82 @@ def update_existing_product(
@router.delete("/{product_id}")
-def delete_existing_product(product_id: int, db: Session = Depends(get_db)):
- # TODO: Add admin check
+def delete_existing_product(
+ product_id: int,
+ db: Session = Depends(get_db),
+ admin: User = Depends(get_current_admin_user),
+):
if not delete_product(db, product_id):
raise HTTPException(status_code=404, detail="Product not found")
return {"message": "Product deleted successfully"}
+
+
+@router.post("/upload-image")
+async def upload_product_image(
+ file: UploadFile = File(...),
+ admin: User = Depends(get_current_admin_user),
+):
+ """Upload a product image and return the URL"""
+ # Validate file type
+ allowed_types = ["image/jpeg", "image/png", "image/jpg", "image/webp", "image/gif"]
+ if file.content_type not in allowed_types:
+ raise HTTPException(
+ status_code=400,
+ detail=f"File type {file.content_type} not allowed. Allowed types: {', '.join(allowed_types)}"
+ )
+
+ try:
+ # Save file and get path
+ file_path = save_upload_file(file, folder="products")
+ # Return full URL
+ return {"url": file_path, "message": "Image uploaded successfully"}
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error uploading file: {str(e)}")
+
+
+@router.post("/upload-images")
+async def upload_multiple_images(
+ files: List[UploadFile] = File(...),
+ admin: User = Depends(get_current_admin_user),
+):
+ """Upload multiple product images and return the URLs"""
+ allowed_types = ["image/jpeg", "image/png", "image/jpg", "image/webp", "image/gif"]
+ uploaded_urls = []
+ errors = []
+
+ for file in files:
+ if file.content_type not in allowed_types:
+ errors.append(f"File {file.filename}: type {file.content_type} not allowed")
+ continue
+
+ try:
+ file_path = save_upload_file(file, folder="products")
+ uploaded_urls.append(file_path)
+ except Exception as e:
+ errors.append(f"File {file.filename}: {str(e)}")
+
+ return {
+ "urls": uploaded_urls,
+ "uploaded_count": len(uploaded_urls),
+ "errors": errors if errors else None,
+ "message": f"Successfully uploaded {len(uploaded_urls)} of {len(files)} images"
+ }
+
+
+@router.get("/filters/brands")
+def get_brands(db: Session = Depends(get_db)):
+ """Get list of unique brands from products"""
+ brands = db.query(Product.brand).distinct().filter(Product.brand.isnot(None)).all()
+ return [brand[0] for brand in brands if brand[0]]
+
+
+@router.get("/filters/sizes")
+def get_sizes(db: Session = Depends(get_db)):
+ """Get list of unique sizes from products"""
+ products = db.query(Product.sizes).filter(Product.sizes.isnot(None)).all()
+ sizes_set = set()
+ for product in products:
+ if product.sizes:
+ for size in product.sizes:
+ sizes_set.add(size)
+ return sorted(list(sizes_set))
diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py
index 8a8cd64..6f678b2 100644
--- a/backend/app/routers/users.py
+++ b/backend/app/routers/users.py
@@ -3,33 +3,30 @@ from sqlalchemy.orm import Session
from app.database.database import get_db
from app.models import User
from app.schemas.user import UserResponse, UserUpdate
-from app.services.auth import verify_token
+from app.services.auth import get_current_user
router = APIRouter(prefix="/api/users", tags=["users"])
-def get_current_user(token: str, db: Session = Depends(get_db)) -> User:
- user_id = verify_token(token)
- if user_id is None:
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid token",
- )
- user = db.query(User).filter(User.id == user_id).first()
- if not user:
- raise HTTPException(status_code=404, detail="User not found")
- return user
-
-
@router.get("/me", response_model=UserResponse)
-def get_current_user_profile(token: str, db: Session = Depends(get_db)):
- user = get_current_user(token, db)
- return user
+def get_current_user_profile(
+ current_user: User = Depends(get_current_user),
+):
+ return current_user
@router.put("/me", response_model=UserResponse)
-def update_user_profile(token: str, user_update: UserUpdate, db: Session = Depends(get_db)):
- user = get_current_user(token, db)
+def update_user_profile(
+ user_update: UserUpdate,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ for field, value in user_update.dict(exclude_unset=True).items():
+ setattr(current_user, field, value)
+
+ db.commit()
+ db.refresh(current_user)
+ return current_user
for field, value in user_update.dict(exclude_unset=True).items():
setattr(user, field, value)
diff --git a/backend/app/routers/wishlist.py b/backend/app/routers/wishlist.py
index 7d18cb5..a4fcd6f 100644
--- a/backend/app/routers/wishlist.py
+++ b/backend/app/routers/wishlist.py
@@ -2,28 +2,20 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from app.database.database import get_db
-from app.models import Wishlist, Product
+from app.models import Wishlist, Product, User
from app.schemas.product import ProductResponse
-from app.services.auth import verify_token
+from app.services.auth import get_current_user
router = APIRouter(prefix="/api/wishlist", tags=["wishlist"])
-def get_user_id_from_token(token: str) -> int:
- user_id = verify_token(token)
- if user_id is None:
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid token",
- )
- return user_id
-
-
@router.get("", response_model=List[ProductResponse])
-def get_wishlist(token: str, db: Session = Depends(get_db)):
- user_id = get_user_id_from_token(token)
+def get_wishlist(
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
wishlist_items = (
- db.query(Wishlist).filter(Wishlist.user_id == user_id).all()
+ db.query(Wishlist).filter(Wishlist.user_id == current_user.id).all()
)
products = [
db.query(Product).filter(Product.id == item.product_id).first()
@@ -33,12 +25,14 @@ def get_wishlist(token: str, db: Session = Depends(get_db)):
@router.post("/{product_id}")
-def add_to_wishlist(product_id: int, token: str, db: Session = Depends(get_db)):
- user_id = get_user_id_from_token(token)
-
+def add_to_wishlist(
+ product_id: int,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
existing = (
db.query(Wishlist)
- .filter(Wishlist.user_id == user_id, Wishlist.product_id == product_id)
+ .filter(Wishlist.user_id == current_user.id, Wishlist.product_id == product_id)
.first()
)
if existing:
@@ -47,19 +41,21 @@ def add_to_wishlist(product_id: int, token: str, db: Session = Depends(get_db)):
detail="Product already in wishlist",
)
- wishlist_item = Wishlist(user_id=user_id, product_id=product_id)
+ wishlist_item = Wishlist(user_id=current_user.id, product_id=product_id)
db.add(wishlist_item)
db.commit()
return {"message": "Product added to wishlist"}
@router.delete("/{product_id}")
-def remove_from_wishlist(product_id: int, token: str, db: Session = Depends(get_db)):
- user_id = get_user_id_from_token(token)
-
+def remove_from_wishlist(
+ product_id: int,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
wishlist_item = (
db.query(Wishlist)
- .filter(Wishlist.user_id == user_id, Wishlist.product_id == product_id)
+ .filter(Wishlist.user_id == current_user.id, Wishlist.product_id == product_id)
.first()
)
if not wishlist_item:
diff --git a/backend/app/schemas/__pycache__/__init__.cpython-314.pyc b/backend/app/schemas/__pycache__/__init__.cpython-314.pyc
new file mode 100644
index 0000000..c9ee5d2
Binary files /dev/null and b/backend/app/schemas/__pycache__/__init__.cpython-314.pyc differ
diff --git a/backend/app/schemas/__pycache__/brand.cpython-314.pyc b/backend/app/schemas/__pycache__/brand.cpython-314.pyc
new file mode 100644
index 0000000..dd60bf0
Binary files /dev/null and b/backend/app/schemas/__pycache__/brand.cpython-314.pyc differ
diff --git a/backend/app/schemas/__pycache__/cart.cpython-314.pyc b/backend/app/schemas/__pycache__/cart.cpython-314.pyc
new file mode 100644
index 0000000..589f677
Binary files /dev/null and b/backend/app/schemas/__pycache__/cart.cpython-314.pyc differ
diff --git a/backend/app/schemas/__pycache__/category.cpython-314.pyc b/backend/app/schemas/__pycache__/category.cpython-314.pyc
new file mode 100644
index 0000000..b57743d
Binary files /dev/null and b/backend/app/schemas/__pycache__/category.cpython-314.pyc differ
diff --git a/backend/app/schemas/__pycache__/contact.cpython-314.pyc b/backend/app/schemas/__pycache__/contact.cpython-314.pyc
new file mode 100644
index 0000000..3e6531e
Binary files /dev/null and b/backend/app/schemas/__pycache__/contact.cpython-314.pyc differ
diff --git a/backend/app/schemas/__pycache__/model.cpython-314.pyc b/backend/app/schemas/__pycache__/model.cpython-314.pyc
new file mode 100644
index 0000000..eee4462
Binary files /dev/null and b/backend/app/schemas/__pycache__/model.cpython-314.pyc differ
diff --git a/backend/app/schemas/__pycache__/order.cpython-314.pyc b/backend/app/schemas/__pycache__/order.cpython-314.pyc
new file mode 100644
index 0000000..83b6f44
Binary files /dev/null and b/backend/app/schemas/__pycache__/order.cpython-314.pyc differ
diff --git a/backend/app/schemas/__pycache__/product.cpython-314.pyc b/backend/app/schemas/__pycache__/product.cpython-314.pyc
new file mode 100644
index 0000000..e8e6a81
Binary files /dev/null and b/backend/app/schemas/__pycache__/product.cpython-314.pyc differ
diff --git a/backend/app/schemas/__pycache__/user.cpython-314.pyc b/backend/app/schemas/__pycache__/user.cpython-314.pyc
new file mode 100644
index 0000000..01355a0
Binary files /dev/null and b/backend/app/schemas/__pycache__/user.cpython-314.pyc differ
diff --git a/backend/app/schemas/brand.py b/backend/app/schemas/brand.py
new file mode 100644
index 0000000..134e7b3
--- /dev/null
+++ b/backend/app/schemas/brand.py
@@ -0,0 +1,16 @@
+from pydantic import BaseModel
+
+class BrandBase(BaseModel):
+ name: str
+
+class BrandCreate(BrandBase):
+ pass
+
+class BrandUpdate(BrandBase):
+ pass
+
+class BrandResponse(BrandBase):
+ id: int
+
+ class Config:
+ from_attributes = True
diff --git a/backend/app/schemas/category.py b/backend/app/schemas/category.py
index a354ebb..99b2c56 100644
--- a/backend/app/schemas/category.py
+++ b/backend/app/schemas/category.py
@@ -7,11 +7,13 @@ class CategoryCreate(BaseModel):
name: str
slug: str
description: Optional[str] = None
+ image: Optional[str] = None
class CategoryUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
+ image: Optional[str] = None
class CategoryResponse(BaseModel):
@@ -19,6 +21,7 @@ class CategoryResponse(BaseModel):
name: str
slug: str
description: Optional[str]
+ image: Optional[str]
class Config:
from_attributes = True
diff --git a/backend/app/schemas/model.py b/backend/app/schemas/model.py
new file mode 100644
index 0000000..722f59d
--- /dev/null
+++ b/backend/app/schemas/model.py
@@ -0,0 +1,36 @@
+from pydantic import BaseModel
+from typing import Optional, List
+from datetime import datetime
+from decimal import Decimal
+
+
+class ModelCreate(BaseModel):
+ name: str
+ category_id: int
+ brand: str
+ base_price: Optional[Decimal] = None
+ sizes: Optional[List[str]] = []
+ description: Optional[str] = None
+
+
+class ModelUpdate(BaseModel):
+ name: Optional[str] = None
+ category_id: Optional[int] = None
+ brand: Optional[str] = None
+ base_price: Optional[Decimal] = None
+ sizes: Optional[List[str]] = None
+ description: Optional[str] = None
+
+
+class ModelResponse(BaseModel):
+ id: int
+ name: str
+ category_id: int
+ brand: str
+ base_price: Optional[Decimal]
+ sizes: Optional[List[str]]
+ description: Optional[str]
+ created_at: datetime
+
+ class Config:
+ from_attributes = True
diff --git a/backend/app/schemas/product.py b/backend/app/schemas/product.py
index 7b65d74..6015b63 100644
--- a/backend/app/schemas/product.py
+++ b/backend/app/schemas/product.py
@@ -1,30 +1,37 @@
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
+from decimal import Decimal
class ProductCreate(BaseModel):
name: str
+ slug: Optional[str] = None
description: str
price: float
discount_price: Optional[float] = None
category_id: int
+ model_id: Optional[int] = None
gender: str # men, women
brand: str
sizes: List[str]
- colors: List[str]
+ colors: Optional[List[str]] = []
stock: int
images: List[str]
is_featured: bool = False
is_on_sale: bool = False
+ override_price: Optional[Decimal] = None
+ override_sizes: Optional[List[str]] = None
class ProductUpdate(BaseModel):
name: Optional[str] = None
+ slug: Optional[str] = None
description: Optional[str] = None
price: Optional[float] = None
discount_price: Optional[float] = None
category_id: Optional[int] = None
+ model_id: Optional[int] = None
gender: Optional[str] = None
brand: Optional[str] = None
sizes: Optional[List[str]] = None
@@ -33,23 +40,29 @@ class ProductUpdate(BaseModel):
images: Optional[List[str]] = None
is_featured: Optional[bool] = None
is_on_sale: Optional[bool] = None
+ override_price: Optional[Decimal] = None
+ override_sizes: Optional[List[str]] = None
class ProductResponse(BaseModel):
id: int
name: str
+ slug: Optional[str]
description: str
price: float
discount_price: Optional[float]
category_id: int
+ model_id: Optional[int]
gender: str
brand: str
sizes: List[str]
- colors: List[str]
+ colors: Optional[List[str]]
stock: int
images: List[str]
is_featured: bool
is_on_sale: bool
+ override_price: Optional[Decimal]
+ override_sizes: Optional[List[str]]
created_at: datetime
class Config:
diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py
index 16c8a33..1ddc7f5 100644
--- a/backend/app/schemas/user.py
+++ b/backend/app/schemas/user.py
@@ -29,6 +29,7 @@ class UserResponse(UserBase):
postal_code: Optional[str]
country: Optional[str]
is_active: bool
+ is_admin: bool
created_at: datetime
class Config:
diff --git a/backend/app/services/__pycache__/__init__.cpython-314.pyc b/backend/app/services/__pycache__/__init__.cpython-314.pyc
new file mode 100644
index 0000000..07afa10
Binary files /dev/null and b/backend/app/services/__pycache__/__init__.cpython-314.pyc differ
diff --git a/backend/app/services/__pycache__/auth.cpython-314.pyc b/backend/app/services/__pycache__/auth.cpython-314.pyc
new file mode 100644
index 0000000..0bfafeb
Binary files /dev/null and b/backend/app/services/__pycache__/auth.cpython-314.pyc differ
diff --git a/backend/app/services/__pycache__/cart.cpython-314.pyc b/backend/app/services/__pycache__/cart.cpython-314.pyc
new file mode 100644
index 0000000..038f8c2
Binary files /dev/null and b/backend/app/services/__pycache__/cart.cpython-314.pyc differ
diff --git a/backend/app/services/__pycache__/order.cpython-314.pyc b/backend/app/services/__pycache__/order.cpython-314.pyc
new file mode 100644
index 0000000..f6681f4
Binary files /dev/null and b/backend/app/services/__pycache__/order.cpython-314.pyc differ
diff --git a/backend/app/services/__pycache__/product.cpython-314.pyc b/backend/app/services/__pycache__/product.cpython-314.pyc
new file mode 100644
index 0000000..885da87
Binary files /dev/null and b/backend/app/services/__pycache__/product.cpython-314.pyc differ
diff --git a/backend/app/services/auth.py b/backend/app/services/auth.py
index a5db368..74743e5 100644
--- a/backend/app/services/auth.py
+++ b/backend/app/services/auth.py
@@ -2,11 +2,15 @@ from datetime import datetime, timedelta
from typing import Optional
from passlib.context import CryptContext
from jose import JWTError, jwt
+from fastapi import Depends, HTTPException, status
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from app.config import settings
from app.models import User
from sqlalchemy.orm import Session
+from app.database.database import get_db
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+security = HTTPBearer()
def verify_password(plain_password: str, hashed_password: str) -> bool:
@@ -37,11 +41,17 @@ def verify_token(token: str) -> Optional[int]:
payload = jwt.decode(
token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm]
)
- user_id: int = payload.get("sub")
- if user_id is None:
+ user_id_str: str = payload.get("sub")
+ if user_id_str is None:
return None
+ # Convert string to integer
+ user_id = int(user_id_str)
return user_id
- except JWTError:
+ except JWTError as e:
+ return None
+ except (ValueError, TypeError) as e:
+ return None
+ except Exception as e:
return None
@@ -50,3 +60,32 @@ def authenticate_user(db: Session, email: str, password: str) -> Optional[User]:
if not user or not verify_password(password, user.hashed_password):
return None
return user
+
+
+def get_current_user(
+ credentials: HTTPAuthorizationCredentials = Depends(security),
+ db: Session = Depends(get_db),
+) -> User:
+ token = credentials.credentials
+ user_id = verify_token(token)
+ if user_id is None:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid authentication credentials",
+ )
+ user = db.query(User).filter(User.id == user_id).first()
+ if user is None:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="User not found",
+ )
+ return user
+
+
+def get_current_admin_user(current_user: User = Depends(get_current_user)) -> User:
+ if not current_user.is_admin:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Admin access required",
+ )
+ return current_user
diff --git a/backend/app/utils.py b/backend/app/utils.py
new file mode 100644
index 0000000..c8f0f41
--- /dev/null
+++ b/backend/app/utils.py
@@ -0,0 +1,32 @@
+import re
+import os
+import uuid
+from pathlib import Path
+
+def generate_slug(text: str) -> str:
+ """Generate a URL-friendly slug from text"""
+ # Convert to lowercase
+ slug = text.lower()
+ # Replace spaces and special characters with hyphens
+ slug = re.sub(r'[^\w\s-]', '', slug)
+ slug = re.sub(r'[\s_-]+', '-', slug)
+ slug = re.sub(r'^-+|-+$', '', slug)
+ return slug
+
+def save_upload_file(upload_file, folder: str = "products") -> str:
+ """Save uploaded file and return the file path"""
+ # Create uploads directory if it doesn't exist
+ upload_dir = Path("uploads") / folder
+ upload_dir.mkdir(parents=True, exist_ok=True)
+
+ # Generate unique filename
+ file_extension = os.path.splitext(upload_file.filename)[1]
+ unique_filename = f"{uuid.uuid4()}{file_extension}"
+ file_path = upload_dir / unique_filename
+
+ # Save file
+ with open(file_path, "wb") as buffer:
+ buffer.write(upload_file.file.read())
+
+ # Return relative path for URL
+ return f"/uploads/{folder}/{unique_filename}"
diff --git a/backend/migrations/001_add_model_table.sql b/backend/migrations/001_add_model_table.sql
new file mode 100644
index 0000000..a026d85
--- /dev/null
+++ b/backend/migrations/001_add_model_table.sql
@@ -0,0 +1,24 @@
+-- Migration: Add model table and update product table
+-- Create model table
+CREATE TABLE IF NOT EXISTS model (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ category_id INTEGER NOT NULL,
+ brand VARCHAR(100) NOT NULL,
+ base_price DECIMAL(10, 2),
+ sizes JSONB,
+ description TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
+ UNIQUE (name, category_id, brand)
+);
+
+CREATE INDEX IF NOT EXISTS idx_model_category ON model(category_id);
+CREATE INDEX IF NOT EXISTS idx_model_brand ON model(brand);
+
+-- Add new columns to product table
+ALTER TABLE product ADD COLUMN IF NOT EXISTS model_id INTEGER REFERENCES model(id) ON DELETE SET NULL;
+ALTER TABLE product ADD COLUMN IF NOT EXISTS override_price DECIMAL(10, 2);
+ALTER TABLE product ADD COLUMN IF NOT EXISTS override_sizes JSONB;
+
+CREATE INDEX IF NOT EXISTS idx_product_model ON product(model_id);
diff --git a/backend/migrations/002_add_brands_table.sql b/backend/migrations/002_add_brands_table.sql
new file mode 100644
index 0000000..191e105
--- /dev/null
+++ b/backend/migrations/002_add_brands_table.sql
@@ -0,0 +1,24 @@
+-- Migration: Add brands table
+-- Date: 2026-05-01
+-- Description: Create brands table for storing brand names
+
+CREATE TABLE IF NOT EXISTS brand (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(100) NOT NULL UNIQUE
+);
+
+CREATE INDEX IF NOT EXISTS idx_brand_name ON brand(name);
+
+-- Insert existing brands from products
+INSERT INTO brand (name)
+SELECT DISTINCT brand
+FROM product
+WHERE brand IS NOT NULL AND brand != ''
+ON CONFLICT (name) DO NOTHING;
+
+-- Insert existing brands from model table
+INSERT INTO brand (name)
+SELECT DISTINCT brand
+FROM model
+WHERE brand IS NOT NULL AND brand != ''
+ON CONFLICT (name) DO NOTHING;
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 669064b..6a093da 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -1,11 +1,11 @@
-fastapi==0.104.1
-uvicorn==0.24.0
-sqlalchemy==2.0.23
-psycopg2-binary==2.9.9
-python-dotenv==1.0.0
-pydantic==2.5.0
-pydantic-settings==2.1.0
-python-multipart==0.0.6
-python-jose[cryptography]==3.3.0
-passlib[bcrypt]==1.7.4
-email-validator==2.1.0
+fastapi>=0.104.1
+uvicorn>=0.24.0
+sqlalchemy>=2.0.23
+psycopg2-binary>=2.9.11
+python-dotenv>=1.0.0
+pydantic>=2.5.0
+pydantic-settings>=2.1.0
+python-multipart>=0.0.6
+python-jose[cryptography]>=3.3.0
+passlib[bcrypt]>=1.7.4
+email-validator>=2.1.0
diff --git a/backend/schema.sql b/backend/schema.sql
index 8079bfb..26976d6 100644
--- a/backend/schema.sql
+++ b/backend/schema.sql
@@ -1,6 +1,48 @@
-- E-Commerce Database Schema
--- Run this file to create tables and populate initial data
--- psql -U ecommerce_user -d ecommerce_db -h localhost -f schema.sql
+-- Run this file to create database, user, tables and populate initial data
+-- Usage: psql -U postgres -h localhost -f schema.sql
+
+-- ============================================
+-- CREATE DATABASE AND USER
+-- ============================================
+
+-- Drop existing database and user if they exist (for clean setup)
+DROP DATABASE IF EXISTS ecommerce_db;
+DROP USER IF EXISTS ecommerce_user;
+
+-- Create database (Windows compatible - uses system defaults)
+CREATE DATABASE ecommerce_db
+ WITH
+ OWNER = postgres
+ ENCODING = 'UTF8'
+ TABLESPACE = pg_default
+ CONNECTION LIMIT = -1;
+
+-- Create user with password
+CREATE USER ecommerce_user WITH PASSWORD 'password';
+
+-- Grant all privileges on database to user
+GRANT ALL PRIVILEGES ON DATABASE ecommerce_db TO ecommerce_user;
+
+-- Connect to the ecommerce_db database
+\c ecommerce_db
+
+-- Grant schema privileges including CREATE permission
+GRANT ALL ON SCHEMA public TO ecommerce_user;
+GRANT CREATE ON SCHEMA public TO ecommerce_user;
+
+-- Make ecommerce_user owner of public schema to avoid permission issues
+ALTER SCHEMA public OWNER TO ecommerce_user;
+
+-- Grant privileges on all current and future tables
+GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ecommerce_user;
+GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ecommerce_user;
+GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ecommerce_user;
+
+-- Grant privileges on future objects
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ecommerce_user;
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ecommerce_user;
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO ecommerce_user;
-- ============================================
-- CREATE TABLES
@@ -11,9 +53,27 @@ CREATE TABLE category (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
- description TEXT
+ description TEXT,
+ image VARCHAR(500)
);
+-- Model Table (e.g., New Balance 9060)
+CREATE TABLE model (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ category_id INTEGER NOT NULL,
+ brand VARCHAR(100) NOT NULL,
+ base_price DECIMAL(10, 2),
+ sizes JSONB,
+ description TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
+ UNIQUE (name, category_id, brand)
+);
+
+CREATE INDEX idx_model_category ON model(category_id);
+CREATE INDEX idx_model_brand ON model(brand);
+
-- User Table
CREATE TABLE "user" (
id SERIAL PRIMARY KEY,
@@ -26,6 +86,7 @@ CREATE TABLE "user" (
postal_code VARCHAR(20),
country VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
+ is_admin BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
@@ -33,10 +94,12 @@ CREATE TABLE "user" (
CREATE TABLE product (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
+ slug VARCHAR(255) UNIQUE,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
discount_price DECIMAL(10, 2),
category_id INTEGER NOT NULL,
+ model_id INTEGER,
gender VARCHAR(50),
brand VARCHAR(100),
sizes JSONB,
@@ -45,10 +108,16 @@ CREATE TABLE product (
images JSONB,
is_featured BOOLEAN DEFAULT FALSE,
is_on_sale BOOLEAN DEFAULT FALSE,
+ override_price DECIMAL(10, 2),
+ override_sizes JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE
+ FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
+ FOREIGN KEY (model_id) REFERENCES model(id) ON DELETE SET NULL
);
+CREATE INDEX idx_product_slug ON product(slug);
+CREATE INDEX idx_product_model ON product(model_id);
+
-- Cart Table
CREATE TABLE cart (
id SERIAL PRIMARY KEY,
@@ -284,61 +353,88 @@ INSERT INTO product (name, description, price, discount_price, category_id, gend
);
-- Insert Sample Users (for testing)
-INSERT INTO "user" (email, hashed_password, full_name, phone, address, city, postal_code, country, is_active) VALUES
+INSERT INTO "user" (email, hashed_password, full_name, phone, address, city, postal_code, country, is_active, is_admin) VALUES
+ (
+ 'admin@example.com',
+ '$2b$12$jNWZdZNMbKEkOjA8Gq3ZUOTml23zfqhFDPJ8AlZQ51WhUyi04AH7C', -- password: password123
+ 'Admin User',
+ '1234567890',
+ '123 Admin Street',
+ 'New York',
+ '10001',
+ 'USA',
+ TRUE,
+ TRUE
+ ),
(
'user@example.com',
- '$2b$12$KIXxPfROLqWHYIgC5FCOO.7yqKU8RvOmOhP7kJZnYLh6pJn2FSfKy', -- password: password123
+ '$2b$12$jNWZdZNMbKEkOjA8Gq3ZUOTml23zfqhFDPJ8AlZQ51WhUyi04AH7C', -- password: password123
'John Doe',
'1234567890',
'123 Main Street',
'New York',
'10001',
'USA',
- TRUE
+ TRUE,
+ FALSE
),
(
'jane@example.com',
- '$2b$12$KIXxPfROLqWHYIgC5FCOO.7yqKU8RvOmOhP7kJZnYLh6pJn2FSfKy', -- password: password123
+ '$2b$12$jNWZdZNMbKEkOjA8Gq3ZUOTml23zfqhFDPJ8AlZQ51WhUyi04AH7C', -- password: password123
'Jane Smith',
'9876543210',
'456 Oak Avenue',
'Los Angeles',
'90001',
'USA',
- TRUE
+ TRUE,
+ FALSE
);
-- Create carts for users
INSERT INTO cart (user_id) VALUES
(1),
- (2);
+ (2),
+ (3);
-- ============================================
--- SET PERMISSIONS
+-- SET PERMISSIONS (After all tables are created)
-- ============================================
+-- Grant all privileges on all tables, sequences, and functions to the user
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ecommerce_user;
-GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ecommerce_user;
+GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ecommerce_user;
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ecommerce_user;
+-- Ensure future objects also get permissions
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ecommerce_user;
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ecommerce_user;
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO ecommerce_user;
+
-- ============================================
-- COMPLETE
-- ============================================
-- Schema created successfully!
--
--- User: ecommerce_user
-- Database: ecommerce_db
+-- User: ecommerce_user
+-- Password: password
-- Host: localhost:5432
--
+-- Connection string for .env file:
+-- DATABASE_URL=postgresql://ecommerce_user:password@localhost:5432/ecommerce_db
+--
-- Tables created: 9
-- - category, user, product, cart, cart_item
-- - order, order_item, user_wishlist, contact_message
--
-- Demo accounts:
--- - user@example.com / password123
--- - jane@example.com / password123
+-- - ADMIN: admin@example.com / password123 (is_admin: true)
+-- - USER: user@example.com / password123
+-- - USER: jane@example.com / password123
--
--- Sample data: 5 categories, 9 products, 2 users
+-- Sample data: 5 categories, 9 products, 3 users
--
-- Next steps:
--- 1. Update backend/.env with your credentials
--- 2. Run: uvicorn app.main:app --reload --port 8000
+-- 1. Update backend/.env with: DATABASE_URL=postgresql://ecommerce_user:password@localhost:5432/ecommerce_db
+-- 2. Run: python backend/app/main.py
+-- 3. Login as admin to create/edit/delete products
diff --git a/ecommerce.db b/ecommerce.db
new file mode 100644
index 0000000..8521be7
Binary files /dev/null and b/ecommerce.db differ
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index febeb1b..9430c8a 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -19,6 +19,8 @@ import Wishlist from './pages/Wishlist'
import About from './pages/About'
import Contact from './pages/Contact'
import Sales from './pages/Sales'
+import Admin from './pages/Admin'
+import Models from './pages/Models'
function App() {
return (
@@ -42,6 +44,8 @@ function App() {
Loading...
+ ) : ( +| ID | +Name | +Brand | +Slug | +Price | +Stock | +Featured | +On Sale | +Actions | +
|---|---|---|---|---|---|---|---|---|
| {product.id} | +{product.name} | +{product.brand} | ++ {product.slug || '-'} + | +${product.price} | +{product.stock} | +{product.is_featured ? '✓' : '-'} | +{product.is_on_sale ? '✓' : '-'} | +
+ |
+
Loading...
+ ) : ( +Slug: {category.slug}
+ {category.description && ( +{category.description}
+ )} ++ {productCount} product{productCount !== 1 ? 's' : ''} +
++ {modelCount} model{modelCount !== 1 ? 's' : ''} +
+{item.product.name}
{item.size &&Size: {item.size}
} - {item.color &&Color: {item.color}
}Demo Account:
-Email: user@example.com
-Password: password123
+Demo Accounts:
++ Admin: admin@example.com / password123 +
+User: user@example.com / password123
Loading...
+ ) : ( +| ID | +Model Name | +Brand | +Category | +Base Price | +Sizes | +Actions | +
|---|---|---|---|---|---|---|
| {model.id} | +{model.name} | +{model.brand} | +{getCategoryName(model.category_id)} | ++ {model.base_price ? `$${parseFloat(model.base_price).toFixed(2)}` : '-'} + | ++ {model.sizes ? model.sizes.join(', ') : '-'} + | +
+ |
+
{product.description}
- {product.colors.length > 0 && ( -