summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRasmus Luha <rasmus.luha@gmail.com>2022-02-06 13:10:32 +0200
committerRasmus Luha <rasmus.luha@gmail.com>2022-02-06 13:10:32 +0200
commit5e19a0569288de21365c61b0db78639880732dd0 (patch)
treee1d3add7376b53157fe8830cbd81b02c1a3a3586
inital commit
-rw-r--r--.gitignore3
-rw-r--r--Projekt/.env8
-rw-r--r--Projekt/app/__init__.py0
-rw-r--r--Projekt/app/config.py16
-rw-r--r--Projekt/app/database.py38
-rw-r--r--Projekt/app/main.py17
-rw-r--r--Projekt/app/models.py31
-rw-r--r--Projekt/app/oauth2.py49
-rw-r--r--Projekt/app/routers/auth.py22
-rw-r--r--Projekt/app/routers/post.py102
-rw-r--r--Projekt/app/routers/user.py27
-rw-r--r--Projekt/app/routers/vote.py45
-rw-r--r--Projekt/app/schemas.py61
-rw-r--r--Projekt/app/utils.py9
14 files changed, 428 insertions, 0 deletions
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
--- /dev/null
+++ b/Projekt/app/__init__.py
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)