diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ccd43f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Ignore Python cache files +__pycache__/ +*.pyc +*.pyo + +# Ignore init files if you don’t want them tracked +__init__.py diff --git a/__pycache__/database.cpython-313.pyc b/__pycache__/database.cpython-313.pyc index 714e5d9..6727ab5 100644 Binary files a/__pycache__/database.cpython-313.pyc and b/__pycache__/database.cpython-313.pyc differ diff --git a/__pycache__/database_model.cpython-313.pyc b/__pycache__/database_model.cpython-313.pyc index 26c67e0..2367e17 100644 Binary files a/__pycache__/database_model.cpython-313.pyc and b/__pycache__/database_model.cpython-313.pyc differ diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc index 1dfe69f..95b7451 100644 Binary files a/__pycache__/main.cpython-313.pyc and b/__pycache__/main.cpython-313.pyc differ diff --git a/__pycache__/model.cpython-313.pyc b/__pycache__/model.cpython-313.pyc index 2dfe74a..3790d70 100644 Binary files a/__pycache__/model.cpython-313.pyc and b/__pycache__/model.cpython-313.pyc differ diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..0449346 --- /dev/null +++ b/app/auth.py @@ -0,0 +1,59 @@ +from datetime import datetime, timedelta +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer +from jose import JWTError, jwt +from passlib.context import CryptContext +from sqlalchemy.orm import Session +from app.models.user import User +from app.database import get_db +from fastapi.security import HTTPAuthorizationCredentials + + +SECRET_KEY = "af3287c8391bb9f4f7a72feb3b85f72e1d5bd07cbf4fa4ad9497c78412923312" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +bearer_scheme = HTTPBearer() + + +# ---------------- PASSWORD UTILS ---------------- # +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +# ---------------- TOKEN CREATION ---------------- # +def create_access_token(user_id: int): + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode = {"sub": str(user_id), "exp": expire} + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + +# ---------------- VERIFY CURRENT USER ---------------- # +def get_current_user( + token: HTTPAuthorizationCredentials = Depends(bearer_scheme), + db: Session = Depends(get_db) +): + credential_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + payload = jwt.decode(token.credentials, SECRET_KEY, + algorithms=[ALGORITHM]) + user_id: str = payload.get("sub") + if user_id is None: + raise credential_exception + except JWTError: + raise credential_exception + + user = db.query(User).filter(User.id == int(user_id)).first() + if user is None: + raise credential_exception + return user diff --git a/app/controllers/auth_controller.py b/app/controllers/auth_controller.py new file mode 100644 index 0000000..d214a06 --- /dev/null +++ b/app/controllers/auth_controller.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.schemas.user import UserCreate, UserResponse +from app.services.user_service import UserService +from app.database import get_db +from app.auth import create_access_token, verify_password +from app.models.user import User +router = APIRouter() + + +@router.post("/register", response_model=UserResponse) +def register(user: UserCreate, db: Session = Depends(get_db)): + service = UserService(db) + try: + return service.create_user(user) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + +@router.post("/token") +def login(user: UserCreate, db: Session = Depends(get_db)): + # ✅ Query using database model + db_user = db.query(User).filter(User.username == user.username).first() + + if not db_user or not verify_password(user.password, db_user.password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") + + token = create_access_token(db_user.id) + return {"access_token": token, "token_type": "bearer"} diff --git a/app/controllers/company_controller.py b/app/controllers/company_controller.py new file mode 100644 index 0000000..76d647f --- /dev/null +++ b/app/controllers/company_controller.py @@ -0,0 +1,46 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from app.schemas.company import CompanyCreate, CompanyResponse +from app.services.company_service import CompanyService +from app.database import get_db +from app.auth import get_current_user + +router = APIRouter() + + +@router.post("/", response_model=CompanyResponse) +def create_company( + company: CompanyCreate, + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = CompanyService(db) + return service.create_company(current_user.id, company) + + +@router.get("/me", response_model=CompanyResponse) +def get_my_company( + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = CompanyService(db) + return service.get_my_company(current_user.id) + + +@router.put("/me", response_model=CompanyResponse) +def edit_my_company( + company: CompanyCreate, + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = CompanyService(db) + return service.edit_company(current_user.id, company) + + +@router.delete("/me", dependencies=[Depends(get_current_user)]) +def delete_my_company( + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = CompanyService(db) + return service.delete_company(current_user.id) diff --git a/app/controllers/product_controller.py b/app/controllers/product_controller.py new file mode 100644 index 0000000..3d06d85 --- /dev/null +++ b/app/controllers/product_controller.py @@ -0,0 +1,63 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.schemas.product import ProductCreate, ProductResponse +from app.services.product_service import ProductService +from app.services.company_service import CompanyService +from app.database import get_db +from app.auth import get_current_user + + +router = APIRouter() + + +@router.post("/", response_model=ProductResponse) +def create_product( + product: ProductCreate, + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + company_service = CompanyService(db) + product_service = ProductService(db) + company = company_service.get_my_company(current_user.id) + if not company: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="no company found") + + return product_service.create_product(company.id, product) + + +@router.get("/", response_model=list[ProductResponse]) +def list_product( + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + product_service = ProductService(db) + return product_service.list_products() + + +@router.get("/{product_id}", response_model=ProductResponse) +def get_product_by_id( + product_id: int, + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + product_service = ProductService(db) + return product_service.get_product(product_id) + + +@router.put("/{product_id}", response_model=ProductResponse) +def update_product_by_id( + product_id: int, + product: ProductCreate, + db: Session = Depends(get_db), + current_user=Depends(get_current_user)): + product_service = ProductService(db) + return product_service.update_product(product_id,product) + +@router.delete("/{product_id}", response_model=ProductResponse) +def delete_product_by_id( + product_id: int, + db: Session = Depends(get_db), + current_user=Depends(get_current_user)): + product_service = ProductService(db) + return product_service.delete_product(product_id) \ No newline at end of file diff --git a/app/controllers/user_controller.py b/app/controllers/user_controller.py new file mode 100644 index 0000000..e69de29 diff --git a/database.py b/app/database.py similarity index 57% rename from database.py rename to app/database.py index dad789c..fbb7968 100644 --- a/database.py +++ b/app/database.py @@ -1,7 +1,20 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base + + db_url = "postgresql://postgres:123@localhost:5432/inventory" engine = create_engine(db_url) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..f59d8a9 --- /dev/null +++ b/app/main.py @@ -0,0 +1,52 @@ +from fastapi import FastAPI +from app.database import Base, engine +from app.controllers import auth_controller, company_controller, product_controller + + +Base.metadata.create_all(bind=engine) +version = "v1" +app = FastAPI(title="Company & Product API", + description="in which user create their company", + version=version + ) + +app.include_router(auth_controller.router, + prefix=f"/api/{version}/auth", tags=["Auth"]) +app.include_router(company_controller.router, + prefix=f"/api/{version}/company", tags=["Company"]) +app.include_router(product_controller.router, + prefix=f"/api/{version}/product", tags=["Product"]) + + +@app.get("/") +def root(): + return {"message": "Welcome to Company API!"} + + +# from fastapi import FastAPI +# from fastapi.middleware.cors import CORSMiddleware +# import db.database_model as database_model +# from app.database import engine +# from routes import file_routes, post_routes, products_routes, user_route + +# version = "v1" +# app = FastAPI(title="Fastapi ", +# description="this is learning project.", +# version=version,) + + +# database_model.Base.metadata.create_all(bind=engine) + + +# app.include_router(products_routes.router, +# prefix=f"/api/{version}/products", tags=['Products']) +# app.include_router(file_routes.router, +# prefix=f"/api/{version}/files", tags=['Files']) +# app.include_router(user_route.router, +# prefix=f"/api/{version}/users", tags=['Users']) +# app.include_router(post_routes.router, +# prefix=f"/api/{version}/posts", tags=["Posts"]) + +# @app.get("/") +# def greet(): +# return {"message": "Hello, World!"} diff --git a/app/models/company.py b/app/models/company.py new file mode 100644 index 0000000..555bc5e --- /dev/null +++ b/app/models/company.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship +from app.database import Base + + +class Company(Base): + __tablename__ = "companies" + + id =Column(Integer, primary_key=True, index=True) + name= Column(String) + location = Column(String) + user_id = Column(Integer, ForeignKey("users.id")) + + user= relationship("User", back_populates="company") + products= relationship("Product", back_populates="company") \ No newline at end of file diff --git a/app/models/model.py b/app/models/model.py new file mode 100644 index 0000000..1643234 --- /dev/null +++ b/app/models/model.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel +from typing import Optional + +class Product(BaseModel): + id: int + name: str + description: str + price: float + quantity: int + + class Config: + from_attributes = True + + +class CreateUser(BaseModel): + username: str + password: str + + +class User(BaseModel): + id: int + username: str + password: str + + +class Post(BaseModel): + post_id: Optional [int] + title: str + description: str + + +class Token(BaseModel): + access_token: str + token_type: str diff --git a/app/models/product.py b/app/models/product.py new file mode 100644 index 0000000..29bce74 --- /dev/null +++ b/app/models/product.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, Integer, String, Float, ForeignKey +from sqlalchemy.orm import relationship +from app.database import Base + + +class Product(Base): + __tablename__ = "products" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String) + price = Column(Float) + description = Column(String,nullable=True) + company_id = Column(Integer, ForeignKey("companies.id")) + + company = relationship("Company", back_populates="products") diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..4b4cdfb --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,13 @@ +from sqlalchemy import Column, Integer, String +from sqlalchemy.orm import relationship +from app.database import Base + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True) + password = Column(String) + + company= relationship("Company",back_populates="user", uselist=False) diff --git a/app/schemas/company.py b/app/schemas/company.py new file mode 100644 index 0000000..7fa504e --- /dev/null +++ b/app/schemas/company.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel +from typing import List, Optional +from app.schemas.product import ProductResponse + + +class CompanyCreate(BaseModel): + name: str + location: str + + +class CompanyResponse(BaseModel): + id: int + name: str + location: str + products: List[ProductResponse] = [] + + class Config: + from_attributes = True diff --git a/app/schemas/model.py b/app/schemas/model.py new file mode 100644 index 0000000..8ce1d9c --- /dev/null +++ b/app/schemas/model.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel +from typing import Optional + +class Product(BaseModel): + id: int + name: str + description: str + price: float + quantity: int + + class Config: + from_attributes = True + + + + + + + +class Post(BaseModel): + post_id: Optional [int] + title: str + description: str + + +class Token(BaseModel): + access_token: str + token_type: str diff --git a/app/schemas/product.py b/app/schemas/product.py new file mode 100644 index 0000000..77ed9f7 --- /dev/null +++ b/app/schemas/product.py @@ -0,0 +1,18 @@ +from typing import Optional +from pydantic import BaseModel + + +class ProductCreate(BaseModel): + name: str + price: float + description: Optional[str] = None + + +class ProductResponse(BaseModel): + id: int + name: str + price: float + description: Optional[str] = None + + class Config: + from_attributes = True diff --git a/app/schemas/user.py b/app/schemas/user.py new file mode 100644 index 0000000..0e5a05c --- /dev/null +++ b/app/schemas/user.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel + + +class UserCreate(BaseModel): + username: str + password: str + + +class UserResponse(BaseModel): + id: int + username: str + password: str + + class Config: + from_attributes = True diff --git a/app/services/company_service.py b/app/services/company_service.py new file mode 100644 index 0000000..e0b6155 --- /dev/null +++ b/app/services/company_service.py @@ -0,0 +1,60 @@ +from fastapi import HTTPException, status +from sqlalchemy.orm import Session +from app.models.company import Company +from app.schemas.company import CompanyCreate + + +class CompanyService: + def __init__(self, db: Session): + self.db = db + + def create_company(self, user_id: int, company_data: CompanyCreate): + existing = self.db.query(Company).filter( + Company.user_id == user_id).first() + if existing: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail="user already has a company") + + new_company = Company( + name=company_data.name, + location=company_data.location, + user_id=user_id + ) + self.db.add(new_company) + self.db.commit() + self.db.refresh(new_company) + return new_company + + def get_my_company(self, user_id: int): + company = self.db.query(Company).filter( + Company.user_id == user_id).first() + if not company: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Company not found") + return company + + def edit_company(self, user_id: int, company_data: CompanyCreate): + company = self.db.query(Company).filter( + Company.user_id == user_id + ).first() + if not company: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="company not found" + ) + company.name = company_data.name + company.location = company_data.location + self.db.commit() + self.db.refresh(company) + return company + + def delete_company(self, user_id: int): + company = self.db.query(Company).filter( + Company.user_id == user_id + ).first() + if not company: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="company not found" + ) + self.db.delete(company) + self.db.commit() + return {"detail": "company deleted"} diff --git a/app/services/product_service.py b/app/services/product_service.py new file mode 100644 index 0000000..c9db8c9 --- /dev/null +++ b/app/services/product_service.py @@ -0,0 +1,47 @@ +from fastapi import HTTPException +from sqlalchemy.orm import Session +from app.models.product import Product +from app.schemas.product import ProductCreate + + +class ProductService: + def __init__(self, db: Session): + self.db = db + + def create_product(self, company_id: int, product_data: ProductCreate): + new_product = Product( + name=product_data.name, + price=product_data.price, + description=product_data.description, + company_id=company_id + ) + self.db.add(new_product) + self.db.commit() + self.db.refresh(new_product) + return new_product + + def list_products(self): + products = self.db.query(Product).all() + return products + + def get_product(self, product_id: int): + product = self.db.query(Product).filter( + Product.id == product_id).first() + if not product: + raise HTTPException(status_code=404, detail="Product not found") + return product + + def update_product(self, product_id: int, product_data: ProductCreate): + product = self.get_product(product_id) + product.name = product_data.name + product.price = product_data.price + product.description = product_data.description + self.db.commit() + self.db.refresh(product) + return product + + def delete_product(self, product_id: int): + product = self.get_product(product_id) + self.db.delete(product) + self.db.commit() + return {"detail": "Product deleted"} diff --git a/app/services/user_service.py b/app/services/user_service.py new file mode 100644 index 0000000..9f6e614 --- /dev/null +++ b/app/services/user_service.py @@ -0,0 +1,33 @@ +from sqlalchemy.orm import Session +from app.schemas.user import UserCreate +from app.models.user import User +from app.auth import get_password_hash, verify_password, create_access_token +from fastapi import HTTPException, status + + +class UserService: + def __init__(self, db: Session): + self.db = db + + def create_user(self, user: UserCreate): + db_user = self.db.query(User).filter( + User.username == user.username).first() + if db_user: + raise ValueError("Username already registered") + hashed_pw = get_password_hash(user.password) + new_user = User(username=user.username, password=hashed_pw) + self.db.add(new_user) + self.db.commit() + self.db.refresh(new_user) + return new_user + +# def login_user(self, user: UserCreate): +# db_user = self.db.query(User).filter(User.username == user.username).first() +# if not db_user or not verify_password(user.password, db_user.password): +# raise HTTPException( +# status_code=status.HTTP_401_UNAUTHORIZED, +# detail="Invalid credentials" +# ) + +# token = create_access_token(db_user.id) +# return {"access_token": token, "token_type": "bearer"} diff --git a/database_model.py b/database_model.py deleted file mode 100644 index ef72229..0000000 --- a/database_model.py +++ /dev/null @@ -1,14 +0,0 @@ -from sqlalchemy import Column, Integer, String, Float -from sqlalchemy.ext.declarative import declarative_base - -Base=declarative_base() - -class Product(Base): - - __tablename__ = "products" - - id = Column(Integer, primary_key=True, index=True) - name = Column(String) - description= Column(String) - price=Column(Float) - quantity=Column(Integer) \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index 412cd1c..0000000 --- a/main.py +++ /dev/null @@ -1,106 +0,0 @@ -from fastapi import Depends, FastAPI -from fastapi.middleware.cors import CORSMiddleware -from model import Product -from database import SessionLocal, engine -import database_model -from sqlalchemy.orm import Session - -app = FastAPI() - -app.add_middleware( - CORSMiddleware, - allow_origins=["http://localhost:3000"], - allow_methods=["*"], -) - -database_model.Base.metadata.create_all(bind=engine) - - -products = [ - Product(id=1, name="Laptop", description="A high-performance laptop", - price=999.99, quantity=10), - Product(id=2, name="Smartphone", - description="A latest model smartphone", price=699.99, quantity=25), - Product(id=3, name="Headphones", - description="Noise-cancelling headphones", price=199.99, quantity=15), - Product(id=4, name="Monitor", description="4K UHD Monitor", - price=299.99, quantity=8), - Product(id=5, name="Keyboard", description="Mechanical keyboard", - price=89.99, quantity=30), -] - - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - - -def init_db(): - db = SessionLocal() - count = db.query(database_model.Product).count() - - if count == 0: - for product in products: - db.add(database_model.Product(**product.model_dump())) - - db.commit() - - -init_db() - - -@app.get("/") -def greet(): - return "Hello, World!" - - -@app.get("/products") -def get_all_products(db: Session = Depends(get_db)): - db_products = db.query(database_model.Product).all() - return db_products - - -@app.get("/products/{id}") -def get_product_by_id(id: int, db: Session = Depends(get_db)): - db_product = db.query(database_model.Product).filter( - database_model.Product.id == id).first() - if db_product: - return db_product - return "product not found" - - -@app.post("/products") -def add_product(product: Product, db: Session = Depends(get_db)): - db.add(database_model.Product(**product.model_dump())) - db.commit() - return product - - -@app.put("/products/{id}") -def update_product(id: int, product: Product, db: Session = Depends(get_db)): - db_product = db.query(database_model.Product).filter( - database_model.Product.id == id).first() - if db_product: - db_product.name = product.name - db_product.description = product.description - db_product.price = product.price - db_product.quantity = product.quantity - db.commit() - return "product updated successfully" - else: - return "product not found" - - -@app.delete("/products/{id}") -def delete_product(id: int, db: Session = Depends(get_db)): - db_product = db.query(database_model.Product).filter( - database_model.Product.id == id).first() - if db_product: - db.delete(db_product) - db.commit() - return "product deleted successfully" - else: - return "product not found" diff --git a/model.py b/model.py deleted file mode 100644 index 23192ce..0000000 --- a/model.py +++ /dev/null @@ -1,8 +0,0 @@ -from pydantic import BaseModel - -class Product(BaseModel): - id:int - name:str - description:str - price:float - quantity:int \ No newline at end of file diff --git a/requirement.txt b/requirement.txt index 96c59e6..f081a46 100644 --- a/requirement.txt +++ b/requirement.txt @@ -1,9 +1,13 @@ annotated-types==0.7.0 anyio==4.11.0 +bcrypt==4.0.1 certifi==2025.10.5 +cffi==2.0.0 click==8.3.0 colorama==0.4.6 +cryptography==46.0.2 dnspython==2.8.0 +ecdsa==0.19.1 email-validator==2.3.0 fastapi==0.118.0 fastapi-cli==0.0.13 @@ -18,19 +22,25 @@ Jinja2==3.1.6 markdown-it-py==4.0.0 MarkupSafe==3.0.3 mdurl==0.1.2 +passlib==1.7.4 psycopg2==2.9.10 psycopg2-binary==2.9.10 +pyasn1==0.6.1 +pycparser==2.23 pydantic==2.11.10 pydantic_core==2.33.2 Pygments==2.19.2 python-dotenv==1.1.1 +python-jose==3.5.0 python-multipart==0.0.20 PyYAML==6.0.3 rich==14.1.0 rich-toolkit==0.15.1 rignore==0.7.0 +rsa==4.9.1 sentry-sdk==2.40.0 shellingham==1.5.4 +six==1.17.0 sniffio==1.3.1 SQLAlchemy==2.0.43 sqlmodel==0.0.25