From b99e0da61c797171cb9c9e744d054ebbebbac8c0 Mon Sep 17 00:00:00 2001
From: duyanhehe <duyanhex@gmail.com>
Date: Tue, 4 Mar 2025 22:52:11 +0700
Subject: [PATCH] Refactor codebase

---
 .env.example                  |  10 ++
 .gitignore                    |   7 ++
 README.md                     |  19 ++++
 app/backend/database.py       |  13 +++
 app/backend/main.py           |  22 +++++
 app/backend/models/product.py |  11 +++
 app/backend/models/user.py    |  10 ++
 app/backend/routes/auth.py    |  36 ++++++++
 app/backend/schemas/user.py   |  33 +++++++
 app/backend/utils/hashing.py  |  11 +++
 app/core/__init__.py          |   0
 app/core/config.py            |  19 ++++
 requirements.txt              |  48 ++++++++++
 shopApp.py                    | 166 ----------------------------------
 14 files changed, 239 insertions(+), 166 deletions(-)
 create mode 100644 .env.example
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 100644 app/backend/database.py
 create mode 100644 app/backend/main.py
 create mode 100644 app/backend/models/product.py
 create mode 100644 app/backend/models/user.py
 create mode 100644 app/backend/routes/auth.py
 create mode 100644 app/backend/schemas/user.py
 create mode 100644 app/backend/utils/hashing.py
 create mode 100644 app/core/__init__.py
 create mode 100644 app/core/config.py
 create mode 100644 requirements.txt
 delete mode 100644 shopApp.py

diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..6a3cb37
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,10 @@
+# Copy this file to .env
+
+# Change to your own database
+DATABASE_URL = mysql+pymysql://your_user:your_password@localhost/shopping     # This template use mysql
+# change your_user to your mysql username
+# change your_password to your mysql password
+# change localhost to 127.0.0.1 if youre running on local
+
+SECRET_KEY = your_secret_key
+DEBUG = True    # Only True or False
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8710c99
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.venv
+venv
+env
+
+__pycache__
+
+.env
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e6f391a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# 1: Setup
+
+create venv \
+`pip install -r requirements.txt`
+
+# 2: Database
+
+- make sure you have mysql server installed
+- create a schema called `shopping` with `utf8` charset and `utf8_unicode_ci` collection
+
+# 3: Env
+
+create `.env` file based on `.env.example` in the main directory
+
+# 4: Run Backend API on your local machine
+
+- open terminal and use \
+  `uvicorn app.backend.main:app --reload`
+- open `127.0.0.1:8000/docs` on your browser
diff --git a/app/backend/database.py b/app/backend/database.py
new file mode 100644
index 0000000..979b340
--- /dev/null
+++ b/app/backend/database.py
@@ -0,0 +1,13 @@
+from sqlmodel import SQLModel, Session, create_engine
+from app.core.config import settings
+
+engine = create_engine(settings.DATABASE_URL, echo=settings.DEBUG)
+
+
+def init_db():
+    SQLModel.metadata.create_all(engine, checkfirst=True)
+
+
+def get_session():
+    with Session(engine) as session:
+        yield session
diff --git a/app/backend/main.py b/app/backend/main.py
new file mode 100644
index 0000000..b7c46af
--- /dev/null
+++ b/app/backend/main.py
@@ -0,0 +1,22 @@
+import sys
+import os
+
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from fastapi import FastAPI
+from backend.routes import auth
+from backend.database import init_db
+from core.config import settings
+
+app = FastAPI(title="Shopping App", version="1.0.0", debug=settings.DEBUG)
+
+# initialize database
+init_db()
+
+# Include API routes
+app.include_router(auth.router, prefix="/auth", tags=["auth"])
+
+
+@app.get("/")
+async def root():
+    return {"message": "Welcome to the shopping app api"}
diff --git a/app/backend/models/product.py b/app/backend/models/product.py
new file mode 100644
index 0000000..96a79bd
--- /dev/null
+++ b/app/backend/models/product.py
@@ -0,0 +1,11 @@
+from sqlmodel import SQLModel, Field
+from typing import Optional
+
+
+class Product(SQLModel, table=True):
+    id: Optional[int] = Field(default=None, primary_key=True)
+    name: str
+    description: str
+    price: float
+    stock: int
+    image_url: Optional[str] = None
diff --git a/app/backend/models/user.py b/app/backend/models/user.py
new file mode 100644
index 0000000..baf1635
--- /dev/null
+++ b/app/backend/models/user.py
@@ -0,0 +1,10 @@
+from sqlmodel import SQLModel, Field
+from typing import Optional
+
+
+class User(SQLModel, table=True):
+    id: Optional[int] = Field(default=None, primary_key=True)
+    username: str = Field(unique=True, index=True)
+    email: str = Field(unique=True, index=True)
+    password: str
+    role: str  # "buyer" or "shop_owner"
diff --git a/app/backend/routes/auth.py b/app/backend/routes/auth.py
new file mode 100644
index 0000000..e208d7d
--- /dev/null
+++ b/app/backend/routes/auth.py
@@ -0,0 +1,36 @@
+from fastapi import APIRouter, Depends, HTTPException
+from backend.models.user import User
+from backend.schemas.user import UserCreate, UserLogin
+from backend.database import get_session
+from sqlmodel import Session, select
+from backend.utils.hashing import hash_password, verify_password
+
+router = APIRouter()
+
+
+@router.post("/signup")
+def signup(user_data: UserCreate, session: Session = Depends(get_session)):
+    existing_user = session.exec(
+        select(User).where(User.email == user_data.email)
+    ).first()
+    if existing_user:
+        raise HTTPException(status_code=400, detail="Email already registered")
+
+    hashed_password = hash_password(user_data.password)
+    user = User(
+        username=user_data.username,
+        email=user_data.email,
+        password=hashed_password,
+        role="buyer",
+    )
+    session.add(user)
+    session.commit()
+    return {"message": "User created successfully"}
+
+
+@router.post("/login")
+def login(user_data: UserLogin, session: Session = Depends(get_session)):
+    user = session.exec(select(User).where(User.email == user_data.email)).first()
+    if not user or not verify_password(user_data.password, user.password):
+        raise HTTPException(status_code=401, detail="Invalid credentials")
+    return {"message": "Login successful", "user_id": user.id}
diff --git a/app/backend/schemas/user.py b/app/backend/schemas/user.py
new file mode 100644
index 0000000..74f7631
--- /dev/null
+++ b/app/backend/schemas/user.py
@@ -0,0 +1,33 @@
+from pydantic import BaseModel, EmailStr
+from typing import Optional
+
+
+# Schema for user registration
+class UserCreate(BaseModel):
+    username: str
+    email: EmailStr
+    password: str
+
+
+# Schema for user login
+class UserLogin(BaseModel):
+    email: EmailStr
+    password: str
+
+
+# Schema for response after user registration or login
+class UserResponse(BaseModel):
+    id: int
+    username: str
+    email: EmailStr
+    role: str  # "buyer", "shop_owner", "admin"
+
+    class Config:
+        from_attributes = True  # Enables ORM mode for SQLModel compatibility
+
+
+# Schema for updating user profile
+class UserUpdate(BaseModel):
+    username: Optional[str] = None
+    email: Optional[EmailStr] = None
+    password: Optional[str] = None
diff --git a/app/backend/utils/hashing.py b/app/backend/utils/hashing.py
new file mode 100644
index 0000000..3b8f8ce
--- /dev/null
+++ b/app/backend/utils/hashing.py
@@ -0,0 +1,11 @@
+from passlib.context import CryptContext
+
+pwd_context = CryptContext(schemes=["bcrypt"], bcrypt__default_rounds=12)
+
+
+def hash_password(password: str) -> str:
+    return pwd_context.hash(password)
+
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+    return pwd_context.verify(plain_password, hashed_password)
diff --git a/app/core/__init__.py b/app/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/core/config.py b/app/core/config.py
new file mode 100644
index 0000000..0afa9ae
--- /dev/null
+++ b/app/core/config.py
@@ -0,0 +1,19 @@
+import os
+from dotenv import load_dotenv
+
+# Load environment variables from .env (located outside /app/)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ENV_PATH = os.path.join(BASE_DIR, ".env")
+load_dotenv(ENV_PATH)
+
+
+class Settings:
+    DATABASE_URL: str = os.getenv("DATABASE_URL")
+    SECRET_KEY: str = os.getenv("SECRET_KEY")
+    DEBUG: bool = os.getenv("DEBUG", "False").lower() in ["true", "1"]
+
+    if DATABASE_URL is None:
+        raise ValueError("DATABASE_URL is not set or empty")
+
+
+settings = Settings()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..db28911
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,48 @@
+annotated-types==0.7.0
+anyio==4.8.0
+bcrypt==3.2.2
+certifi==2025.1.31
+cffi==1.17.1
+click==8.1.8
+colorama==0.4.6
+dnspython==2.7.0
+email_validator==2.2.0
+fastapi==0.115.11
+fastapi-cli==0.0.7
+greenlet==3.1.1
+h11==0.14.0
+httpcore==1.0.7
+httptools==0.6.4
+httpx==0.28.1
+idna==3.10
+itsdangerous==2.2.0
+Jinja2==3.1.5
+markdown-it-py==3.0.0
+MarkupSafe==3.0.2
+mdurl==0.1.2
+mysql-connector-python==9.2.0
+orjson==3.10.15
+passlib==1.7.4
+pycparser==2.22
+pydantic==2.10.6
+pydantic-extra-types==2.10.2
+pydantic-settings==2.8.1
+pydantic_core==2.27.2
+Pygments==2.19.1
+PyMySQL==1.1.1
+python-dotenv==1.0.1
+python-multipart==0.0.20
+PyYAML==6.0.2
+rich==13.9.4
+rich-toolkit==0.13.2
+shellingham==1.5.4
+sniffio==1.3.1
+SQLAlchemy==2.0.38
+sqlmodel==0.0.23
+starlette==0.46.0
+typer==0.15.2
+typing_extensions==4.12.2
+ujson==5.10.0
+uvicorn==0.34.0
+watchfiles==1.0.4
+websockets==15.0
diff --git a/shopApp.py b/shopApp.py
deleted file mode 100644
index 127fad1..0000000
--- a/shopApp.py
+++ /dev/null
@@ -1,166 +0,0 @@
-from abc import ABC, abstractmethod
-from typing import List, Dict
-
-
-# Single Responsibility Principle (SRP): Class for handling user-related functionalities.
-class User:
-    def __init__(self, user_id: str, name: str, location: str):
-        self.user_id = user_id
-        self.name = name
-        self.location = location
-
-    def update_location(self, new_location: str):
-        self.location = new_location
-
-
-# Store Class for storing store information, stores information like store name, location, and items.
-class Store:
-    def __init__(self, store_id: str, name: str, location: str, items: Dict[str, float]):
-        self.store_id = store_id
-        self.name = name
-        self.location = location
-        self.items = items
-
-    def get_items(self) -> Dict[str, float]:
-        return self.items
-
-    def add_item(self, item_name: str, price: float):
-        self.items[item_name] = price
-
-
-# Open/Closed Principle (OCP): Payment system can be extended by adding new payment types.
-class PaymentMethod(ABC):
-    @abstractmethod
-    def pay(self, amount: float) -> bool:
-        pass
-
-
-class CreditCardPayment(PaymentMethod):
-    def pay(self, amount: float) -> bool:
-        print(f"Paying {amount} with Credit Card")
-        return True
-
-
-class PayPalPayment(PaymentMethod):
-    def pay(self, amount: float) -> bool:
-        print(f"Paying {amount} with PayPal")
-        return True
-
-
-# Liskov Substitution Principle (LSP): User can use different payment methods.
-class ShoppingCart:
-    def __init__(self):
-        self.items = []
-
-    def add_item(self, store: Store, item_name: str):
-        if item_name in store.items:
-            self.items.append((item_name, store.items[item_name]))
-        else:
-            print(f"Item {item_name} not available in store.")
-
-    def total_price(self) -> float:
-        return sum(item[1] for item in self.items)
-
-    def checkout(self, payment_method: PaymentMethod):
-        total = self.total_price()
-        payment_method.pay(total)
-        print(f"Total paid: {total}")
-
-
-# Interface Segregation Principle (ISP): Small interfaces for location services.
-class LocationService(ABC):
-    @abstractmethod
-    def find_nearby_stores(self, user_location: str) -> List[Store]:
-        pass
-
-
-class StoreLocator(LocationService):
-    def __init__(self, stores: List[Store]):
-        self.stores = stores
-
-    def find_nearby_stores(self, user_location: str) -> List[Store]:
-        # Simple logic to return stores near the user based on location
-        return [store for store in self.stores if store.location == user_location]
-
-
-# Dependency Inversion Principle (DIP): High-level modules depend on abstractions.
-class LocationBasedShop:
-    def __init__(self, user: User, store_locator: LocationService):
-        self.user = user
-        self.store_locator = store_locator
-
-    def show_nearby_stores(self):
-        stores = self.store_locator.find_nearby_stores(self.user.location)
-        print(f"Stores near {self.user.name}:")
-        for store in stores:
-            print(f"{store.name} - Items: {store.get_items()}")
-
-    def shop_at_store(self, store: Store, item_name: str, payment_method: PaymentMethod):
-        cart = ShoppingCart()
-        cart.add_item(store, item_name)
-        cart.checkout(payment_method)
-
-
-# Authentication Providers: Handle user authentication through SSO or cookies.
-class AuthenticationProvider(ABC):
-    @abstractmethod
-    def authenticate(self, user_id: str) -> bool:
-        pass
-
-
-class GoogleAuthProvider(AuthenticationProvider):
-    def authenticate(self, user_id: str) -> bool:
-        print(f"Authenticating user {user_id} via Google SSO...")
-        return True  # Mock successful authentication
-
-
-class FacebookAuthProvider(AuthenticationProvider):
-    def authenticate(self, user_id: str) -> bool:
-        print(f"Authenticating user {user_id} via Facebook SSO...")
-        return True  # Mock successful authentication
-
-
-class CookieAuthProvider(AuthenticationProvider):
-    def authenticate(self, user_id: str) -> bool:
-        print(f"Authenticating user {user_id} via Cookies...")
-        return True  # Mock successful authentication
-
-
-# Authentication Manager to handle authentication flow
-class AuthManager:
-    def __init__(self, provider: AuthenticationProvider):
-        self.provider = provider
-
-    def login(self, user: User):
-        if self.provider.authenticate(user.user_id):
-            print(f"User {user.name} authenticated successfully!")
-        else:
-            print(f"Authentication failed for user {user.name}.")
-
-
-# Example Usage
-if __name__ == "__main__":
-    # Initialize stores
-    stores = [
-        Store("1", "Store A", "Location1", {"Item1": 10.0, "Item2": 20.0}),
-        Store("2", "Store B", "Location2", {"Item1": 15.0, "Item3": 25.0}),
-    ]
-
-    # Create a user
-    user = User("1", "Alice", "Location1")
-
-    # Authentication flow
-    google_auth = GoogleAuthProvider()
-    auth_manager = AuthManager(google_auth)
-    auth_manager.login(user)
-
-    # Location and shopping flow
-    store_locator = StoreLocator(stores)
-    location_based_shop = LocationBasedShop(user, store_locator)
-
-    # Show nearby stores
-    location_based_shop.show_nearby_stores()
-
-    # Shopping and checkout
-    store = stores[0]  # Store A
-    location_based_shop.shop_at_store(store, "Item1", CreditCardPayment())
-- 
GitLab