diff --git a/auth/models.py b/auth/models.py index 1608fee..8a86691 100644 --- a/auth/models.py +++ b/auth/models.py @@ -1,26 +1,27 @@ from enum import Enum from backend.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES -from backend.config import pwd_context +from backend.config import pwd_context, get_session_db from datetime import datetime, timedelta, timezone from pydantic import BaseModel from fastapi import Depends, HTTPException -from typing import Annotated +from typing import Annotated, Optional from fastapi.security import OAuth2PasswordBearer import jwt +from sqlmodel import SQLModel, Field, Session, select +from pydantic.networks import EmailStr +### TOKEN MODELLERİ ### class Token(BaseModel): - access_token : str - token_type : str + access_token: str + token_type: str class TokenData(BaseModel): - username : str | None = None - role : str | None = None - status : str | None = None + username: Optional[str] = None + role: Optional[str] = None + status: Optional[str] = None -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") - -#### ENUMS #### +### ENUMS ### class Role(str, Enum): user = "user" admin = "admin" @@ -32,46 +33,38 @@ class Status(str, Enum): banned = "banned" suspended = "suspended" -class User(BaseModel): - username : str | None = None - user_id : int | None = None - role : Role | None = None - status : Status | None = None +### KULLANICI MODELLERİ ### +class UserBase(SQLModel): + username: Optional[str] = None + user_id: Optional[int] = None + role: Optional[Role] = None + status: Optional[Status] = None + +class UserInDb(UserBase): + hashed_password: str | None = None + +class UserPublic(UserBase): + pass + +class UserCreate(BaseModel): + username: Optional[str] = None + role: Optional[Role] = None + email : EmailStr | None = None + status: Optional[Status] = None + password : str | None = None + +### VERİTABANI MODELİ ### +class DBUser(SQLModel, table=True): + __tablename__ = "users" # opsiyonel, sqlmodel bunu otomatik de atar + user_id: Optional[int] = Field(default=None, primary_key=True) + username: str = Field(index=True, nullable=False) + hashed_password: str = Field(nullable=False) + role: Role = Field(default=Role.user) + status: Status = Field(default=Status.active) -class UserInDb(User): - hashed_password : str | None = None - -class UserPublic(BaseModel): - username : str | None = None - role : Role | None = None - status : Status | None = None - user_id : int | None = None - - -fake_db = { - "bedir": { - "username": "bedir", - "user_id": 1, - "hashed_password": "$2a$12$mYGWGo9c3Di3SJyYjYf3XOAsu5nP8jekf3KTItO9pbUBEm5BcapRO", # Bcrypt örneği - "role": Role.user, - "status": Status.active, - }, - "alice": { - "username": "alice", - "user_id": 2, - "hashed_password": "$2b$12$Alic3FakeHashedPasSw0rdxxxxxxxyyyyyyzzzzzz", - "role": Role.user, - "status": Status.suspended, - }, - "adminuser": { - "username": "adminuser", - "user_id": 3, - "hashed_password": "$2b$12$AdminFakeHashedPasSw0rdxxxxxxxyyyyyyzzzzzz", - "role": Role.admin, - "status": Status.active, - } -} +### AUTH ### +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") def verify_password(plain_password: str, hashed_password: str) -> bool: @@ -80,18 +73,22 @@ def verify_password(plain_password: str, hashed_password: str) -> bool: def get_password_hash(password: str) -> str: return pwd_context.hash(password) -def authenticate_user(fake_db, username: str, password: str) -> UserInDb | bool: - user = fake_db.get(username) - if not user: - return False - if not verify_password(password, user["hashed_password"]): - return False - - return user +def authenticate_user( + session: Annotated[Session, Depends(get_session_db)], + username: str, + password: str + ) -> UserInDb | None: + + statement = select(DBUser).where(DBUser.username == username) + result = session.exec(statement).first() + if not result or not verify_password(password, result.hashed_password): + return None + return result + def create_access_token( - data : dict, - expires_delta : Annotated[timedelta, None] = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES), + data: dict, + expires_delta: Optional[timedelta] = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES), ) -> str: to_encode = data.copy() expire = datetime.now(timezone.utc) + expires_delta @@ -100,79 +97,46 @@ def create_access_token( return encoded_jwt -def get_user(db, username: str) -> UserInDb | None: - if username in db: - user_dict = db[username] - return UserInDb(**user_dict) - return None +async def get_user( + session: Annotated[Session, Depends(get_session_db)], + username: str + ) -> UserInDb | None: + + statement = select(DBUser).where(DBUser.username == username) + result = session.exec(statement).first() + return result + + +async def get_current_user( + token: Annotated[str, Depends(oauth2_scheme)], + session: Annotated[Session, Depends(get_session_db)] +) -> UserPublic: -def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> UserPublic | None: credentials_exception = HTTPException( status_code=401, - detail="Burda bir hata var", + detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) token_data = TokenData(**payload) - token_data.username = payload.get("sub") - username : str = token_data.username + username: Optional[str] = payload.get("sub") if username is None: raise credentials_exception - except jwt.PyJWTError: raise credentials_exception - - user = get_user(fake_db, username=username) + + user = await get_user(session, username) if user is None: raise credentials_exception - + return user + async def get_current_active_user( - current_user : Annotated[UserInDb, Depends(get_current_user)] -) -> UserPublic | None: - + current_user: Annotated[UserInDb, Depends(get_current_user)] +) -> UserPublic: + if current_user.status == Status.banned: raise HTTPException(status_code=400, detail="Inactive user") return current_user - -""" -class User(BaseModel): - username : str - name : str | None = None - surname : str | None = None - email : EmailStr | None = None - role : Role | None = None - status : Status | None = None - bio : str | None = None - created_date : datetime | None = None - - collections : list[str] | None = None - items = list[str] | None = None - -class UserInDB(User): - hashed_password : str | None = None - -class UserSelfProfile(BaseModel): - username : str - name : str | None = None - surname : str | None = None - email : EmailStr | None = None - role : Role | None = None - status : Status | None = None - bio : str | None = None - created_date : datetime | None = None - - collections : list[str] | None = None - items = list[str] | None = None - -class UserPublicProfile(BaseModel): - username : str - role : Role | None = None - bio : str | None = None - created_date : datetime | None = None - collections : list[str] | None = None - items = list[str] | None = None - -""" \ No newline at end of file diff --git a/auth/router.py b/auth/router.py index 39110e2..2e1a25d 100644 --- a/auth/router.py +++ b/auth/router.py @@ -1,10 +1,15 @@ from fastapi import APIRouter, Depends, HTTPException, status -from .models import UserInDb, User, Role, Token, UserPublic -from .models import get_current_active_user, authenticate_user, create_access_token , fake_db, get_current_user +from .models import UserInDb, Role, Token, UserPublic, UserBase +from .models import get_current_active_user, authenticate_user, create_access_token,get_current_user from datetime import timedelta, datetime, timezone +from ..auth.models import get_password_hash, verify_password from ..config import ACCESS_TOKEN_EXPIRE_MINUTES from typing import Annotated, Optional +from sqlmodel import Session +from ..config import get_session_db +from fastapi import Depends from fastapi.security import OAuth2PasswordRequestForm +from .models import UserCreate, DBUser router = APIRouter( @@ -16,7 +21,7 @@ router = APIRouter( @router.get("/me") async def read_users_me( - current_user: Annotated[User, Depends(get_current_active_user)], + current_user: Annotated[UserBase, Depends(get_current_active_user)], ) -> UserPublic: return current_user @@ -28,15 +33,16 @@ def ADMIN(current_user: Annotated[UserInDb, Depends(get_current_user)]): @router.get('/home') -async def home(current_user : Annotated[User, Depends(ADMIN)]): +async def home(current_user : Annotated[UserBase, Depends(ADMIN)]): return {"message" : f"Welcome to home page {current_user.username}"} @router.post('/login') async def login_for_access_token( form_data : Annotated[OAuth2PasswordRequestForm, Depends()], + session : Annotated[Session, Depends(get_session_db)], ) -> Token: - user = authenticate_user(fake_db, form_data.username, form_data.password) + user = authenticate_user(session, form_data.username, form_data.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -45,6 +51,26 @@ async def login_for_access_token( ) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( - data={"sub": user['username'], "role": user['role'], 'status': user['status']}, expires_delta=access_token_expires + data={"sub": user.username, "role": user.role, 'status': user.status}, expires_delta=access_token_expires ) - return Token(access_token=access_token, token_type="bearer") \ No newline at end of file + return Token(access_token=access_token, token_type="bearer") + + +@router.post('/register', response_model=UserPublic) +async def create_user( + session : Annotated[Session, Depends(get_session_db)], + user : Annotated[UserCreate, Depends()] +): + user_dict = user.dict() + print(user.password) + user_dict['hashed_password'] = get_password_hash(user.password) + print (user_dict['hashed_password']) + + if not verify_password(user.password, user_dict['hashed_password']): + raise HTTPException(status_code=400, detail="Password hashing failed") + + db_user = DBUser.model_validate(user_dict) + session.add(db_user) + session.commit() + session.refresh(db_user) + return db_user \ No newline at end of file diff --git a/config.py b/config.py index c615c11..b2fcaf3 100644 --- a/config.py +++ b/config.py @@ -4,18 +4,30 @@ from sqlalchemy.orm import sessionmaker from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from passlib.context import CryptContext +from sqlmodel import SQLModel, Field, Session from dotenv import load_dotenv import os load_dotenv() -Base = declarative_base() #basic class for declarative models +# Veritabanı URL'sini oluştur +DATABASE_URL = ( + f"postgresql://{os.getenv('USERNAME_DB')}:" + f"{os.getenv('PASSWORD_DB')}@" + f"{os.getenv('HOST_DB')}:" + f"{os.getenv('PORT_DB')}/" + f"{os.getenv('NAME_DB')}" +) + +engine = create_engine(DATABASE_URL, echo=False) +def init_db(): + SQLModel.metadata.create_all(engine) + +def get_session_db(): + with Session(engine) as session: + yield session -DATABASE_URL = f"postgresql://{os.getenv('USERNAME_DB')}:{os.getenv('PASSWORD_DB')}@{os.getenv('HOST_DB')}:{os.getenv('PORT_DB')}/{os.getenv('NAME_DB')}" -engine = create_engine(DATABASE_URL) -SessionLocal = sessionmaker(bind=engine) -Base.metadata.create_all(bind=engine) ### SECRET KEY ### @@ -34,6 +46,9 @@ origins = [ ] app = FastAPI() +@app.on_event("startup") +def on_startup(): + init_db() app.add_middleware( CORSMiddleware, @@ -42,3 +57,6 @@ app.add_middleware( allow_methods=["*"], allow_headers=["*"], ) + + + diff --git a/docker-compose.yml b/docker-compose.yml index aa503a2..76c3981 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,10 +9,4 @@ services: POSTGRES_DB: postgres_db ports: - - "5434:5432" - volumes: - - postgres_data:/db - -volumes: - postgres_data: - driver: local \ No newline at end of file + - "5434:5432" \ No newline at end of file diff --git a/items/models.py b/items/models.py index c4f9981..8d7cb69 100644 --- a/items/models.py +++ b/items/models.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta, timezone -from ..auth.models import UserInDb +from ..auth.models import UserBase -class UserProfile(UserInDb): +class UserProfile(UserBase): bio : str | None = None created_date : datetime | None = None collections : list[str] | None = None diff --git a/requirements.txt b/requirements.txt index 5a6d67c..3f98454 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,6 +30,7 @@ rich-toolkit==0.14.4 shellingham==1.5.4 sniffio==1.3.1 SQLAlchemy==2.0.40 +sqlmodel==0.0.24 starlette==0.46.2 typer==0.15.3 typing-inspection==0.4.0