From 5e19a0569288de21365c61b0db78639880732dd0 Mon Sep 17 00:00:00 2001 From: Rasmus Luha Date: Sun, 6 Feb 2022 13:10:32 +0200 Subject: inital commit --- .gitignore | 3 ++ 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 ++++ 14 files changed, 428 insertions(+) create mode 100644 .gitignore create mode 100644 Projekt/.env create mode 100644 Projekt/app/__init__.py create mode 100644 Projekt/app/config.py create mode 100644 Projekt/app/database.py create mode 100644 Projekt/app/main.py create mode 100644 Projekt/app/models.py create mode 100644 Projekt/app/oauth2.py create mode 100644 Projekt/app/routers/auth.py create mode 100644 Projekt/app/routers/post.py create mode 100644 Projekt/app/routers/user.py create mode 100644 Projekt/app/routers/vote.py create mode 100644 Projekt/app/schemas.py create mode 100644 Projekt/app/utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..597e415 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +venv/ +.env/ diff --git a/Projekt/.env b/Projekt/.env new file mode 100644 index 0000000..a024c73 --- /dev/null +++ b/Projekt/.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/app/__init__.py b/Projekt/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Projekt/app/config.py b/Projekt/app/config.py new file mode 100644 index 0000000..d7f622d --- /dev/null +++ b/Projekt/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/Projekt/app/database.py b/Projekt/app/database.py new file mode 100644 index 0000000..1abafa0 --- /dev/null +++ b/Projekt/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/Projekt/app/main.py b/Projekt/app/main.py new file mode 100644 index 0000000..87db9d0 --- /dev/null +++ b/Projekt/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/Projekt/app/models.py b/Projekt/app/models.py new file mode 100644 index 0000000..97d1e35 --- /dev/null +++ b/Projekt/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/Projekt/app/oauth2.py b/Projekt/app/oauth2.py new file mode 100644 index 0000000..f381f97 --- /dev/null +++ b/Projekt/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/Projekt/app/routers/auth.py b/Projekt/app/routers/auth.py new file mode 100644 index 0000000..30668cf --- /dev/null +++ b/Projekt/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/Projekt/app/routers/post.py b/Projekt/app/routers/post.py new file mode 100644 index 0000000..ce1c11d --- /dev/null +++ b/Projekt/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/Projekt/app/routers/user.py b/Projekt/app/routers/user.py new file mode 100644 index 0000000..1afe06e --- /dev/null +++ b/Projekt/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/Projekt/app/routers/vote.py b/Projekt/app/routers/vote.py new file mode 100644 index 0000000..af17b88 --- /dev/null +++ b/Projekt/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/Projekt/app/schemas.py b/Projekt/app/schemas.py new file mode 100644 index 0000000..439722c --- /dev/null +++ b/Projekt/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/Projekt/app/utils.py b/Projekt/app/utils.py new file mode 100644 index 0000000..3cd58bf --- /dev/null +++ b/Projekt/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