diff --git a/.env.example b/.env.example index 5b749851cc044dc5c9d53faa89dd4814a59d0696..9449d42baa6ea495573fcb82d169322bf506c0ac 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,5 @@ DATABASE_PASSWORD="your_mysql_password" DATABASE_HOST="127.0.0.1" DATABASE_NAME="shopping" SECRET_KEY="your_secret_key" +ALGORITHM=HS256 DEBUG=True diff --git a/app/backend/main.py b/app/backend/main.py index d03cae7ea6b458687e22d798b4f8d3da73827ec3..34d7e884625695a28c5e7cce0f3042eb146b08f1 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -4,7 +4,7 @@ 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.routes import auth, shop from backend.database import init_db from core.config import settings @@ -15,6 +15,7 @@ init_db() # Include API routes app.include_router(auth.router, prefix="/auth", tags=["auth"]) +app.include_router(shop.router, prefix="/shops", tags=["shops"]) @app.get("/") diff --git a/app/backend/routes/auth.py b/app/backend/routes/auth.py index c073e7c56f7d784c6eb9cf8af603a1971500fb97..3b68f08a8e96dba22fb2c1add89bcc4df9fb7a3b 100644 --- a/app/backend/routes/auth.py +++ b/app/backend/routes/auth.py @@ -1,12 +1,28 @@ from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from backend.models.models 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 +from app.core.security import decode_token, create_access_token router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") + + +def get_current_user( + token: str = Depends(oauth2_scheme), session: Session = Depends(get_session) +) -> User: + user_id = decode_token(token) + user = session.get(User, user_id) + if not user: + raise HTTPException( + status_code=401, detail="Invalid authentication credentials" + ) + return user + @router.post("/signup") def signup(user_data: UserCreate, session: Session = Depends(get_session)): @@ -29,9 +45,28 @@ def signup(user_data: UserCreate, session: Session = Depends(get_session)): 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") +# access_token = create_access_token(data={"sub": str(user.id)}) +# return { +# "message": "Login successful", +# "user_id": user.id, +# "access_token": access_token, +# } + + @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): +def login( + form_data: OAuth2PasswordRequestForm = Depends(), + session: Session = Depends(get_session), +): + user = session.exec(select(User).where(User.email == form_data.username)).first() + if not user or not verify_password(form_data.password, user.password): raise HTTPException(status_code=401, detail="Invalid credentials") - return {"message": "Login successful", "user_id": user.id} + + access_token = create_access_token(data={"sub": str(user.id)}) + + return {"access_token": access_token, "token_type": "bearer"} diff --git a/app/backend/routes/shop.py b/app/backend/routes/shop.py new file mode 100644 index 0000000000000000000000000000000000000000..7775a1c71739c280181c1d99c31728bc136e4948 --- /dev/null +++ b/app/backend/routes/shop.py @@ -0,0 +1,102 @@ +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form +from sqlmodel import Session +from backend.models.models import Shop, User +from backend.schemas.shop import ShopCreate, ShopRead +from backend.database import get_session +from backend.routes.auth import get_current_user +import shutil +import os + +router = APIRouter() + +static_dir = os.path.join("app", "static") +os.makedirs(static_dir, exist_ok=True) + + +@router.post("/", response_model=ShopRead) +def create_shop( + name: str = Form(...), + description: str = Form(None), + file: UploadFile = File(None), + session: Session = Depends(get_session), + current_user: User = Depends(get_current_user), +): + shop = ShopCreate(name=name, description=description, owner_id=current_user.id) + db_shop = Shop.from_orm(shop) + + if file and file.filename: + # Save the image to the static directory + file_location = os.path.join(static_dir, f"{db_shop.name}_{file.filename}") + with open(file_location, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + db_shop.image_url = file_location + else: + # Set a default image URL if no file is uploaded + db_shop.image_url = os.path.join(static_dir, "default_shop_image.png") + + session.add(db_shop) + session.commit() + session.refresh(db_shop) + + if file: + # Delete the image file after session commit + os.remove(file_location) + return db_shop + + +@router.get("/{shop_id}", response_model=ShopRead) +def read_shop(shop_id: int, session: Session = Depends(get_session)): + shop = session.get(Shop, shop_id) + if not shop: + raise HTTPException(status_code=404, detail="Shop not found") + return shop + + +@router.put("/{shop_id}", response_model=ShopRead) +def update_shop( + shop_id: int, + name: str = Form(None), + description: str = Form(None), + file: UploadFile = File(None), + session: Session = Depends(get_session), +): + db_shop = session.get(Shop, shop_id) + if not db_shop: + raise HTTPException(status_code=404, detail="Shop not found") + + if name: + db_shop.name = name + if description: + db_shop.description = description + + if file and file.filename: + # Save the image to the static directory + file_location = os.path.join(static_dir, f"{db_shop.name}_{file.filename}") + with open(file_location, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + db_shop.image_url = file_location + else: + # Set a default image URL if no file is uploaded + db_shop.image_url = os.path.join(static_dir, "default_shop_image.png") + + session.add(db_shop) + session.commit() + session.refresh(db_shop) + + if file: + # Delete the image file after session commit + os.remove(file_location) + + return db_shop + + +@router.delete("/{shop_id}") +def delete_shop(shop_id: int, session: Session = Depends(get_session)): + shop = session.get(Shop, shop_id) + if not shop: + raise HTTPException(status_code=404, detail="Shop not found") + session.delete(shop) + session.commit() + return {"message": "Shop deleted successfully"} diff --git a/app/backend/schemas/shop.py b/app/backend/schemas/shop.py new file mode 100644 index 0000000000000000000000000000000000000000..71763bb8b375e6051849d32a9ad5efad889a106b --- /dev/null +++ b/app/backend/schemas/shop.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel +from typing import Optional + + +class ShopBase(BaseModel): + name: str + description: Optional[str] = None + + +class ShopCreate(ShopBase): + owner_id: int + + +class ShopRead(ShopBase): + id: int + owner_id: int + image_url: Optional[str] = None + + class Config: + orm_mode = True + + +class ShopUpdate(ShopBase): + name: Optional[str] = None + description: Optional[str] = None diff --git a/app/core/config.py b/app/core/config.py index 6aed5e95bbf84aa6033c1e13f1693d42f3f566b5..9666c8a4a488a66f56b3f690756a9d3aac9d3985 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -9,6 +9,7 @@ class Settings(BaseSettings): database_host: str database_name: str secret_key: str + algorithm: str debug: bool = True @property diff --git a/app/core/security.py b/app/core/security.py new file mode 100644 index 0000000000000000000000000000000000000000..411ea600c4cb19086ad4355aa5bfa62addebde47 --- /dev/null +++ b/app/core/security.py @@ -0,0 +1,33 @@ +import jwt +from datetime import datetime, timedelta +from fastapi import HTTPException +from jwt import PyJWTError +from core.config import settings + +SECRET_KEY = settings.secret_key +ALGORITHM = settings.algorithm +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +def create_access_token(data: dict): + to_encode = data.copy() + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def decode_token(token: str) -> int: + try: + token = token.replace("Bearer ", "") # Remove "Bearer " prefix + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + user_id: int = payload.get("sub") + if user_id is None: + raise HTTPException( + status_code=401, detail="Invalid authentication credentials" + ) + return user_id + except PyJWTError: + raise HTTPException( + status_code=401, detail="Invalid authentication credentials" + ) diff --git a/app/static/default_shop_image.png b/app/static/default_shop_image.png new file mode 100644 index 0000000000000000000000000000000000000000..cbe892f8fcae34a8022b471f94c9ef8b7140d7d6 Binary files /dev/null and b/app/static/default_shop_image.png differ