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