├── .gitignore ├── README.md ├── alembic.ini ├── alembic ├── README ├── env.py └── script.py.mako ├── app ├── __init__.py ├── core │ ├── __init__.py │ ├── config.py │ └── security.py ├── db │ ├── __init__.py │ └── database.py ├── main.py ├── models │ ├── __init__.py │ └── models.py ├── routers │ ├── __init__.py │ ├── accounts.py │ ├── auth.py │ ├── carts.py │ ├── categories.py │ ├── products.py │ └── users.py ├── schemas │ ├── __init__.py │ ├── accounts.py │ ├── auth.py │ ├── carts.py │ ├── categories.py │ ├── products.py │ └── users.py ├── services │ ├── __init__.py │ ├── accounts.py │ ├── auth.py │ ├── carts.py │ ├── categories.py │ ├── products.py │ └── users.py └── utils │ ├── __init__.py │ └── responses.py ├── main.py ├── migrate.py ├── requirements.txt └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .idea 3 | .ipynb_checkpoints 4 | .mypy_cache 5 | .vscode 6 | __pycache__ 7 | .pytest_cache 8 | htmlcov 9 | dist 10 | site 11 | .coverage 12 | coverage.xml 13 | .netlify 14 | test.db 15 | log.txt 16 | Pipfile.lock 17 | env3.* 18 | env 19 | docs_build 20 | site_build 21 | venv 22 | docs.zip 23 | archive.zip 24 | # alembic 25 | alembic/versions/ 26 | .env 27 | # vim temporary files 28 | *~ 29 | .*.sw? 30 | .cache 31 | 32 | # macOS 33 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Ecommerce API with Fast API Framework 3 | 4 | A simple Ecommerce API built with Fast API Framework 5 | 6 | ## Table of Contents 7 | 8 | - [Ecommerce API with Fast API Framework](#ecommerce-api-with-fast-api-framework) 9 | - [Table of Contents](#table-of-contents) 10 | - [Demo](#demo) 11 | - [Features](#features) 12 | - [Technologies Used](#technologies-used) 13 | - [API Endpoints](#api-endpoints) 14 | - [Screenshots](#screenshots) 15 | - [Installation](#installation) 16 | - [Usage](#usage) 17 | - [Contributing](#contributing) 18 | - [License](#license) 19 | 20 | 21 | ## Demo 22 | 23 | > [!IMPORTANT] 24 | > The Render.com free plan may experience a short delay (approximately 1 minute) when starting up. Please be patient for the initial access. 25 | 26 | - **Render.com** 27 | - [Swagger](https://ecommerce-jgao.onrender.com/docs) 28 | - [ReDoc](https://ecommerce-jgao.onrender.com/redoc) 29 | 30 | - **Online Code** 31 | - [Github1s](https://github1s.com/aliseyedi01/Ecommerce-Api) 32 | 33 | - **Database** 34 | - [dbdiagram](https://dbdiagram.io/d/6574832756d8064ca0b3b776) 35 | 36 | 37 | ## Features 38 | 39 | - **Product Endpoints:** 40 | - Comprehensive CRUD operations for managing product details, covering creation, retrieval, updating, and deletion. 41 | - **User Authentication:** 42 | - Implementation of secure user authentication using JWT (JSON Web Token) for robust access control and identity verification. 43 | - **Cart Management:** 44 | - Robust operations for managing shopping carts, empowering users to effortlessly add, remove, or update items in their carts. 45 | - **Search and Filter:** 46 | - Implementation of advanced search and filter functionalities to elevate the product browsing experience, allowing users to find specific information efficiently. 47 | - **Account Management:** 48 | - User-friendly operations for managing user accounts, enabling users to retrieve, update, or delete their account information. 49 | - **Swagger / FastAPI Integration:** 50 | - Seamless integration of Swagger UI or ReDoc for comprehensive API documentation. This ensures developers have clear and accessible documentation to understand and utilize the API effectively. 51 | 52 | 53 | ## Technologies Used 54 | 55 | - **FastAPI:** 56 | - A modern, fast web framework for building APIs with Python 3.7+ based on standard Python type hints. 57 | - **PostgreSQL:** 58 | - A powerful open-source relational database management system used for data storage. 59 | - **Supabase:** 60 | - Utilizing Supabase for its real-time database capabilities and other features. 61 | - **JWT Authentication:** 62 | - Implementing JSON Web Token authentication for secure user authentication. 63 | - **Pydantic:** 64 | - A data validation and settings management library for Python, often used with FastAPI. 65 | - **Uvicorn:** 66 | - A lightweight ASGI server that serves FastAPI applications. It is used for running FastAPI applications in production. 67 | - **SQLAlchemy:** 68 | - An SQL toolkit and Object-Relational Mapping (ORM) library for Python, useful for database interactions. 69 | 70 | 71 | 72 | ## API Endpoints 73 | 74 | 75 | 76 | | Endpoint | HTTP Method | Path | Description | User Type | 77 | |-----------------------------------|-------------|-------------------------------------------|---------------------------------------------------------|-----------------| 78 | | Product List | GET | `/products/` | Get a list of all products | User | 79 | | Create Product | POST | `/products/` | Create a new product | Admin | 80 | | Retrieve Product by ID | GET | `/products/{id}/` | Get details of a specific product by ID | User | 81 | | Update Product by ID | PUT | `/products/{id}/` | Update details of a specific product by ID | Admin | 82 | | Delete Product by ID | DELETE | `/products/{id}/` | Delete a specific product by ID | Admin | 83 | | Category List | GET | `/categories/` | Get a list of all categories | User | 84 | | Create Category | POST | `/categories/` | Create a new category | Admin | 85 | | Retrieve Category by ID | GET | `/categories/{id}/` | Get details of a specific category by ID | User | 86 | | Update Category by ID | PUT | `/categories/{id}/` | Update details of a specific category by ID | Admin | 87 | | Delete Category by ID | DELETE | `/categories/{id}/` | Delete a specific category by ID | Admin | 88 | | User List (Admin Only) | GET | `/users/` | Get a list of all users (admin-only) | Admin | 89 | | Get User By ID (Admin Only) | GET | `/users/{user_id}/` | Get details of a specific user by ID (admin-only) | Admin | 90 | | Create User (Admin Only) | POST | `/users/` | Create a new user (admin-only) | Admin | 91 | | Update User By ID (Admin Only) | PUT | `/users/{user_id}/` | Update details of a specific user by ID (admin-only) | Admin | 92 | | Delete User By ID (Admin Only) | DELETE | `/users/{user_id}/` | Delete a specific user by ID (admin-only) | Admin | 93 | | Get My Account Info | GET | `/account/` | Get information about the authenticated user | User | 94 | | Edit My Account Info | PUT | `/account/` | Edit the information of the authenticated user | User | 95 | | Remove My Account | DELETE | `/account/` | Remove the account of the authenticated user | User | 96 | | User Signup | POST | `/auth/signup/` | Register a new user | User | 97 | | User Login | POST | `/auth/login/` | Authenticate and generate access tokens for a user | User | 98 | | Refresh Access Token | POST | `/auth/refresh/` | Refresh an access token using a refresh token | User | 99 | | Swagger UI | - | `/docs/` | Swagger UI for API documentation | User | 100 | | Swagger JSON (without UI) | - | `/openapi.json` | OpenAPI JSON for API documentation without UI | User | 101 | | ReDoc UI | - | `/redoc/` | ReDoc UI for API documentation | User | 102 | 103 | 104 | 105 | ## Screenshots 106 | 107 | ![image](https://github.com/aliseyedi01/Ecommerce-Api/assets/118107025/d7262b0d-161c-4324-b343-27eeb0ec302a) 108 | 109 | 110 | ![image](https://github.com/aliseyedi01/Ecommerce-Api/assets/118107025/0d8bc0bf-0eac-4e96-812d-f3e09783efb0) 111 | 112 | ## Installation 113 | 114 | 1. **Clone the repository:** 115 | 116 | ```bash 117 | git clone https://github.com/aliseyedi01/Ecommerce-Api.git 118 | ``` 119 | 120 | 2. **Navigate to the project directory:** 121 | 122 | ```bash 123 | Ecommerce-Api 124 | ``` 125 | 126 | 3. **Create a virtual environment:** 127 | 128 | ```bash 129 | python3 -m venv venv 130 | ``` 131 | 132 | 4. **Activate the virtual environment:** 133 | 134 | On Windows: 135 | 136 | ```bash 137 | venv\Scripts\activate 138 | ``` 139 | 140 | On macOS and Linux: 141 | 142 | ```bash 143 | source venv/bin/activate 144 | ``` 145 | 146 | 5. **Install dependencies:** 147 | 148 | ```bash 149 | pip install -r requirements.txt 150 | ``` 151 | 152 | ## Usage 153 | 154 | 1. **Run Alembic migrations:** 155 | 156 | ```bash 157 | python migrate.py 158 | ``` 159 | 160 | This will apply any pending database migrations. 161 | 162 | 2. **Run the FastAPI development server:** 163 | 164 | ```bash 165 | python run.py 166 | ``` 167 | 168 | The API will be accessible at [http://127.0.0.1:8000/](http://127.0.0.1:8000/) 169 | 170 | 3. **Access the Swagger UI and ReDoc:** 171 | 172 | - Swagger UI: [http://127.0.0.1:8000/docs/](http://127.0.0.1:8000/docs/) 173 | - ReDoc: [http://127.0.0.1:8000/redoc/](http://127.0.0.1:8000/redoc/) 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | ## Contributing 182 | 183 | Feel free to contribute to the project. Fork the repository, make changes, and submit a pull request. 184 | 185 | ## License 186 | 187 | This project is licensed under the [MIT License](LICENSE). 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | [alembic] 3 | # path to migration scripts 4 | script_location = alembic 5 | 6 | # defaults to the current working directory. 7 | prepend_sys_path = . 8 | 9 | # version_path_separator = space 10 | version_path_separator = os 11 | 12 | sqlalchemy.url = driver://user:pass@localhost/dbname 13 | 14 | [post_write_hooks] 15 | 16 | # Logging configuration 17 | [loggers] 18 | keys = root,sqlalchemy,alembic 19 | 20 | [handlers] 21 | keys = console 22 | 23 | [formatters] 24 | keys = generic 25 | 26 | [logger_root] 27 | level = WARN 28 | handlers = console 29 | qualname = 30 | 31 | [logger_sqlalchemy] 32 | level = WARN 33 | handlers = 34 | qualname = sqlalchemy.engine 35 | 36 | [logger_alembic] 37 | level = INFO 38 | handlers = 39 | qualname = alembic 40 | 41 | [handler_console] 42 | class = StreamHandler 43 | args = (sys.stderr,) 44 | level = NOTSET 45 | formatter = generic 46 | 47 | [formatter_generic] 48 | format = %(levelname)-5.5s [%(name)s] %(message)s 49 | datefmt = %H:%M:%S 50 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | 6 | from alembic import context 7 | from app.core.config import settings 8 | from app.db.database import Base 9 | 10 | config = context.config 11 | 12 | 13 | config.set_main_option( 14 | "sqlalchemy.url", f"postgresql://{settings.db_username}:{settings.db_password}@{settings.db_hostname}:{settings.db_port}/{settings.db_name}") 15 | 16 | 17 | if config.config_file_name is not None: 18 | fileConfig(config.config_file_name) 19 | 20 | target_metadata = Base.metadata 21 | 22 | 23 | def run_migrations_offline() -> None: 24 | url = config.get_main_option("sqlalchemy.url") 25 | context.configure( 26 | url=url, 27 | target_metadata=target_metadata, 28 | literal_binds=True, 29 | dialect_opts={"paramstyle": "named"}, 30 | ) 31 | 32 | with context.begin_transaction(): 33 | context.run_migrations() 34 | 35 | 36 | def run_migrations_online() -> None: 37 | connectable = engine_from_config( 38 | config.get_section(config.config_ini_section, {}), 39 | prefix="sqlalchemy.", 40 | poolclass=pool.NullPool, 41 | ) 42 | 43 | with connectable.connect() as connection: 44 | context.configure( 45 | connection=connection, target_metadata=target_metadata 46 | ) 47 | 48 | with context.begin_transaction(): 49 | context.run_migrations() 50 | 51 | 52 | if context.is_offline_mode(): 53 | run_migrations_offline() 54 | else: 55 | run_migrations_online() 56 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | ${imports if imports else ""} 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = ${repr(up_revision)} 16 | down_revision: Union[str, None] = ${repr(down_revision)} 17 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} 18 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} 19 | 20 | 21 | def upgrade() -> None: 22 | ${upgrades if upgrades else "pass"} 23 | 24 | 25 | def downgrade() -> None: 26 | ${downgrades if downgrades else "pass"} 27 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliseyedi01/FastAPI-Ecommerce-API/5aff219ceba7efbb97d3d7866256fccda640af1c/app/__init__.py -------------------------------------------------------------------------------- /app/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliseyedi01/FastAPI-Ecommerce-API/5aff219ceba7efbb97d3d7866256fccda640af1c/app/core/__init__.py -------------------------------------------------------------------------------- /app/core/config.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings 2 | 3 | 4 | class Settings(BaseSettings): 5 | # Database Config 6 | db_username: str 7 | db_password: str 8 | db_hostname: str 9 | db_port: str 10 | db_name: str 11 | 12 | # JWT Config 13 | secret_key: str 14 | algorithm: str 15 | access_token_expire_minutes: int 16 | 17 | class Config: 18 | env_file = ".env" 19 | 20 | 21 | settings = Settings() 22 | -------------------------------------------------------------------------------- /app/core/security.py: -------------------------------------------------------------------------------- 1 | from fastapi.security.http import HTTPAuthorizationCredentials 2 | from passlib.context import CryptContext 3 | from datetime import datetime, timedelta 4 | from app.core.config import settings 5 | from jose import JWTError, jwt 6 | from app.schemas.auth import TokenResponse 7 | from fastapi.encoders import jsonable_encoder 8 | from fastapi import HTTPException, Depends, status 9 | from app.models.models import User 10 | from sqlalchemy.orm import Session 11 | from fastapi.security import HTTPBearer 12 | from app.db.database import get_db 13 | from app.utils.responses import ResponseHandler 14 | 15 | 16 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 17 | auth_scheme = HTTPBearer() 18 | 19 | # Create Hash Password 20 | 21 | 22 | def get_password_hash(password): 23 | return pwd_context.hash(password) 24 | 25 | 26 | # Verify Hash Password 27 | def verify_password(plain_password, hashed_password): 28 | return pwd_context.verify(plain_password, hashed_password) 29 | 30 | 31 | # Create Access & Refresh Token 32 | async def get_user_token(id: int, refresh_token=None): 33 | payload = {"id": id} 34 | 35 | access_token_expiry = timedelta(minutes=settings.access_token_expire_minutes) 36 | 37 | access_token = await create_access_token(payload, access_token_expiry) 38 | 39 | if not refresh_token: 40 | refresh_token = await create_refresh_token(payload) 41 | 42 | return TokenResponse( 43 | access_token=access_token, 44 | refresh_token=refresh_token, 45 | expires_in=access_token_expiry.seconds 46 | ) 47 | 48 | 49 | # Create Access Token 50 | async def create_access_token(data: dict, access_token_expiry=None): 51 | payload = data.copy() 52 | 53 | expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes) 54 | payload.update({"exp": expire}) 55 | 56 | return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm) 57 | 58 | 59 | # Create Refresh Token 60 | async def create_refresh_token(data): 61 | return jwt.encode(data, settings.secret_key, settings.algorithm) 62 | 63 | 64 | # Get Payload Of Token 65 | def get_token_payload(token): 66 | try: 67 | return jwt.decode(token, settings.secret_key, [settings.algorithm]) 68 | except JWTError: 69 | raise ResponseHandler.invalid_token('access') 70 | 71 | 72 | def get_current_user(token): 73 | user = get_token_payload(token.credentials) 74 | return user.get('id') 75 | 76 | 77 | def check_admin_role( 78 | token: HTTPAuthorizationCredentials = Depends(auth_scheme), 79 | db: Session = Depends(get_db)): 80 | user = get_token_payload(token.credentials) 81 | user_id = user.get('id') 82 | role_user = db.query(User).filter(User.id == user_id).first() 83 | if role_user.role != "admin": 84 | raise HTTPException(status_code=403, detail="Admin role required") 85 | -------------------------------------------------------------------------------- /app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliseyedi01/FastAPI-Ecommerce-API/5aff219ceba7efbb97d3d7866256fccda640af1c/app/db/__init__.py -------------------------------------------------------------------------------- /app/db/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | from typing import Generator 5 | from app.core.config import settings 6 | 7 | 8 | DATABASE_URL = f"postgresql://{settings.db_username}:{settings.db_password}@{settings.db_hostname}:{settings.db_port}/{settings.db_name}" 9 | 10 | # Establish a connection to the PostgreSQL database 11 | engine = create_engine(DATABASE_URL) 12 | 13 | 14 | # Create database tables based on the defined SQLAlchemy models (subclasses of the Base class) 15 | Base = declarative_base() 16 | Base.metadata.create_all(engine) 17 | 18 | 19 | # Connect to the database and provide a session for interacting with it 20 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 21 | 22 | 23 | def get_db() -> Generator: 24 | db = SessionLocal() 25 | try: 26 | yield db 27 | finally: 28 | db.close() 29 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from app.routers import products, categories, carts, users, auth, accounts 2 | from fastapi import FastAPI 3 | 4 | 5 | description = """ 6 | Welcome to the E-commerce API! 🚀 7 | 8 | This API provides a comprehensive set of functionalities for managing your e-commerce platform. 9 | 10 | Key features include: 11 | 12 | - **Crud** 13 | - Create, Read, Update, and Delete endpoints. 14 | - **Search** 15 | - Find specific information with parameters and pagination. 16 | - **Auth** 17 | - Verify user/system identity. 18 | - Secure with Access and Refresh tokens. 19 | - **Permission** 20 | - Assign roles with specific permissions. 21 | - Different access levels for User/Admin. 22 | - **Validation** 23 | - Ensure accurate and secure input data. 24 | 25 | 26 | For any inquiries, please contact: 27 | 28 | * Github: https://github.com/aliseyedi01 29 | """ 30 | 31 | 32 | app = FastAPI( 33 | description=description, 34 | title="E-commerce API", 35 | version="1.0.0", 36 | contact={ 37 | "name": "Ali Seyedi", 38 | "url": "https://github.com/aliseyedi01", 39 | }, 40 | swagger_ui_parameters={ 41 | "syntaxHighlight.theme": "monokai", 42 | "layout": "BaseLayout", 43 | "filter": True, 44 | "tryItOutEnabled": True, 45 | "onComplete": "Ok" 46 | }, 47 | ) 48 | 49 | 50 | app.include_router(products.router) 51 | app.include_router(categories.router) 52 | app.include_router(carts.router) 53 | app.include_router(users.router) 54 | app.include_router(accounts.router) 55 | app.include_router(auth.router) 56 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliseyedi01/FastAPI-Ecommerce-API/5aff219ceba7efbb97d3d7866256fccda640af1c/app/models/__init__.py -------------------------------------------------------------------------------- /app/models/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, Integer, String, ForeignKey, Float, ARRAY, Enum 2 | from sqlalchemy.sql.expression import text 3 | from sqlalchemy.sql.sqltypes import TIMESTAMP 4 | from sqlalchemy.orm import relationship 5 | from app.db.database import Base 6 | 7 | 8 | class User(Base): 9 | __tablename__ = "users" 10 | 11 | id = Column(Integer, primary_key=True, nullable=False, unique=True, autoincrement=True) 12 | username = Column(String, unique=True, nullable=False) 13 | email = Column(String, unique=True, nullable=False) 14 | password = Column(String, nullable=False) 15 | full_name = Column(String, nullable=False) 16 | is_active = Column(Boolean, server_default="True", nullable=False) 17 | created_at = Column(TIMESTAMP(timezone=True), server_default=text("NOW()"), nullable=False) 18 | 19 | # New column for role 20 | role = Column(Enum("admin", "user", name="user_roles"), nullable=False, server_default="user") 21 | 22 | # Relationship with carts 23 | carts = relationship("Cart", back_populates="user") 24 | 25 | 26 | class Cart(Base): 27 | __tablename__ = "carts" 28 | 29 | id = Column(Integer, primary_key=True, nullable=False, unique=True, autoincrement=True) 30 | user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) 31 | created_at = Column(TIMESTAMP(timezone=True), server_default=text("NOW()"), nullable=False) 32 | total_amount = Column(Float, nullable=False) 33 | 34 | # Relationship with user 35 | user = relationship("User", back_populates="carts") 36 | 37 | # Relationship with cart items 38 | cart_items = relationship("CartItem", back_populates="cart") 39 | 40 | 41 | class CartItem(Base): 42 | __tablename__ = "cart_items" 43 | 44 | id = Column(Integer, primary_key=True, nullable=False, unique=True, autoincrement=True) 45 | cart_id = Column(Integer, ForeignKey("carts.id", ondelete="CASCADE"), nullable=False) 46 | product_id = Column(Integer, ForeignKey("products.id", ondelete="CASCADE"), nullable=False) 47 | quantity = Column(Integer, nullable=False) 48 | subtotal = Column(Float, nullable=False) 49 | 50 | # Relationship with cart and product 51 | cart = relationship("Cart", back_populates="cart_items") 52 | product = relationship("Product", back_populates="cart_items") 53 | 54 | 55 | class Category(Base): 56 | __tablename__ = "categories" 57 | 58 | id = Column(Integer, primary_key=True, nullable=False, unique=True, autoincrement=True) 59 | name = Column(String, unique=True, nullable=False) 60 | 61 | # Relationship with products 62 | products = relationship("Product", back_populates="category") 63 | 64 | 65 | class Product(Base): 66 | __tablename__ = "products" 67 | 68 | id = Column(Integer, primary_key=True, nullable=False, unique=True, autoincrement=True) 69 | title = Column(String, nullable=False) 70 | description = Column(String, nullable=False) 71 | price = Column(Integer, nullable=False) 72 | discount_percentage = Column(Float, nullable=False) 73 | rating = Column(Float, nullable=False) 74 | stock = Column(Integer, nullable=False) 75 | brand = Column(String, nullable=False) 76 | thumbnail = Column(String, nullable=False) 77 | images = Column(ARRAY(String), nullable=False) 78 | is_published = Column(Boolean, server_default="True", nullable=False) 79 | created_at = Column(TIMESTAMP(timezone=True), server_default=text("NOW()"), nullable=False) 80 | 81 | # Relationship with category 82 | category_id = Column(Integer, ForeignKey("categories.id", ondelete="CASCADE"), nullable=False) 83 | category = relationship("Category", back_populates="products") 84 | 85 | # Relationship with cart items 86 | cart_items = relationship("CartItem", back_populates="product") 87 | -------------------------------------------------------------------------------- /app/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliseyedi01/FastAPI-Ecommerce-API/5aff219ceba7efbb97d3d7866256fccda640af1c/app/routers/__init__.py -------------------------------------------------------------------------------- /app/routers/accounts.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | from app.db.database import get_db 3 | from app.services.accounts import AccountService 4 | from sqlalchemy.orm import Session 5 | from fastapi.security import HTTPBearer 6 | from app.schemas.accounts import AccountOut, AccountUpdate 7 | from fastapi.security import HTTPBearer 8 | from app.core.security import auth_scheme 9 | from fastapi.security.http import HTTPAuthorizationCredentials 10 | 11 | 12 | router = APIRouter(tags=["Account"], prefix="/me") 13 | auth_scheme = HTTPBearer() 14 | 15 | 16 | @router.get("/", response_model=AccountOut) 17 | def get_my_info( 18 | db: Session = Depends(get_db), 19 | token: HTTPAuthorizationCredentials = Depends(auth_scheme)): 20 | return AccountService.get_my_info(db, token) 21 | 22 | 23 | @router.put("/", response_model=AccountOut) 24 | def edit_my_info( 25 | updated_user: AccountUpdate, 26 | db: Session = Depends(get_db), 27 | token: HTTPAuthorizationCredentials = Depends(auth_scheme)): 28 | return AccountService.edit_my_info(db, token, updated_user) 29 | 30 | 31 | @router.delete("/", response_model=AccountOut) 32 | def remove_my_account( 33 | db: Session = Depends(get_db), 34 | token: HTTPAuthorizationCredentials = Depends(auth_scheme)): 35 | return AccountService.remove_my_account(db, token) 36 | -------------------------------------------------------------------------------- /app/routers/auth.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, status, Header 2 | from sqlalchemy.orm import Session 3 | from app.services.auth import AuthService 4 | from app.db.database import get_db 5 | from fastapi.security.oauth2 import OAuth2PasswordRequestForm 6 | from app.schemas.auth import UserOut, Signup 7 | 8 | 9 | router = APIRouter(tags=["Auth"], prefix="/auth") 10 | 11 | 12 | @router.post("/signup", status_code=status.HTTP_200_OK, response_model=UserOut) 13 | async def user_login( 14 | user: Signup, 15 | db: Session = Depends(get_db)): 16 | return await AuthService.signup(db, user) 17 | 18 | 19 | @router.post("/login", status_code=status.HTTP_200_OK) 20 | async def user_login( 21 | user_credentials: OAuth2PasswordRequestForm = Depends(), 22 | db: Session = Depends(get_db)): 23 | return await AuthService.login(user_credentials, db) 24 | 25 | 26 | @router.post("/refresh", status_code=status.HTTP_200_OK) 27 | async def refresh_access_token( 28 | refresh_token: str = Header(), 29 | db: Session = Depends(get_db)): 30 | return await AuthService.get_refresh_token(token=refresh_token, db=db) 31 | -------------------------------------------------------------------------------- /app/routers/carts.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Query, status 2 | from app.db.database import get_db 3 | from app.services.carts import CartService 4 | from sqlalchemy.orm import Session 5 | from app.schemas.carts import CartCreate, CartUpdate, CartOut, CartOutDelete, CartsOutList 6 | from app.core.security import get_current_user 7 | from fastapi.security import HTTPBearer 8 | from fastapi.security.http import HTTPAuthorizationCredentials 9 | 10 | router = APIRouter(tags=["Carts"], prefix="/carts") 11 | auth_scheme = HTTPBearer() 12 | 13 | 14 | # Get All Carts 15 | @router.get("/", status_code=status.HTTP_200_OK, response_model=CartsOutList) 16 | def get_all_carts( 17 | db: Session = Depends(get_db), 18 | page: int = Query(1, ge=1, description="Page number"), 19 | limit: int = Query(10, ge=1, le=100, description="Items per page"), 20 | token: HTTPAuthorizationCredentials = Depends(auth_scheme) 21 | ): 22 | return CartService.get_all_carts(token, db, page, limit) 23 | 24 | 25 | # Get Cart By User ID 26 | @router.get("/{cart_id}", status_code=status.HTTP_200_OK, response_model=CartOut) 27 | def get_cart( 28 | cart_id: int, 29 | db: Session = Depends(get_db), 30 | token: HTTPAuthorizationCredentials = Depends(auth_scheme)): 31 | return CartService.get_cart(token, db, cart_id) 32 | 33 | 34 | # Create New Cart 35 | @router.post("/", status_code=status.HTTP_201_CREATED, response_model=CartOut) 36 | def create_cart( 37 | cart: CartCreate, db: Session = Depends(get_db), 38 | token: HTTPAuthorizationCredentials = Depends(auth_scheme)): 39 | return CartService.create_cart(token, db, cart) 40 | 41 | 42 | # Update Existing Cart 43 | @router.put("/{cart_id}", status_code=status.HTTP_200_OK, response_model=CartOut) 44 | def update_cart( 45 | cart_id: int, 46 | updated_cart: CartUpdate, 47 | db: Session = Depends(get_db), 48 | token: HTTPAuthorizationCredentials = Depends(auth_scheme)): 49 | return CartService.update_cart(token, db, cart_id, updated_cart) 50 | 51 | 52 | # Delete Cart By User ID 53 | @router.delete("/{cart_id}", status_code=status.HTTP_200_OK, response_model=CartOutDelete) 54 | def delete_cart( 55 | cart_id: int, db: Session = Depends(get_db), 56 | token: HTTPAuthorizationCredentials = Depends(auth_scheme)): 57 | return CartService.delete_cart(token, db, cart_id) 58 | -------------------------------------------------------------------------------- /app/routers/categories.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Query, status 2 | from app.db.database import get_db 3 | from app.services.categories import CategoryService 4 | from sqlalchemy.orm import Session 5 | from app.schemas.categories import CategoryCreate, CategoryOut, CategoriesOut, CategoryOutDelete, CategoryUpdate 6 | from app.core.security import check_admin_role 7 | 8 | 9 | router = APIRouter(tags=["Categories"], prefix="/categories") 10 | 11 | 12 | # Get All Categories 13 | @router.get( 14 | "/", 15 | status_code=status.HTTP_200_OK, 16 | response_model=CategoriesOut) 17 | def get_all_categories( 18 | db: Session = Depends(get_db), 19 | page: int = Query(1, ge=1, description="Page number"), 20 | limit: int = Query(10, ge=1, le=100, description="Items per page"), 21 | search: str | None = Query("", description="Search based name of categories"), 22 | ): 23 | return CategoryService.get_all_categories(db, page, limit, search) 24 | 25 | 26 | # Get Category By ID 27 | @router.get( 28 | "/{category_id}", 29 | status_code=status.HTTP_200_OK, 30 | response_model=CategoryOut) 31 | def get_category(category_id: int, db: Session = Depends(get_db)): 32 | return CategoryService.get_category(db, category_id) 33 | 34 | 35 | # Create New Category 36 | @router.post( 37 | "/", 38 | status_code=status.HTTP_201_CREATED, 39 | response_model=CategoryOut, 40 | dependencies=[Depends(check_admin_role)]) 41 | def create_category(category: CategoryCreate, db: Session = Depends(get_db)): 42 | return CategoryService.create_category(db, category) 43 | 44 | 45 | # Update Existing Category 46 | @router.put( 47 | "/{category_id}", 48 | status_code=status.HTTP_200_OK, 49 | response_model=CategoryOut, 50 | dependencies=[Depends(check_admin_role)]) 51 | def update_category(category_id: int, updated_category: CategoryUpdate, db: Session = Depends(get_db)): 52 | return CategoryService.update_category(db, category_id, updated_category) 53 | 54 | 55 | # Delete Category By ID 56 | @router.delete( 57 | "/{category_id}", 58 | status_code=status.HTTP_200_OK, 59 | response_model=CategoryOutDelete, 60 | dependencies=[Depends(check_admin_role)]) 61 | def delete_category(category_id: int, db: Session = Depends(get_db)): 62 | return CategoryService.delete_category(db, category_id) 63 | -------------------------------------------------------------------------------- /app/routers/products.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Query, status 2 | from app.db.database import get_db 3 | from app.services.products import ProductService 4 | from sqlalchemy.orm import Session 5 | from app.schemas.products import ProductCreate, ProductOut, ProductsOut, ProductOutDelete, ProductUpdate 6 | from app.core.security import get_current_user, check_admin_role 7 | 8 | 9 | router = APIRouter(tags=["Products"], prefix="/products") 10 | 11 | 12 | # Get All Products 13 | @router.get("/", status_code=status.HTTP_200_OK, response_model=ProductsOut) 14 | def get_all_products( 15 | db: Session = Depends(get_db), 16 | page: int = Query(1, ge=1, description="Page number"), 17 | limit: int = Query(10, ge=1, le=100, description="Items per page"), 18 | search: str | None = Query("", description="Search based title of products"), 19 | ): 20 | return ProductService.get_all_products(db, page, limit, search) 21 | 22 | 23 | # Get Product By ID 24 | @router.get("/{product_id}", status_code=status.HTTP_200_OK, response_model=ProductOut) 25 | def get_product(product_id: int, db: Session = Depends(get_db)): 26 | return ProductService.get_product(db, product_id) 27 | 28 | 29 | # Create New Product 30 | @router.post( 31 | "/", 32 | status_code=status.HTTP_201_CREATED, 33 | response_model=ProductOut, 34 | dependencies=[Depends(check_admin_role)]) 35 | def create_product( 36 | product: ProductCreate, 37 | db: Session = Depends(get_db)): 38 | return ProductService.create_product(db, product) 39 | 40 | 41 | # Update Exist Product 42 | @router.put( 43 | "/{product_id}", 44 | status_code=status.HTTP_200_OK, 45 | response_model=ProductOut, 46 | dependencies=[Depends(check_admin_role)]) 47 | def update_product( 48 | product_id: int, 49 | updated_product: ProductUpdate, 50 | db: Session = Depends(get_db)): 51 | return ProductService.update_product(db, product_id, updated_product) 52 | 53 | 54 | # Delete Product By ID 55 | @router.delete( 56 | "/{product_id}", 57 | status_code=status.HTTP_200_OK, 58 | response_model=ProductOutDelete, 59 | dependencies=[Depends(check_admin_role)]) 60 | def delete_product( 61 | product_id: int, 62 | db: Session = Depends(get_db)): 63 | return ProductService.delete_product(db, product_id) 64 | -------------------------------------------------------------------------------- /app/routers/users.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Query, status 2 | from app.db.database import get_db 3 | from app.services.users import UserService 4 | from sqlalchemy.orm import Session 5 | from app.schemas.users import UserCreate, UserOut, UsersOut, UserOutDelete, UserUpdate 6 | from app.core.security import check_admin_role 7 | 8 | 9 | router = APIRouter(tags=["Users"], prefix="/users") 10 | 11 | 12 | # Get All Users 13 | @router.get( 14 | "/", 15 | status_code=status.HTTP_200_OK, 16 | response_model=UsersOut, 17 | dependencies=[Depends(check_admin_role)]) 18 | def get_all_users( 19 | db: Session = Depends(get_db), 20 | page: int = Query(1, ge=1, description="Page number"), 21 | limit: int = Query(10, ge=1, le=100, description="Items per page"), 22 | search: str | None = Query("", description="Search based username"), 23 | role: str = Query("user", enum=["user", "admin"]) 24 | ): 25 | return UserService.get_all_users(db, page, limit, search, role) 26 | 27 | 28 | # Get User By ID 29 | @router.get( 30 | "/{user_id}", 31 | status_code=status.HTTP_200_OK, 32 | response_model=UserOut, 33 | dependencies=[Depends(check_admin_role)]) 34 | def get_user(user_id: int, db: Session = Depends(get_db)): 35 | return UserService.get_user(db, user_id) 36 | 37 | 38 | # Create New User 39 | @router.post( 40 | "/", 41 | status_code=status.HTTP_201_CREATED, 42 | response_model=UserOut, 43 | dependencies=[Depends(check_admin_role)]) 44 | def create_user(user: UserCreate, db: Session = Depends(get_db)): 45 | return UserService.create_user(db, user) 46 | 47 | 48 | # Update Existing User 49 | @router.put( 50 | "/{user_id}", 51 | status_code=status.HTTP_200_OK, 52 | response_model=UserOut, 53 | dependencies=[Depends(check_admin_role)]) 54 | def update_user(user_id: int, updated_user: UserUpdate, db: Session = Depends(get_db)): 55 | return UserService.update_user(db, user_id, updated_user) 56 | 57 | 58 | # Delete User By ID 59 | @router.delete( 60 | "/{user_id}", 61 | status_code=status.HTTP_200_OK, 62 | response_model=UserOutDelete, 63 | dependencies=[Depends(check_admin_role)]) 64 | def delete_user(user_id: int, db: Session = Depends(get_db)): 65 | return UserService.delete_user(db, user_id) 66 | -------------------------------------------------------------------------------- /app/schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliseyedi01/FastAPI-Ecommerce-API/5aff219ceba7efbb97d3d7866256fccda640af1c/app/schemas/__init__.py -------------------------------------------------------------------------------- /app/schemas/accounts.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr 2 | from datetime import datetime 3 | from typing import List 4 | from app.schemas.carts import CartBase 5 | 6 | 7 | class AccountBase(BaseModel): 8 | id: int 9 | username: str 10 | email: EmailStr 11 | full_name: str 12 | role: str 13 | is_active: bool 14 | created_at: datetime 15 | carts: List[CartBase] 16 | 17 | class Config: 18 | from_attributes = True 19 | 20 | 21 | class AccountUpdate(BaseModel): 22 | username: str 23 | email: EmailStr 24 | full_name: str 25 | 26 | 27 | class AccountOut(BaseModel): 28 | message: str 29 | data: AccountBase 30 | 31 | class Config: 32 | from_attributes = True 33 | -------------------------------------------------------------------------------- /app/schemas/auth.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr 2 | from datetime import datetime 3 | from typing import List 4 | from app.schemas.carts import CartBase 5 | 6 | 7 | # Base 8 | class BaseConfig: 9 | from_attributes = True 10 | 11 | 12 | class UserBase(BaseModel): 13 | id: int 14 | username: str 15 | email: EmailStr 16 | full_name: str 17 | password: str 18 | role: str 19 | is_active: bool 20 | created_at: datetime 21 | carts: List[CartBase] 22 | 23 | class Config(BaseConfig): 24 | pass 25 | 26 | 27 | class Signup(BaseModel): 28 | full_name: str 29 | username: str 30 | email: str 31 | password: str 32 | 33 | class Config(BaseConfig): 34 | pass 35 | 36 | 37 | class UserOut(BaseModel): 38 | message: str 39 | data: UserBase 40 | 41 | class Config(BaseConfig): 42 | pass 43 | 44 | 45 | # Token 46 | class TokenResponse(BaseModel): 47 | access_token: str 48 | refresh_token: str 49 | token_type: str = 'Bearer' 50 | expires_in: int 51 | -------------------------------------------------------------------------------- /app/schemas/carts.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from typing import List 3 | from datetime import datetime 4 | from app.schemas.products import ProductBase, CategoryBase 5 | 6 | 7 | # Base Config 8 | class BaseConfig: 9 | from_attributes = True 10 | 11 | 12 | class ProductBaseCart(ProductBase): 13 | category: CategoryBase = Field(exclude=True) 14 | 15 | class Config(BaseConfig): 16 | pass 17 | 18 | 19 | # Base Cart & Cart_Item 20 | class CartItemBase(BaseModel): 21 | id: int 22 | product_id: int 23 | quantity: int 24 | subtotal: float 25 | product: ProductBaseCart 26 | 27 | 28 | class CartBase(BaseModel): 29 | id: int 30 | user_id: int 31 | created_at: datetime 32 | total_amount: float 33 | cart_items: List[CartItemBase] 34 | 35 | class Config(BaseConfig): 36 | pass 37 | 38 | 39 | class CartOutBase(BaseModel): 40 | id: int 41 | user_id: int 42 | created_at: datetime 43 | total_amount: float 44 | cart_items: List[CartItemBase] 45 | 46 | class Config(BaseConfig): 47 | pass 48 | 49 | 50 | # Get Cart 51 | class CartOut(BaseModel): 52 | message: str 53 | data: CartBase 54 | 55 | class Config(BaseConfig): 56 | pass 57 | 58 | 59 | class CartsOutList(BaseModel): 60 | message: str 61 | data: List[CartBase] 62 | 63 | 64 | class CartsUserOutList(BaseModel): 65 | message: str 66 | data: List[CartBase] 67 | 68 | class Config(BaseConfig): 69 | pass 70 | 71 | 72 | # Delete Cart 73 | class CartOutDelete(BaseModel): 74 | message: str 75 | data: CartOutBase 76 | 77 | 78 | # Create Cart 79 | class CartItemCreate(BaseModel): 80 | product_id: int 81 | quantity: int 82 | 83 | 84 | class CartCreate(BaseModel): 85 | cart_items: List[CartItemCreate] 86 | 87 | class Config(BaseConfig): 88 | pass 89 | 90 | 91 | # Update Cart 92 | class CartUpdate(CartCreate): 93 | pass 94 | -------------------------------------------------------------------------------- /app/schemas/categories.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from pydantic import BaseModel, Field 3 | 4 | 5 | class CategoryBase(BaseModel): 6 | id: int 7 | name: str 8 | 9 | 10 | class CategoryCreate(BaseModel): 11 | name: str 12 | 13 | 14 | class CategoryUpdate(BaseModel): 15 | name: str 16 | 17 | 18 | class CategoryOut(BaseModel): 19 | message: str 20 | data: CategoryBase 21 | 22 | 23 | class CategoriesOut(BaseModel): 24 | message: str 25 | data: List[CategoryBase] 26 | 27 | 28 | class CategoryDelete(BaseModel): 29 | id: int 30 | name: str 31 | 32 | 33 | class CategoryOutDelete(BaseModel): 34 | message: str 35 | data: CategoryDelete 36 | -------------------------------------------------------------------------------- /app/schemas/products.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, validator 2 | from datetime import datetime 3 | from typing import List, Optional, ClassVar 4 | from app.schemas.categories import CategoryBase 5 | 6 | 7 | # Base Models 8 | class BaseConfig: 9 | from_attributes = True 10 | 11 | 12 | class ProductBase(BaseModel): 13 | id: int 14 | title: str 15 | description: Optional[str] 16 | price: int 17 | 18 | @validator("discount_percentage", pre=True) 19 | def validate_discount_percentage(cls, v): 20 | if v < 0 or v > 100: 21 | raise ValueError("discount_percentage must be between 0 and 100") 22 | return v 23 | 24 | discount_percentage: float 25 | rating: float 26 | stock: int 27 | brand: str 28 | thumbnail: str 29 | images: List[str] 30 | is_published: bool 31 | created_at: datetime 32 | category_id: int 33 | category: CategoryBase 34 | 35 | class Config(BaseConfig): 36 | pass 37 | 38 | 39 | # Create Product 40 | class ProductCreate(ProductBase): 41 | id: ClassVar[int] 42 | category: ClassVar[CategoryBase] 43 | 44 | class Config(BaseConfig): 45 | pass 46 | 47 | 48 | # Update Product 49 | class ProductUpdate(ProductCreate): 50 | pass 51 | 52 | 53 | # Get Products 54 | class ProductOut(BaseModel): 55 | message: str 56 | data: ProductBase 57 | 58 | class Config(BaseConfig): 59 | pass 60 | 61 | 62 | class ProductsOut(BaseModel): 63 | message: str 64 | data: List[ProductBase] 65 | 66 | class Config(BaseConfig): 67 | pass 68 | 69 | 70 | # Delete Product 71 | class ProductDelete(ProductBase): 72 | category: ClassVar[CategoryBase] 73 | 74 | 75 | class ProductOutDelete(BaseModel): 76 | message: str 77 | data: ProductDelete 78 | -------------------------------------------------------------------------------- /app/schemas/users.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel , EmailStr 2 | from typing import List 3 | from datetime import datetime 4 | from app.schemas.carts import CartBase 5 | 6 | 7 | class BaseConfig: 8 | from_attributes = True 9 | 10 | 11 | class UserBase(BaseModel): 12 | id: int 13 | username: str 14 | email: EmailStr 15 | full_name: str 16 | password: str 17 | role: str 18 | is_active: bool 19 | created_at: datetime 20 | carts: List[CartBase] 21 | 22 | class Config(BaseConfig): 23 | pass 24 | 25 | 26 | class UserCreate(BaseModel): 27 | full_name: str 28 | username: str 29 | email: str 30 | password: str 31 | 32 | class Config(BaseConfig): 33 | pass 34 | 35 | 36 | class UserUpdate(UserCreate): 37 | pass 38 | 39 | 40 | class UserOut(BaseModel): 41 | message: str 42 | data: UserBase 43 | 44 | class Config(BaseConfig): 45 | pass 46 | 47 | 48 | class UsersOut(BaseModel): 49 | message: str 50 | data: List[UserBase] 51 | 52 | class Config(BaseConfig): 53 | pass 54 | 55 | 56 | class UserOutDelete(BaseModel): 57 | message: str 58 | data: UserBase 59 | 60 | class Config(BaseConfig): 61 | pass 62 | -------------------------------------------------------------------------------- /app/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliseyedi01/FastAPI-Ecommerce-API/5aff219ceba7efbb97d3d7866256fccda640af1c/app/services/__init__.py -------------------------------------------------------------------------------- /app/services/accounts.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from app.models.models import User 3 | from app.utils.responses import ResponseHandler 4 | from app.core.security import get_password_hash, get_token_payload 5 | 6 | 7 | class AccountService: 8 | @staticmethod 9 | def get_my_info(db: Session, token): 10 | user_id = get_token_payload(token.credentials).get('id') 11 | user = db.query(User).filter(User.id == user_id).first() 12 | if not user: 13 | ResponseHandler.not_found_error("User", user_id) 14 | return ResponseHandler.get_single_success(user.username, user.id, user) 15 | 16 | @staticmethod 17 | def edit_my_info(db: Session, token, updated_user): 18 | user_id = get_token_payload(token.credentials).get('id') 19 | db_user = db.query(User).filter(User.id == user_id).first() 20 | if not db_user: 21 | ResponseHandler.not_found_error("User", user_id) 22 | 23 | for key, value in updated_user.model_dump().items(): 24 | setattr(db_user, key, value) 25 | 26 | db.commit() 27 | db.refresh(db_user) 28 | return ResponseHandler.update_success(db_user.username, db_user.id, db_user) 29 | 30 | @staticmethod 31 | def remove_my_account(db: Session, token): 32 | user_id = get_token_payload(token.credentials).get('id') 33 | db_user = db.query(User).filter(User.id == user_id).first() 34 | if not db_user: 35 | ResponseHandler.not_found_error("User", user_id) 36 | db.delete(db_user) 37 | db.commit() 38 | return ResponseHandler.delete_success(db_user.username, db_user.id, db_user) 39 | -------------------------------------------------------------------------------- /app/services/auth.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException, Depends, status 2 | from fastapi.security.oauth2 import OAuth2PasswordBearer, OAuth2PasswordRequestForm 3 | from sqlalchemy.orm import Session 4 | from app.models.models import User 5 | from app.db.database import get_db 6 | from app.core.security import verify_password, get_user_token, get_token_payload 7 | from app.core.security import get_password_hash 8 | from app.utils.responses import ResponseHandler 9 | from app.schemas.auth import Signup 10 | 11 | 12 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") 13 | 14 | 15 | class AuthService: 16 | @staticmethod 17 | async def login(user_credentials: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): 18 | user = db.query(User).filter(User.username == user_credentials.username).first() 19 | if not user: 20 | raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid Credentials") 21 | 22 | if not verify_password(user_credentials.password, user.password): 23 | raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid Credentials") 24 | 25 | return await get_user_token(id=user.id) 26 | 27 | @staticmethod 28 | async def signup(db: Session, user: Signup): 29 | hashed_password = get_password_hash(user.password) 30 | user.password = hashed_password 31 | db_user = User(id=None, **user.model_dump()) 32 | db.add(db_user) 33 | db.commit() 34 | db.refresh(db_user) 35 | return ResponseHandler.create_success(db_user.username, db_user.id, db_user) 36 | 37 | @staticmethod 38 | async def get_refresh_token(token, db): 39 | payload = get_token_payload(token) 40 | user_id = payload.get('id', None) 41 | if not user_id: 42 | raise ResponseHandler.invalid_token('refresh') 43 | 44 | user = db.query(User).filter(User.id == user_id).first() 45 | if not user: 46 | raise ResponseHandler.invalid_token('refresh') 47 | 48 | return await get_user_token(id=user.id, refresh_token=token) 49 | -------------------------------------------------------------------------------- /app/services/carts.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from app.models.models import Cart, CartItem, Product 3 | from app.schemas.carts import CartUpdate, CartCreate 4 | from app.utils.responses import ResponseHandler 5 | from sqlalchemy.orm import joinedload 6 | from app.core.security import get_current_user 7 | 8 | 9 | class CartService: 10 | # Get All Carts 11 | @staticmethod 12 | def get_all_carts(token, db: Session, page: int, limit: int): 13 | user_id = get_current_user(token) 14 | carts = db.query(Cart).filter(Cart.user_id == user_id).offset((page - 1) * limit).limit(limit).all() 15 | message = f"Page {page} with {limit} carts" 16 | return ResponseHandler.success(message, carts) 17 | 18 | # Get A Cart By ID 19 | @staticmethod 20 | def get_cart(token, db: Session, cart_id: int): 21 | user_id = get_current_user(token) 22 | cart = db.query(Cart).filter(Cart.id == cart_id, Cart.user_id == user_id).first() 23 | if not cart: 24 | ResponseHandler.not_found_error("Cart", cart_id) 25 | return ResponseHandler.get_single_success("cart", cart_id, cart) 26 | 27 | # Create a new Cart 28 | @staticmethod 29 | def create_cart(token, db: Session, cart: CartCreate): 30 | user_id = get_current_user(token) 31 | cart_dict = cart.model_dump() 32 | 33 | cart_items_data = cart_dict.pop("cart_items", []) 34 | cart_items = [] 35 | total_amount = 0 36 | for item_data in cart_items_data: 37 | product_id = item_data['product_id'] 38 | quantity = item_data['quantity'] 39 | 40 | product = db.query(Product).filter(Product.id == product_id).first() 41 | if not product: 42 | return ResponseHandler.not_found_error("Product", product_id) 43 | 44 | subtotal = quantity * product.price * (product.discount_percentage / 100) 45 | cart_item = CartItem(product_id=product_id, quantity=quantity, subtotal=subtotal) 46 | total_amount += subtotal 47 | 48 | cart_items.append(cart_item) 49 | cart_db = Cart(cart_items=cart_items, user_id=user_id, total_amount=total_amount, **cart_dict) 50 | db.add(cart_db) 51 | db.commit() 52 | db.refresh(cart_db) 53 | return ResponseHandler.create_success("Cart", cart_db.id, cart_db) 54 | 55 | # Update Cart & CartItem 56 | @staticmethod 57 | def update_cart(token, db: Session, cart_id: int, updated_cart: CartUpdate): 58 | user_id = get_current_user(token) 59 | 60 | cart = db.query(Cart).filter(Cart.id == cart_id, Cart.user_id == user_id).first() 61 | if not cart: 62 | return ResponseHandler.not_found_error("Cart", cart_id) 63 | 64 | # Delete existing cart_items 65 | db.query(CartItem).filter(CartItem.cart_id == cart_id).delete() 66 | 67 | for item in updated_cart.cart_items: 68 | product_id = item.product_id 69 | quantity = item.quantity 70 | 71 | product = db.query(Product).filter(Product.id == product_id).first() 72 | if not product: 73 | return ResponseHandler.not_found_error("Product", product_id) 74 | 75 | subtotal = quantity * product.price * (product.discount_percentage / 100) 76 | 77 | cart_item = CartItem( 78 | cart_id=cart_id, 79 | product_id=product_id, 80 | quantity=quantity, 81 | subtotal=subtotal 82 | ) 83 | db.add(cart_item) 84 | 85 | cart.total_amount = sum(item.subtotal for item in cart.cart_items) 86 | 87 | db.commit() 88 | db.refresh(cart) 89 | return ResponseHandler.update_success("cart", cart.id, cart) 90 | 91 | # Delete Both Cart and CartItems 92 | @staticmethod 93 | def delete_cart(token, db: Session, cart_id: int): 94 | user_id = get_current_user(token) 95 | cart = ( 96 | db.query(Cart) 97 | .options(joinedload(Cart.cart_items).joinedload(CartItem.product)) 98 | .filter(Cart.id == cart_id, Cart.user_id == user_id) 99 | .first() 100 | ) 101 | if not cart: 102 | ResponseHandler.not_found_error("Cart", cart_id) 103 | 104 | for cart_item in cart.cart_items: 105 | db.delete(cart_item) 106 | 107 | db.delete(cart) 108 | db.commit() 109 | return ResponseHandler.delete_success("Cart", cart_id, cart) 110 | -------------------------------------------------------------------------------- /app/services/categories.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from app.models.models import Category 3 | from app.schemas.categories import CategoryCreate, CategoryUpdate 4 | from app.utils.responses import ResponseHandler 5 | 6 | 7 | class CategoryService: 8 | @staticmethod 9 | def get_all_categories(db: Session, page: int, limit: int, search: str = ""): 10 | categories = db.query(Category).order_by(Category.id.asc()).filter( 11 | Category.name.contains(search)).limit(limit).offset((page - 1) * limit).all() 12 | return {"message": f"Page {page} with {limit} categories", "data": categories} 13 | 14 | @staticmethod 15 | def get_category(db: Session, category_id: int): 16 | category = db.query(Category).filter(Category.id == category_id).first() 17 | if not category: 18 | ResponseHandler.not_found_error("Category", category_id) 19 | return ResponseHandler.get_single_success(category.name, category_id, category) 20 | 21 | @staticmethod 22 | def create_category(db: Session, category: CategoryCreate): 23 | category_dict = category.dict() 24 | db_category = Category(**category_dict) 25 | db.add(db_category) 26 | db.commit() 27 | db.refresh(db_category) 28 | return ResponseHandler.create_success(db_category.name, db_category.id, db_category) 29 | 30 | @staticmethod 31 | def update_category(db: Session, category_id: int, updated_category: CategoryUpdate): 32 | db_category = db.query(Category).filter(Category.id == category_id).first() 33 | if not db_category: 34 | ResponseHandler.not_found_error("Category", category_id) 35 | 36 | for key, value in updated_category.model_dump().items(): 37 | setattr(db_category, key, value) 38 | 39 | db.commit() 40 | db.refresh(db_category) 41 | return ResponseHandler.update_success(db_category.name, db_category.id, db_category) 42 | 43 | @staticmethod 44 | def delete_category(db: Session, category_id: int): 45 | db_category = db.query(Category).filter(Category.id == category_id).first() 46 | if not db_category: 47 | ResponseHandler.not_found_error("Category", category_id) 48 | db.delete(db_category) 49 | db.commit() 50 | return ResponseHandler.delete_success(db_category.name, db_category.id, db_category) 51 | -------------------------------------------------------------------------------- /app/services/products.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from app.models.models import Product, Category 3 | from app.schemas.products import ProductCreate, ProductUpdate 4 | from app.utils.responses import ResponseHandler 5 | 6 | 7 | class ProductService: 8 | @staticmethod 9 | def get_all_products(db: Session, page: int, limit: int, search: str = ""): 10 | products = db.query(Product).order_by(Product.id.asc()).filter( 11 | Product.title.contains(search)).limit(limit).offset((page - 1) * limit).all() 12 | return {"message": f"Page {page} with {limit} products", "data": products} 13 | 14 | @staticmethod 15 | def get_product(db: Session, product_id: int): 16 | product = db.query(Product).filter(Product.id == product_id).first() 17 | if not product: 18 | ResponseHandler.not_found_error("Product", product_id) 19 | return ResponseHandler.get_single_success(product.title, product_id, product) 20 | 21 | @staticmethod 22 | def create_product(db: Session, product: ProductCreate): 23 | category_exists = db.query(Category).filter(Category.id == product.category_id).first() 24 | if not category_exists: 25 | ResponseHandler.not_found_error("Category", product.category_id) 26 | 27 | product_dict = product.model_dump() 28 | db_product = Product(**product_dict) 29 | db.add(db_product) 30 | db.commit() 31 | db.refresh(db_product) 32 | return ResponseHandler.create_success(db_product.title, db_product.id, db_product) 33 | 34 | @staticmethod 35 | def update_product(db: Session, product_id: int, updated_product: ProductUpdate): 36 | db_product = db.query(Product).filter(Product.id == product_id).first() 37 | if not db_product: 38 | ResponseHandler.not_found_error("Product", product_id) 39 | 40 | for key, value in updated_product.model_dump().items(): 41 | setattr(db_product, key, value) 42 | 43 | db.commit() 44 | db.refresh(db_product) 45 | return ResponseHandler.update_success(db_product.title, db_product.id, db_product) 46 | 47 | @staticmethod 48 | def delete_product(db: Session, product_id: int): 49 | db_product = db.query(Product).filter(Product.id == product_id).first() 50 | if not db_product: 51 | ResponseHandler.not_found_error("Product", product_id) 52 | db.delete(db_product) 53 | db.commit() 54 | return ResponseHandler.delete_success(db_product.title, db_product.id, db_product) 55 | -------------------------------------------------------------------------------- /app/services/users.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from app.models.models import User 3 | from app.schemas.users import UserCreate, UserUpdate 4 | from app.utils.responses import ResponseHandler 5 | from app.core.security import get_password_hash 6 | 7 | 8 | class UserService: 9 | @staticmethod 10 | def get_all_users(db: Session, page: int, limit: int, search: str = "", role: str = "user"): 11 | users = db.query(User).order_by(User.id.asc()).filter( 12 | User.username.contains(search), User.role == role).limit(limit).offset((page - 1) * limit).all() 13 | return {"message": f"Page {page} with {limit} users", "data": users} 14 | 15 | @staticmethod 16 | def get_user(db: Session, user_id: int): 17 | user = db.query(User).filter(User.id == user_id).first() 18 | if not user: 19 | ResponseHandler.not_found_error("User", user_id) 20 | return ResponseHandler.get_single_success(user.username, user_id, user) 21 | 22 | @staticmethod 23 | def create_user(db: Session, user: UserCreate): 24 | hashed_password = get_password_hash(user.password) 25 | user.password = hashed_password 26 | db_user = User(id=None, **user.model_dump()) 27 | db.add(db_user) 28 | db.commit() 29 | db.refresh(db_user) 30 | return ResponseHandler.create_success(db_user.username, db_user.id, db_user) 31 | 32 | @staticmethod 33 | def update_user(db: Session, user_id: int, updated_user: UserUpdate): 34 | db_user = db.query(User).filter(User.id == user_id).first() 35 | if not db_user: 36 | ResponseHandler.not_found_error("User", user_id) 37 | 38 | for key, value in updated_user.model_dump().items(): 39 | setattr(db_user, key, value) 40 | 41 | db.commit() 42 | db.refresh(db_user) 43 | return ResponseHandler.update_success(db_user.username, db_user.id, db_user) 44 | 45 | @staticmethod 46 | def delete_user(db: Session, user_id: int): 47 | db_user = db.query(User).filter(User.id == user_id).first() 48 | if not db_user: 49 | ResponseHandler.not_found_error("User", user_id) 50 | db.delete(db_user) 51 | db.commit() 52 | return ResponseHandler.delete_success(db_user.username, db_user.id, db_user) 53 | -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliseyedi01/FastAPI-Ecommerce-API/5aff219ceba7efbb97d3d7866256fccda640af1c/app/utils/__init__.py -------------------------------------------------------------------------------- /app/utils/responses.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException, status 2 | 3 | 4 | class ResponseHandler: 5 | @staticmethod 6 | def success(message, data=None): 7 | return {"message": message, "data": data} 8 | 9 | @staticmethod 10 | def get_single_success(name, id, data): 11 | message = f"Details for {name} with id {id}" 12 | return ResponseHandler.success(message, data) 13 | 14 | @staticmethod 15 | def create_success(name, id, data): 16 | message = f"{name} with id {id} created successfully" 17 | return ResponseHandler.success(message, data) 18 | 19 | @staticmethod 20 | def update_success(name, id, data): 21 | message = f"{name} with id {id} updated successfully" 22 | return ResponseHandler.success(message, data) 23 | 24 | @staticmethod 25 | def delete_success(name, id, data): 26 | message = f"{name} with id {id} deleted successfully" 27 | return ResponseHandler.success(message, data) 28 | 29 | @staticmethod 30 | def not_found_error(name="", id=None): 31 | message = f"{name} With Id {id} Not Found!" 32 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=message) 33 | 34 | @staticmethod 35 | def invalid_token(name=""): 36 | raise HTTPException( 37 | status_code=status.HTTP_401_UNAUTHORIZED, 38 | detail=f"Invalid {name} token.", 39 | headers={"WWW-Authenticate": "Bearer"}) 40 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from app.main import app 2 | 3 | if __name__ == "__main__": 4 | app.run() 5 | -------------------------------------------------------------------------------- /migrate.py: -------------------------------------------------------------------------------- 1 | # migrate.py 2 | from alembic.config import Config 3 | from alembic import command 4 | 5 | alembic_cfg = Config("alembic.ini") 6 | command.upgrade(alembic_cfg, "head") 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.12.1 2 | annotated-types==0.6.0 3 | anyio==3.7.1 4 | bcrypt==4.1.1 5 | click==8.1.7 6 | colorama==0.4.6 7 | dnspython==2.4.2 8 | ecdsa==0.18.0 9 | email-validator==2.1.0.post1 10 | exceptiongroup==1.2.0 11 | fastapi==0.104.1 12 | greenlet==3.0.1 13 | h11==0.14.0 14 | httptools==0.6.1 15 | idna==3.4 16 | Mako==1.3.0 17 | MarkupSafe==2.1.3 18 | passlib==1.7.4 19 | psycopg2-binary==2.9.9 20 | pyasn1==0.5.1 21 | pydantic==2.5.1 22 | pydantic-settings==2.1.0 23 | pydantic_core==2.14.3 24 | python-dotenv==1.0.0 25 | python-jose==3.3.0 26 | python-multipart==0.0.6 27 | PyYAML==6.0.1 28 | rsa==4.9 29 | six==1.16.0 30 | sniffio==1.3.0 31 | SQLAlchemy==2.0.23 32 | starlette==0.27.0 33 | typing_extensions==4.8.0 34 | uvicorn==0.24.0.post1 35 | watchfiles==0.21.0 36 | websockets==12.0 37 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True) 5 | --------------------------------------------------------------------------------