From 6a6afdbe72c626b01245c9372c9d10be79789bb0 Mon Sep 17 00:00:00 2001 From: Rasmus Luha Date: Sun, 6 Feb 2022 13:41:11 +0200 Subject: restrucurring the stucture of the folderstruture --> structure --- .env | 8 ++++ Projekt/.env | 8 ---- Projekt/app/__init__.py | 0 Projekt/app/config.py | 16 ------- Projekt/app/database.py | 38 ----------------- Projekt/app/main.py | 17 -------- Projekt/app/models.py | 31 -------------- Projekt/app/oauth2.py | 49 --------------------- Projekt/app/routers/auth.py | 22 ---------- Projekt/app/routers/post.py | 102 -------------------------------------------- Projekt/app/routers/user.py | 27 ------------ Projekt/app/routers/vote.py | 45 ------------------- Projekt/app/schemas.py | 61 -------------------------- Projekt/app/utils.py | 9 ---- app/__init__.py | 0 app/config.py | 16 +++++++ app/database.py | 38 +++++++++++++++++ app/main.py | 17 ++++++++ app/models.py | 31 ++++++++++++++ app/oauth2.py | 49 +++++++++++++++++++++ app/routers/auth.py | 22 ++++++++++ app/routers/post.py | 102 ++++++++++++++++++++++++++++++++++++++++++++ app/routers/user.py | 27 ++++++++++++ app/routers/vote.py | 45 +++++++++++++++++++ app/schemas.py | 61 ++++++++++++++++++++++++++ app/utils.py | 9 ++++ 26 files changed, 425 insertions(+), 425 deletions(-) create mode 100644 .env delete mode 100644 Projekt/.env delete mode 100644 Projekt/app/__init__.py delete mode 100644 Projekt/app/config.py delete mode 100644 Projekt/app/database.py delete mode 100644 Projekt/app/main.py delete mode 100644 Projekt/app/models.py delete mode 100644 Projekt/app/oauth2.py delete mode 100644 Projekt/app/routers/auth.py delete mode 100644 Projekt/app/routers/post.py delete mode 100644 Projekt/app/routers/user.py delete mode 100644 Projekt/app/routers/vote.py delete mode 100644 Projekt/app/schemas.py delete mode 100644 Projekt/app/utils.py create mode 100644 app/__init__.py create mode 100644 app/config.py create mode 100644 app/database.py create mode 100644 app/main.py create mode 100644 app/models.py create mode 100644 app/oauth2.py create mode 100644 app/routers/auth.py create mode 100644 app/routers/post.py create mode 100644 app/routers/user.py create mode 100644 app/routers/vote.py create mode 100644 app/schemas.py create mode 100644 app/utils.py diff --git a/.env b/.env new file mode 100644 index 0000000..a024c73 --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +DATABASE_HOSTNAME=localhost +DATABASE_PORT=5432 +DATABASE_PASSWORD=password123 +DATABASE_NAME=fastapi +DATABASE_USERNAME=postgres +SECRET_KEY=131abcf910f1ec1dc0cf33bbaf5157654e54b62d211f28b95176d3d4bef5d52c +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 diff --git a/Projekt/.env b/Projekt/.env deleted file mode 100644 index a024c73..0000000 --- a/Projekt/.env +++ /dev/null @@ -1,8 +0,0 @@ -DATABASE_HOSTNAME=localhost -DATABASE_PORT=5432 -DATABASE_PASSWORD=password123 -DATABASE_NAME=fastapi -DATABASE_USERNAME=postgres -SECRET_KEY=131abcf910f1ec1dc0cf33bbaf5157654e54b62d211f28b95176d3d4bef5d52c -ALGORITHM=HS256 -ACCESS_TOKEN_EXPIRE_MINUTES=30 diff --git a/Projekt/app/__init__.py b/Projekt/app/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Projekt/app/config.py b/Projekt/app/config.py deleted file mode 100644 index d7f622d..0000000 --- a/Projekt/app/config.py +++ /dev/null @@ -1,16 +0,0 @@ -from pydantic import BaseSettings - -class Settings(BaseSettings): - database_hostname: str - database_port: str - database_password: str - database_name: str - database_username: str - secret_key: str - algorithm: str - access_token_expire_minutes: int - - class Config: - env_file = ".env" - -settings = Settings() diff --git a/Projekt/app/database.py b/Projekt/app/database.py deleted file mode 100644 index 1abafa0..0000000 --- a/Projekt/app/database.py +++ /dev/null @@ -1,38 +0,0 @@ -from sqlalchemy import create_engine # Copied imports from fastapi sqlalchemy docs. -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker -from psycopg2.extras import RealDictCursor -from time import sleep -import psycopg2 #Library to connect to postgres -from .config import settings - -SQLALCHEMY_DATABASE_URL = f'postgresql://{settings.database_username}:{settings.database_password}@{settings.database_hostname}:{settings.database_port}/{settings.database_name}' - - -engine = create_engine(SQLALCHEMY_DATABASE_URL) - -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -Base = declarative_base() - -# Dependency -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - - -# Setting connection with DataBase -# PS: thats only for reference --> kautad nüüd ju hoopiki sqlalchemyt db sessioniks. Ülal - -#while True: -# try: -# conn = psycopg2.connect(host='localhost', database='fastapi', user='postgres', password='password123', cursor_factory = RealDictCursor)#CursorFactory minig library teema, no matter::prg pole password -# cursor = conn.cursor() -# print("DataBase Connection Was a Huge and Massive Success!") -# break -# except Exception as error: -# print("Connecting to database failed") -# print("error was: ", error) -# sleep(3) diff --git a/Projekt/app/main.py b/Projekt/app/main.py deleted file mode 100644 index 87db9d0..0000000 --- a/Projekt/app/main.py +++ /dev/null @@ -1,17 +0,0 @@ -from fastapi import FastAPI -from . import models -from .database import engine, SessionLocal -from .routers import post, user, auth, vote -from .config import settings - -models.Base.metadata.create_all(bind=engine) -app = FastAPI() - -app.include_router(post.router) -app.include_router(user.router) -app.include_router(auth.router) -app.include_router(vote.router) - -@app.get("/") -async def root(): - return {"message" : "Hellow piipl"} diff --git a/Projekt/app/models.py b/Projekt/app/models.py deleted file mode 100644 index 97d1e35..0000000 --- a/Projekt/app/models.py +++ /dev/null @@ -1,31 +0,0 @@ -from sqlalchemy import Column, Integer, String, Boolean, ForeignKey -from sqlalchemy.orm import relationship -from sqlalchemy.sql.expression import text -from sqlalchemy.sql.sqltypes import TIMESTAMP -from .database import Base - -class Post(Base): - __tablename__= "posts" - - id = Column(Integer, primary_key=True, nullable=False) - title = Column(String, nullable=False) - content = Column(String, nullable=False) - published = Column(Boolean, server_default="true", nullable=False) - created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text('now()') ) - - owner_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) - owner = relationship("User") #Fetcib owner_id põhjal( see foreign key) vastava Posti teinud User callsi useri - -class User(Base): - __tablename__ = "users" - - id = Column(Integer, primary_key=True, nullable=False) - email = Column(String, nullable=False, unique = True) - password = Column(String, nullable=False) - created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text('now()') ) - - -class Vote(Base): - __tablename__ = "votes" - user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True) - post_id = Column(Integer, ForeignKey("posts.id", ondelete="CASCADE"), primary_key=True) diff --git a/Projekt/app/oauth2.py b/Projekt/app/oauth2.py deleted file mode 100644 index f381f97..0000000 --- a/Projekt/app/oauth2.py +++ /dev/null @@ -1,49 +0,0 @@ -from fastapi import Depends, status, HTTPException -from jose import JWTError, jwt -from datetime import datetime, timedelta -from sqlalchemy.orm import Session -from . import schemas, database, models -from fastapi.security import OAuth2PasswordBearer -from .config import settings - -oaut2_scheme = OAuth2PasswordBearer(tokenUrl="login") - -#Secrete_Key -#Algorütm -#Säilivusaeg, expiration time - -SECRET_KEY = settings.secret_key -ALGORITHM = settings.algorithm -ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes - -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 verify_access_token(token: str, credentials_exception): - - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) #Decodeme tokeni - id: str = payload.get("user_id") # Ekstraktime tokenist id - if id is None: - raise credentials_exception - token_data = schemas.TokenData(id=id) - except JWTError: - raise credentials_exception - - return token_data - - -def get_current_user( token: str = Depends(oaut2_scheme), db: Session = Depends(database.get_db)): - credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, - detail=f"Could not validate credentials", headers={"WWW-Authenticate": "Bearer"} ) - - token = verify_access_token(token, credentials_exception) - user = db.query(models.User).filter(models.User.id == token.id).first() - - return user diff --git a/Projekt/app/routers/auth.py b/Projekt/app/routers/auth.py deleted file mode 100644 index 30668cf..0000000 --- a/Projekt/app/routers/auth.py +++ /dev/null @@ -1,22 +0,0 @@ -from fastapi import APIRouter, Depends, status, HTTPException, Response -from fastapi.security.oauth2 import OAuth2PasswordRequestForm -from sqlalchemy.orm import Session -from .. import database, schemas, models, utils, oauth2 - -router = APIRouter(tags = ["Authentication"]) - -@router.post("/login", response_model = schemas.Token) -def login(user_credentials: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(database.get_db)): #OAuth2Password... --> nüüd ei oota api requesti bodysse email, password vaid hoopis form-data. - - user = db.query(models.User).filter(models.User.email == user_credentials.username).first() - - if not user: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid Credentials unfortunatuun") - - if not utils.verify(user_credentials.password, user.password): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN , detail="Invalid Credentials unfortunatuun") - - #Create and retrn token - access_token = oauth2.create_access_token(data = {"user_id":user.id}) - - return {"access_token" : access_token, "token_type" : "bearer" } diff --git a/Projekt/app/routers/post.py b/Projekt/app/routers/post.py deleted file mode 100644 index ce1c11d..0000000 --- a/Projekt/app/routers/post.py +++ /dev/null @@ -1,102 +0,0 @@ -from fastapi import FastAPI, Response, status, HTTPException, Depends, APIRouter -from .. import models, schemas, oauth2 -from sqlalchemy import func -from sqlalchemy.orm import Session -from ..database import get_db -from typing import List, Optional - -router = APIRouter(prefix="/posts", tags=["Posts"]) - - -@router.get("/", response_model=List[schemas.PostOut]) # Siin List (from typing lib), sest võttame mitu posti, teistes vaid 1 -async def get_posts(db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user) - ,limit: int = 10, skip: int = 0, search: Optional[str] = ""): - - #cursor.execute(""" SELECT * FROM posts """) - #posts = cursor.fetchall() - - - #Lots of query parameter stuff here, kinda testing atm. - #posts = db.query(models.Post).filter(models.Post.title.contains(search)).limit(limit).offset(skip).all() - - posts = db.query(models.Post, func.count(models.Vote.post_id).label("votes") ).join(models.Vote, - models.Vote.post_id == models.Post.id, isouter=True).group_by(models.Post.id).filter(models.Post.title.contains(search)).limit(limit).offset(skip).all() - - return posts #FastAPI converts automaticaly into json types stuff - - -@router.get("/{id}", response_model=schemas.PostOut) #ID is called "path parameter" , could name it dingdong if wanted. Its just like a argument/variable -async def get_post(id: int, db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user)): #id: int > Et url ei oleks "afasdfasdfasf" <-- Siis viskab errori. Response imported from FastAPI - #cursor.execute(""" SELECT * FROM posts WHERE id = %s RETURNING *""", (str(id))) - #post = cursor.fetchone() - #post = db.query(models.Post).filter(models.Post.id == id).first() - - post = db.query(models.Post, func.count(models.Vote.post_id).label("votes") ).join(models.Vote, - models.Vote.post_id == models.Post.id, isouter=True).group_by(models.Post.id).filter(models.Post.id == id).first() - - - if not post: - raise HTTPException(status_code = status.HTTP_404_NOT_FOUND, - detail=f"Post with id: {id} was not found") - return post - - -@router.post("/", status_code = status.HTTP_201_CREATED, response_model=schemas.Post) # Create puhul default kood 201. Miks mitte lihtsalt "201", miks status.blabblalba -async def create_posts(post: schemas.PostCreate,db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user) ): # Ekstraktib body read dictiks "pay.. - #cursor.execute(""" INSERT INTO posts(title, content, published) VALUES (%s, %s, %s) - #RETURNING * """, (post.title, post.content, post.published) ) - #new_post = cursor.fetchone() - #conn.commit() - print(current_user.email) - - #new_post = models.Post(title=post.title, content=post.content, published=post.published) - new_post = models.Post(owner_id=current_user.id, **post.dict()) # Sama mis ülal, ilusamini - - - db.add(new_post) - db.commit() # not sure why have to add+commit, but hey - db.refresh(new_post) # sama mis SQL: "RESPONDING *" - return new_post - -@router.put("/{id}") -def update_post(id: int, post: schemas.PostCreate, db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user) ): - - post_query = db.query(models.Post).filter(models.Post.id == id) - posters = post_query.first() - - if posters == None: - raise HTTPException(status_code = status.HTTP_404_NOT_FOUND, detail = f"post with id: {id} was not found") - - - if posters.owner_id != current_user.id: - raise HTTPException(status_code = status.HTTP_403_FORBIDDEN, - detail=f"Not authorized duuud") - - post_query.update(post.dict(), synchronize_session=False) - - db.commit() - - return post_query.first() - - -@router.delete("/{id}", status_code = status.HTTP_204_NO_CONTENT) -async def delete_post(id: int, db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user) ): - #cursor.execute(""" DELETE FROM posts WHERE id = %s RETURNING * """, (str(id),)) - #deleted_post = cursor.fetchone() - #conn.commit() - - post_query = db.query(models.Post).filter(models.Post.id == id) - post = post_query.first() - - if post == None: - raise HTTPException(status_code = status.HTTP_404_NOT_FOUND, - detail=f"Post with id: {id} was not found") - - if post.owner_id != current_user.id: - raise HTTPException(status_code = status.HTTP_403_FORBIDDEN, - detail=f"Not authorized duuud") - - post_query.delete(synchronize_session=False) # Ei tea mis teeb, fastapi docs ütles et panna, vist ka default - db.commit() - - return Response(status_code = status.HTTP_204_NO_CONTENT) diff --git a/Projekt/app/routers/user.py b/Projekt/app/routers/user.py deleted file mode 100644 index 1afe06e..0000000 --- a/Projekt/app/routers/user.py +++ /dev/null @@ -1,27 +0,0 @@ -from fastapi import FastAPI, Response, status, HTTPException, Depends, APIRouter -from .. import models, schemas, utils -from sqlalchemy.orm import Session -from ..database import get_db - -router = APIRouter(prefix = "/users", tags=["Users"]) - -@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.UserOut) -def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): - #hash the password <-> user.password - hashed_password= utils.hash(user.password) - user.password = hashed_password - - new_user = models.User(**user.dict()) - db.add(new_user) - db.commit() # not sure why have to add+commit, but hey - db.refresh(new_user) # sama mis SQL: "RESPONDING *" - return new_user - -@router.get("/{id}", response_model=schemas.UserOut) -def get_user(id: int, db: Session = Depends(get_db)): - user = db.query(models.User).filter(models.User.id == id).first() - - if not user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"User with id: {id} not found") - - return user diff --git a/Projekt/app/routers/vote.py b/Projekt/app/routers/vote.py deleted file mode 100644 index af17b88..0000000 --- a/Projekt/app/routers/vote.py +++ /dev/null @@ -1,45 +0,0 @@ -from fastapi import APIRouter, Depends, status, HTTPException, Response -from sqlalchemy.orm import Session -from .. import schemas, database, models, oauth2 - -router = APIRouter( - prefix="/vote", - tags=["vote"] -) - - -@router.post("/", status_code=status.HTTP_201_CREATED) -def vote(vote: schemas.Vote, db: Session = Depends(database.get_db) - , current_user: int = Depends(oauth2.get_current_user) ): - - post = db.query(models.Post).filter(models.Post.id == vote.post_id).first() # Cheki seda, et ei proovi likeda posti mida ei eksisteeri - if not post: - raise HTTPException(status_code=status.HTTP404_NOT_FOUND, - detail=f"The post with id {vote.id} that you are trying to like doesn't exsits") - - - vote_query = db.query(models.Vote).filter(models.Vote.post_id == vote.post_id, - models.Vote.user_id == current_user.id) - found_vote=vote_query.first() # kui oled juba posti likenud, siis filter leiab, kui ei ole, siis ei leia - - if (vote.dir == 1): - if found_vote: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, - detail=f"user {current_user.email} has alreadey liked the post {vote.post_id}") - new_vote = models.Vote(post_id = vote.post_id, user_id = current_user.id) - db.add(new_vote) - db.commit() - return {"Message":"Successfully liked Post"} - else: - if not found_vote: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, - detail=f"user {current_user.email} has not liked the post {vote.post_id}") - vote_query.delete(synchronize_session=False) - db.commit() - - return {"Message":"Succesfulle Unliked the Post"} - - - - - diff --git a/Projekt/app/schemas.py b/Projekt/app/schemas.py deleted file mode 100644 index 439722c..0000000 --- a/Projekt/app/schemas.py +++ /dev/null @@ -1,61 +0,0 @@ -from pydantic import BaseModel, EmailStr -from pydantic.types import conint -from datetime import datetime -from typing import Optional - -class PostBase(BaseModel): - title: str - content: str - published: bool = True # Default True - -class PostCreate(PostBase): - pass - -# User Stuff - -class UserCreate(BaseModel): - email: EmailStr #Selleks vaja emaild-validator lib, mis tuli pip install fastapi[all]-iga koos. - password: str - -class UserOut(BaseModel): - id: int - email: EmailStr - created_at: datetime - - class Config: - orm_mode = True - - -class UserLogin(BaseModel): - email: EmailStr - password: str - - -class Token(BaseModel): - access_token: str - token_type: str - -class TokenData(BaseModel): - id: Optional[str] = None - - -## Response - -class Post(PostBase): - id: int - created_at: datetime - owner_id: int - owner: UserOut # Class alt poolt - see skeem, mille mis kehtib ka Get Useri puhul. - - class Config: # Selleks, et pydantic oskaks lugeda sqlalchemy type modelit mida talle et antakse - orm_mode = True - -class PostOut(BaseModel): - Post: Post - votes: int - - -class Vote(BaseModel): - post_id: int - dir: conint(le=1) #int that can be only 0 or 1 (and also negative, but that should be fine) - diff --git a/Projekt/app/utils.py b/Projekt/app/utils.py deleted file mode 100644 index 3cd58bf..0000000 --- a/Projekt/app/utils.py +++ /dev/null @@ -1,9 +0,0 @@ - -from passlib.context import CryptContext # passwordide hashimise jaoks - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -def hash(password: str): - return pwd_context.hash(password) - -def verify(plain_password, hashed_password): - return pwd_context.verify(plain_password, hashed_password) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..d7f622d --- /dev/null +++ b/app/config.py @@ -0,0 +1,16 @@ +from pydantic import BaseSettings + +class Settings(BaseSettings): + database_hostname: str + database_port: str + database_password: str + database_name: str + database_username: str + secret_key: str + algorithm: str + access_token_expire_minutes: int + + class Config: + env_file = ".env" + +settings = Settings() diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..1abafa0 --- /dev/null +++ b/app/database.py @@ -0,0 +1,38 @@ +from sqlalchemy import create_engine # Copied imports from fastapi sqlalchemy docs. +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from psycopg2.extras import RealDictCursor +from time import sleep +import psycopg2 #Library to connect to postgres +from .config import settings + +SQLALCHEMY_DATABASE_URL = f'postgresql://{settings.database_username}:{settings.database_password}@{settings.database_hostname}:{settings.database_port}/{settings.database_name}' + + +engine = create_engine(SQLALCHEMY_DATABASE_URL) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +# Setting connection with DataBase +# PS: thats only for reference --> kautad nüüd ju hoopiki sqlalchemyt db sessioniks. Ülal + +#while True: +# try: +# conn = psycopg2.connect(host='localhost', database='fastapi', user='postgres', password='password123', cursor_factory = RealDictCursor)#CursorFactory minig library teema, no matter::prg pole password +# cursor = conn.cursor() +# print("DataBase Connection Was a Huge and Massive Success!") +# break +# except Exception as error: +# print("Connecting to database failed") +# print("error was: ", error) +# sleep(3) diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..87db9d0 --- /dev/null +++ b/app/main.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from . import models +from .database import engine, SessionLocal +from .routers import post, user, auth, vote +from .config import settings + +models.Base.metadata.create_all(bind=engine) +app = FastAPI() + +app.include_router(post.router) +app.include_router(user.router) +app.include_router(auth.router) +app.include_router(vote.router) + +@app.get("/") +async def root(): + return {"message" : "Hellow piipl"} diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..97d1e35 --- /dev/null +++ b/app/models.py @@ -0,0 +1,31 @@ +from sqlalchemy import Column, Integer, String, Boolean, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.expression import text +from sqlalchemy.sql.sqltypes import TIMESTAMP +from .database import Base + +class Post(Base): + __tablename__= "posts" + + id = Column(Integer, primary_key=True, nullable=False) + title = Column(String, nullable=False) + content = Column(String, nullable=False) + published = Column(Boolean, server_default="true", nullable=False) + created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text('now()') ) + + owner_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + owner = relationship("User") #Fetcib owner_id põhjal( see foreign key) vastava Posti teinud User callsi useri + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, nullable=False) + email = Column(String, nullable=False, unique = True) + password = Column(String, nullable=False) + created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text('now()') ) + + +class Vote(Base): + __tablename__ = "votes" + user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True) + post_id = Column(Integer, ForeignKey("posts.id", ondelete="CASCADE"), primary_key=True) diff --git a/app/oauth2.py b/app/oauth2.py new file mode 100644 index 0000000..f381f97 --- /dev/null +++ b/app/oauth2.py @@ -0,0 +1,49 @@ +from fastapi import Depends, status, HTTPException +from jose import JWTError, jwt +from datetime import datetime, timedelta +from sqlalchemy.orm import Session +from . import schemas, database, models +from fastapi.security import OAuth2PasswordBearer +from .config import settings + +oaut2_scheme = OAuth2PasswordBearer(tokenUrl="login") + +#Secrete_Key +#Algorütm +#Säilivusaeg, expiration time + +SECRET_KEY = settings.secret_key +ALGORITHM = settings.algorithm +ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes + +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 verify_access_token(token: str, credentials_exception): + + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) #Decodeme tokeni + id: str = payload.get("user_id") # Ekstraktime tokenist id + if id is None: + raise credentials_exception + token_data = schemas.TokenData(id=id) + except JWTError: + raise credentials_exception + + return token_data + + +def get_current_user( token: str = Depends(oaut2_scheme), db: Session = Depends(database.get_db)): + credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, + detail=f"Could not validate credentials", headers={"WWW-Authenticate": "Bearer"} ) + + token = verify_access_token(token, credentials_exception) + user = db.query(models.User).filter(models.User.id == token.id).first() + + return user diff --git a/app/routers/auth.py b/app/routers/auth.py new file mode 100644 index 0000000..30668cf --- /dev/null +++ b/app/routers/auth.py @@ -0,0 +1,22 @@ +from fastapi import APIRouter, Depends, status, HTTPException, Response +from fastapi.security.oauth2 import OAuth2PasswordRequestForm +from sqlalchemy.orm import Session +from .. import database, schemas, models, utils, oauth2 + +router = APIRouter(tags = ["Authentication"]) + +@router.post("/login", response_model = schemas.Token) +def login(user_credentials: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(database.get_db)): #OAuth2Password... --> nüüd ei oota api requesti bodysse email, password vaid hoopis form-data. + + user = db.query(models.User).filter(models.User.email == user_credentials.username).first() + + if not user: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid Credentials unfortunatuun") + + if not utils.verify(user_credentials.password, user.password): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN , detail="Invalid Credentials unfortunatuun") + + #Create and retrn token + access_token = oauth2.create_access_token(data = {"user_id":user.id}) + + return {"access_token" : access_token, "token_type" : "bearer" } diff --git a/app/routers/post.py b/app/routers/post.py new file mode 100644 index 0000000..ce1c11d --- /dev/null +++ b/app/routers/post.py @@ -0,0 +1,102 @@ +from fastapi import FastAPI, Response, status, HTTPException, Depends, APIRouter +from .. import models, schemas, oauth2 +from sqlalchemy import func +from sqlalchemy.orm import Session +from ..database import get_db +from typing import List, Optional + +router = APIRouter(prefix="/posts", tags=["Posts"]) + + +@router.get("/", response_model=List[schemas.PostOut]) # Siin List (from typing lib), sest võttame mitu posti, teistes vaid 1 +async def get_posts(db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user) + ,limit: int = 10, skip: int = 0, search: Optional[str] = ""): + + #cursor.execute(""" SELECT * FROM posts """) + #posts = cursor.fetchall() + + + #Lots of query parameter stuff here, kinda testing atm. + #posts = db.query(models.Post).filter(models.Post.title.contains(search)).limit(limit).offset(skip).all() + + posts = db.query(models.Post, func.count(models.Vote.post_id).label("votes") ).join(models.Vote, + models.Vote.post_id == models.Post.id, isouter=True).group_by(models.Post.id).filter(models.Post.title.contains(search)).limit(limit).offset(skip).all() + + return posts #FastAPI converts automaticaly into json types stuff + + +@router.get("/{id}", response_model=schemas.PostOut) #ID is called "path parameter" , could name it dingdong if wanted. Its just like a argument/variable +async def get_post(id: int, db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user)): #id: int > Et url ei oleks "afasdfasdfasf" <-- Siis viskab errori. Response imported from FastAPI + #cursor.execute(""" SELECT * FROM posts WHERE id = %s RETURNING *""", (str(id))) + #post = cursor.fetchone() + #post = db.query(models.Post).filter(models.Post.id == id).first() + + post = db.query(models.Post, func.count(models.Vote.post_id).label("votes") ).join(models.Vote, + models.Vote.post_id == models.Post.id, isouter=True).group_by(models.Post.id).filter(models.Post.id == id).first() + + + if not post: + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND, + detail=f"Post with id: {id} was not found") + return post + + +@router.post("/", status_code = status.HTTP_201_CREATED, response_model=schemas.Post) # Create puhul default kood 201. Miks mitte lihtsalt "201", miks status.blabblalba +async def create_posts(post: schemas.PostCreate,db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user) ): # Ekstraktib body read dictiks "pay.. + #cursor.execute(""" INSERT INTO posts(title, content, published) VALUES (%s, %s, %s) + #RETURNING * """, (post.title, post.content, post.published) ) + #new_post = cursor.fetchone() + #conn.commit() + print(current_user.email) + + #new_post = models.Post(title=post.title, content=post.content, published=post.published) + new_post = models.Post(owner_id=current_user.id, **post.dict()) # Sama mis ülal, ilusamini + + + db.add(new_post) + db.commit() # not sure why have to add+commit, but hey + db.refresh(new_post) # sama mis SQL: "RESPONDING *" + return new_post + +@router.put("/{id}") +def update_post(id: int, post: schemas.PostCreate, db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user) ): + + post_query = db.query(models.Post).filter(models.Post.id == id) + posters = post_query.first() + + if posters == None: + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND, detail = f"post with id: {id} was not found") + + + if posters.owner_id != current_user.id: + raise HTTPException(status_code = status.HTTP_403_FORBIDDEN, + detail=f"Not authorized duuud") + + post_query.update(post.dict(), synchronize_session=False) + + db.commit() + + return post_query.first() + + +@router.delete("/{id}", status_code = status.HTTP_204_NO_CONTENT) +async def delete_post(id: int, db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user) ): + #cursor.execute(""" DELETE FROM posts WHERE id = %s RETURNING * """, (str(id),)) + #deleted_post = cursor.fetchone() + #conn.commit() + + post_query = db.query(models.Post).filter(models.Post.id == id) + post = post_query.first() + + if post == None: + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND, + detail=f"Post with id: {id} was not found") + + if post.owner_id != current_user.id: + raise HTTPException(status_code = status.HTTP_403_FORBIDDEN, + detail=f"Not authorized duuud") + + post_query.delete(synchronize_session=False) # Ei tea mis teeb, fastapi docs ütles et panna, vist ka default + db.commit() + + return Response(status_code = status.HTTP_204_NO_CONTENT) diff --git a/app/routers/user.py b/app/routers/user.py new file mode 100644 index 0000000..1afe06e --- /dev/null +++ b/app/routers/user.py @@ -0,0 +1,27 @@ +from fastapi import FastAPI, Response, status, HTTPException, Depends, APIRouter +from .. import models, schemas, utils +from sqlalchemy.orm import Session +from ..database import get_db + +router = APIRouter(prefix = "/users", tags=["Users"]) + +@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.UserOut) +def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + #hash the password <-> user.password + hashed_password= utils.hash(user.password) + user.password = hashed_password + + new_user = models.User(**user.dict()) + db.add(new_user) + db.commit() # not sure why have to add+commit, but hey + db.refresh(new_user) # sama mis SQL: "RESPONDING *" + return new_user + +@router.get("/{id}", response_model=schemas.UserOut) +def get_user(id: int, db: Session = Depends(get_db)): + user = db.query(models.User).filter(models.User.id == id).first() + + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"User with id: {id} not found") + + return user diff --git a/app/routers/vote.py b/app/routers/vote.py new file mode 100644 index 0000000..af17b88 --- /dev/null +++ b/app/routers/vote.py @@ -0,0 +1,45 @@ +from fastapi import APIRouter, Depends, status, HTTPException, Response +from sqlalchemy.orm import Session +from .. import schemas, database, models, oauth2 + +router = APIRouter( + prefix="/vote", + tags=["vote"] +) + + +@router.post("/", status_code=status.HTTP_201_CREATED) +def vote(vote: schemas.Vote, db: Session = Depends(database.get_db) + , current_user: int = Depends(oauth2.get_current_user) ): + + post = db.query(models.Post).filter(models.Post.id == vote.post_id).first() # Cheki seda, et ei proovi likeda posti mida ei eksisteeri + if not post: + raise HTTPException(status_code=status.HTTP404_NOT_FOUND, + detail=f"The post with id {vote.id} that you are trying to like doesn't exsits") + + + vote_query = db.query(models.Vote).filter(models.Vote.post_id == vote.post_id, + models.Vote.user_id == current_user.id) + found_vote=vote_query.first() # kui oled juba posti likenud, siis filter leiab, kui ei ole, siis ei leia + + if (vote.dir == 1): + if found_vote: + raise HTTPException(status_code=status.HTTP_409_CONFLICT, + detail=f"user {current_user.email} has alreadey liked the post {vote.post_id}") + new_vote = models.Vote(post_id = vote.post_id, user_id = current_user.id) + db.add(new_vote) + db.commit() + return {"Message":"Successfully liked Post"} + else: + if not found_vote: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail=f"user {current_user.email} has not liked the post {vote.post_id}") + vote_query.delete(synchronize_session=False) + db.commit() + + return {"Message":"Succesfulle Unliked the Post"} + + + + + diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000..439722c --- /dev/null +++ b/app/schemas.py @@ -0,0 +1,61 @@ +from pydantic import BaseModel, EmailStr +from pydantic.types import conint +from datetime import datetime +from typing import Optional + +class PostBase(BaseModel): + title: str + content: str + published: bool = True # Default True + +class PostCreate(PostBase): + pass + +# User Stuff + +class UserCreate(BaseModel): + email: EmailStr #Selleks vaja emaild-validator lib, mis tuli pip install fastapi[all]-iga koos. + password: str + +class UserOut(BaseModel): + id: int + email: EmailStr + created_at: datetime + + class Config: + orm_mode = True + + +class UserLogin(BaseModel): + email: EmailStr + password: str + + +class Token(BaseModel): + access_token: str + token_type: str + +class TokenData(BaseModel): + id: Optional[str] = None + + +## Response + +class Post(PostBase): + id: int + created_at: datetime + owner_id: int + owner: UserOut # Class alt poolt - see skeem, mille mis kehtib ka Get Useri puhul. + + class Config: # Selleks, et pydantic oskaks lugeda sqlalchemy type modelit mida talle et antakse + orm_mode = True + +class PostOut(BaseModel): + Post: Post + votes: int + + +class Vote(BaseModel): + post_id: int + dir: conint(le=1) #int that can be only 0 or 1 (and also negative, but that should be fine) + diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..3cd58bf --- /dev/null +++ b/app/utils.py @@ -0,0 +1,9 @@ + +from passlib.context import CryptContext # passwordide hashimise jaoks + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +def hash(password: str): + return pwd_context.hash(password) + +def verify(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) -- cgit v1.2.3