diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dec8b0a --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +#Enviroment +#ENVIROMENT=production +ENVIROMENT=development + +#Auth +TOKEN_EXPIRE_MINUTES=3600 +MY_SECRET_KEY=bb635b7c649162d4623307e6633dfb595437bd7cefe86cc63349156cc52b212c + +#Database +DB_NAME=root +DB_USER=root +DB_PASS=6UF4Kie7tzLSeThX78tqBUuptI1th8T6 +DB_HOST=localhost +DB_PORT=5432 + diff --git a/README.md b/README.md index 496740e..58c4f0c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ Follow these steps to set up and run the project on your local machine: ```sh git clone git@github.com:vcjuliocesar/pynotes-api.git ``` -rename env.example by .env +**Create enviroment file** +rename .env.example by .env **Create a Virtual Environment:** ```sh diff --git a/config/config.py b/config/config.py index c375c86..563978f 100644 --- a/config/config.py +++ b/config/config.py @@ -4,13 +4,17 @@ settings = Settings() class ProductionConfig: + DATABASE_URI = f"postgresql://{settings.DB_USER}:{settings.DB_PASS}@{settings.DB_HOST}/{settings.DB_NAME}" + DEBUG = False class DevelopmentConfig: + sqlite_file_name = "../database.sqlite" base_dir = os.path.dirname(os.path.realpath(__file__)) DATABASE_URI = f"sqlite:///{os.path.join(base_dir,sqlite_file_name)}" + DEBUG = True \ No newline at end of file diff --git a/config/database.py b/config/database.py index 0e1b76f..f57be83 100644 --- a/config/database.py +++ b/config/database.py @@ -1,4 +1,3 @@ -import os from sqlalchemy import create_engine from sqlalchemy.orm.session import sessionmaker from sqlalchemy.ext.declarative import declarative_base @@ -8,11 +7,13 @@ settings = Settings() if settings.ENVIROMENT == "production": + conf = ProductionConfig database_url = conf.DATABASE_URI else: + conf = DevelopmentConfig database_url = conf.DATABASE_URI diff --git a/env.example b/env.example deleted file mode 100644 index 657f278..0000000 --- a/env.example +++ /dev/null @@ -1,14 +0,0 @@ -#Enviroment -ENVIROMENT = development - -#Auth -TOKEN_EXPIRE_MINUTES = 3600 -MY_SECRET_KEY = 1234569089080990 - -#Database -DB_NAME=api -DB_USER=root -DB_PASS=root -DB_HOST=localhost -DB_PORT=5432 - \ No newline at end of file diff --git a/main.py b/main.py index a886a46..1077dd8 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,4 @@ -from fastapi import FastAPI,HTTPException,status -from fastapi.security import HTTPBearer -from utils.jwt_manager import validate_token -from starlette.requests import Request +from fastapi import FastAPI from config.database import engine,Base from middlewares.error_handler import ErrorHandler from config.config import ProductionConfig,DevelopmentConfig diff --git a/middlewares/error_handler.py b/middlewares/error_handler.py index a40382f..d27c3eb 100644 --- a/middlewares/error_handler.py +++ b/middlewares/error_handler.py @@ -6,11 +6,15 @@ from starlette.responses import Response class ErrorHandler(BaseHTTPMiddleware): + def __init__(self, app: FastAPI) -> None: super().__init__(app) async def dispatch(self, request: Request, call_next) -> Response | JSONResponse: try: + return await call_next(request) + except Exception as error: + return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,content={"error":str(error)}) \ No newline at end of file diff --git a/middlewares/jwt_bearer.py b/middlewares/jwt_bearer.py index 38e9c64..e96ae09 100644 --- a/middlewares/jwt_bearer.py +++ b/middlewares/jwt_bearer.py @@ -4,24 +4,34 @@ from starlette.requests import Request from services.user import UserService from config.database import Session -from datetime import datetime,timedelta +from datetime import datetime class JWTBearer(HTTPBearer): + async def __call__(self, request: Request): + credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Ivalid Credentials") + db = Session() + auth = await super().__call__(request) + data = validate_token(auth.credentials) + expire = datetime.fromtimestamp(data['exp']) + user = UserService(db).get_user_by_email(email=data["email"]) if not user: + raise credentials_exception if expire is None: + raise credentials_exception if datetime.utcnow() > expire: + raise credentials_exception return user \ No newline at end of file diff --git a/models/note.py b/models/note.py index 5afce74..1e82f2c 100644 --- a/models/note.py +++ b/models/note.py @@ -7,8 +7,11 @@ class Note(Base): __tablename__ = "notes" id = Column(Integer,primary_key=True) + title = Column(String) + content = Column(String) + owner_id = Column(Integer,ForeignKey("users.id")) owner = relationship("User",back_populates="notes") \ No newline at end of file diff --git a/models/user.py b/models/user.py index 1d161e7..615c2d4 100644 --- a/models/user.py +++ b/models/user.py @@ -7,8 +7,11 @@ class User(Base): __tablename__ = "users" id = Column(Integer,primary_key=True) + email = Column(String,unique=True) + password = Column(String) + is_active = Column(Boolean,default=True) notes = relationship("Note",back_populates="owner") \ No newline at end of file diff --git a/routers/note.py b/routers/note.py index 7e2bbb4..c9d5f1b 100644 --- a/routers/note.py +++ b/routers/note.py @@ -11,19 +11,26 @@ @note_router.get('/',tags=['Notes'],response_model=List[Note],status_code=status.HTTP_200_OK,dependencies=[Depends(JWTBearer())]) def get_notes(token:str = Depends(JWTBearer())) -> List[Note]: + db = Session() + result = NoteService(db).get_notes(token.id) + if not result: + return JSONResponse(status_code=status.HTTP_404_NOT_FOUND,content={"message":"Note not found"}) return JSONResponse(status_code=status.HTTP_200_OK,content=jsonable_encoder(result)) @note_router.get('/notes/{id}',tags=['Notes'],response_model=Note,status_code=status.HTTP_200_OK,dependencies=[Depends(JWTBearer())]) def get_note(id:int = Path(le=2000),token:str = Depends(JWTBearer())) ->Note : + db = Session() + result = NoteService(db).get_note(id,token.id) if not result: + return JSONResponse(status_code=status.HTTP_404_NOT_FOUND,content={"message":"Note not found"}) return JSONResponse(status_code=status.HTTP_200_OK,content=jsonable_encoder(result)) @@ -32,26 +39,40 @@ def get_note(id:int = Path(le=2000),token:str = Depends(JWTBearer())) ->Note : @note_router.post('/notes',tags=['Notes'],response_model=dict,status_code=status.HTTP_200_OK,dependencies=[Depends(JWTBearer())]) def create_note(note:NoteCreate,token:str = Depends(JWTBearer())) -> dict: + db = Session() + NoteService(db).create_note(note,token.id) + return JSONResponse(status_code=status.HTTP_201_CREATED,content={"message":"Note created successfully"}) @note_router.put('/notes',tags=['Notes'],response_model=dict,status_code=status.HTTP_200_OK,dependencies=[Depends(JWTBearer())]) def update_note(id:int,note:NoteBase,token:str = Depends(JWTBearer()))->dict: + db = Session() + result = NoteService(db).get_note(id,token.id) if not result: + return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST,content={"message":"Oops, something went wrong! Try again later."}) + NoteService(db).update_note(id,note,token.id) + return JSONResponse(status_code=status.HTTP_200_OK,content={"message":"Note updated successfully"}) @note_router.delete('/notes/{id}',tags=['Notes'],response_model=dict,status_code=status.HTTP_200_OK,dependencies=[Depends(JWTBearer())]) def delete_note(id:int,token:str = Depends(JWTBearer())) -> dict: + db = Session() + result = NoteService(db).get_note(id,token.id) + if not result: + return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST,content={"message":"Oops, something went wrong! Try again later."}) + NoteService(db).delete_note(id,token.id) + return JSONResponse(status_code=status.HTTP_200_OK,content={"message":"Note deleted successfully"}) \ No newline at end of file diff --git a/routers/user.py b/routers/user.py index be72009..65e335d 100644 --- a/routers/user.py +++ b/routers/user.py @@ -10,20 +10,30 @@ @user_router.post('/users',tags=['Auth'],response_model=User,status_code=status.HTTP_200_OK) def create_user(user:UserCreate): + db = Session() + result = UserService(db).get_user_by_email(email=user.email) - if not result: - UserService(db).create_user(user) - return JSONResponse(status_code=status.HTTP_200_OK,content={"message":"User created"}) - return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST,content={"message":"User already exists"}) + if result: + + return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST,content={"message":"User already exists"}) + + UserService(db).create_user(user) + + return JSONResponse(status_code=status.HTTP_200_OK,content={"message":"User created"}) + @user_router.post('/login',tags=['Auth'],status_code=status.HTTP_200_OK) def login(user:UserCreate): + db = Session() result = UserService(db).get_user_by_email(email=user.email) - if result and Auth().verify_password(user.password,result.password): - token:str = create_token(user.dict()) - return JSONResponse(status_code=status.HTTP_200_OK,content=token) - return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED,content={"message":"Unauthorized"}) \ No newline at end of file + if not (result and Auth().verify_password(user.password,result.password)): + + return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED,content={"message":"Unauthorized"}) + + token:str = create_token(user.dict()) + + return JSONResponse(status_code=status.HTTP_200_OK,content=token) \ No newline at end of file diff --git a/schemas/note.py b/schemas/note.py index 14343cf..6afbd3d 100644 --- a/schemas/note.py +++ b/schemas/note.py @@ -2,8 +2,9 @@ from typing import Optional class NoteBase(BaseModel): - #id:Optional[int] = Field(default=None) + title:str = Field(min_length=5,max_length=15) + content:str = Field(min_length=5,max_length=200) class Config: @@ -13,11 +14,14 @@ class Config: 'content':'my content' } } + class NoteCreate(NoteBase): pass class Note(NoteBase): + id:Optional[int] = Field(default=None) + owner_id:int class Config: diff --git a/schemas/user.py b/schemas/user.py index abd098a..0028354 100644 --- a/schemas/user.py +++ b/schemas/user.py @@ -3,8 +3,11 @@ class UserBase(BaseModel): + email:str + class Config: + json_schema_extra = { "example":{ 'email':'jhon.doe@fake.com', @@ -12,12 +15,17 @@ class Config: } } + class UserCreate(UserBase): + password:str = Field(min_length=8,max_length=16) + class User(UserBase): + id:Optional[int] = Field(default=None) + is_active:bool class Config: diff --git a/services/auth.py b/services/auth.py index d1a7f81..1560ab4 100644 --- a/services/auth.py +++ b/services/auth.py @@ -1,11 +1,15 @@ from passlib.context import CryptContext class Auth(): + def __init__(self): + self.pwd_context = CryptContext(schemes=['bcrypt'],deprecated="auto") def verify_password(self,password:str,hash:str): + return self.pwd_context.verify(password,hash) def get_password(self,password:str): + return self.pwd_context.hash(password) \ No newline at end of file diff --git a/services/note.py b/services/note.py index 808cd4e..37d159f 100644 --- a/services/note.py +++ b/services/note.py @@ -2,32 +2,52 @@ from schemas.note import Note,NoteBase,NoteCreate class NoteService(): + def __init__(self,db) -> None: + self.db = db def get_notes(self,owner_id:int): + result = self.db.query(NoteModel).filter(NoteModel.owner_id == owner_id).all() + return result def get_note(self,id:int,owner_id:int): + result = self.db.query(NoteModel).filter((NoteModel.id == id) & (NoteModel.owner_id == owner_id)).first() + return result def create_note(self,data:Note,owner_id:int): + new_note = NoteModel(**data.dict(),owner_id = owner_id) + self.db.add(new_note) + self.db.commit() + self.db.refresh(new_note) + return def update_note(self,id:int,data:NoteBase,owner_id:int): + note = self.get_note(id,owner_id) + note.title = data.title + note.content = data.content + self.db.commit() + return def delete_note(self,id:int,owner_id:int): + self.db.query(NoteModel).filter((NoteModel.id == id) and (NoteModel.owner_id == owner_id)).delete() + self.db.commit() - return \ No newline at end of file + + return + \ No newline at end of file diff --git a/services/user.py b/services/user.py index 39f012e..ebc47b7 100644 --- a/services/user.py +++ b/services/user.py @@ -5,18 +5,26 @@ class UserService(): def __init__(self,db) -> None: + self.db = db def get_user(self,id:int): + return self.db.query(UserModel).filter(UserModel.id == id).first() def get_user_by_email(self,email:str): + return self.db.query(UserModel).filter(UserModel.email == email).first() def create_user(self,user:UserCreate): + user.password = Auth().get_password(user.password) + new_user = UserModel(**user.dict()) + self.db.add(new_user) + self.db.commit() + return \ No newline at end of file