-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add: Postgres DB Add: SQLAlchemy Add: Pydantic and model schemas Update: Separate users and event routers Update: save secrets in .env file Add: requirement.txt Add: Authentication
- Loading branch information
0 parents
commit 680fcd7
Showing
14 changed files
with
518 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.vscode | ||
index.html | ||
__pycache__/ | ||
.env | ||
|
||
|
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from pydantic_settings import BaseSettings, SettingsConfigDict | ||
|
||
|
||
class Settings(BaseSettings): | ||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") | ||
|
||
|
||
settings = Settings(_env_file=".env", _env_file_encoding="utf-8").model_dump() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
from sqlalchemy import create_engine | ||
from sqlalchemy.ext.declarative import declarative_base | ||
from sqlalchemy.orm import sessionmaker | ||
from .config import settings | ||
|
||
# URL syntax '<type_of_database>://<username>:<password>@<ip-address/hostname>/<databe_name>' | ||
SQLALCHEMY_DATABASE_URL = f"postgresql://{settings.database_username}:{settings.database_password}@{settings.database_port}/{settings.database_name}" | ||
|
||
engine = create_engine(SQLALCHEMY_DATABASE_URL, echo=True) | ||
|
||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) | ||
|
||
Base = declarative_base() | ||
|
||
|
||
def get_db(): | ||
db = SessionLocal() | ||
try: | ||
yield db | ||
finally: | ||
db.close() | ||
|
||
|
||
# import psycopg2 | ||
# from psycopg2.extras import RealDictCursor | ||
# import time | ||
# while True: | ||
# try: | ||
# conn = psycopg2.connect( | ||
# host="localhost", | ||
# database="fastapi", | ||
# user="postgres", | ||
# password="cheesecake", | ||
# cursor_factory=RealDictCursor, | ||
# ) | ||
# cursor = conn.cursor() | ||
# print("DB connection successfully established.") | ||
# break | ||
# except Exception as error: | ||
# print(f"Connecting to DB failed \nError: {error}") | ||
# time.sleep(2) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from fastapi import FastAPI | ||
from . import models | ||
from .database import engine | ||
from .routers import event, user, auth | ||
|
||
|
||
models.Base.metadata.create_all(bind=engine) | ||
|
||
app = FastAPI() | ||
|
||
|
||
@app.get("/") | ||
async def health(): | ||
return {"status": "I Am Aokayyy!!"} | ||
|
||
|
||
# @app.get("/sqlalchemy") | ||
# def test_orm_connection(db: Session = Depends(get_db)): | ||
# events = db.query(models.Events).all() | ||
# return {"data": events} | ||
|
||
|
||
app.include_router(event.router) | ||
app.include_router(user.router) | ||
app.include_router(auth.router) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# Every model is a table in the database | ||
from .database import Base | ||
from sqlalchemy import ( | ||
Column, | ||
Integer, | ||
String, | ||
Float, | ||
Boolean, | ||
TIMESTAMP, | ||
text, | ||
ForeignKey, | ||
) | ||
import uuid | ||
from sqlalchemy.dialects.postgresql import UUID | ||
from sqlalchemy.orm import relationship | ||
|
||
|
||
class Events(Base): | ||
__tablename__ = "events" | ||
|
||
id = Column( | ||
UUID(as_uuid=True), | ||
primary_key=True, | ||
unique=True, | ||
nullable=False, | ||
server_default=text("gen_random_uuid()"), | ||
) | ||
title = Column(String, nullable=False) | ||
description = Column(String, nullable=True) | ||
duration = Column(Float, nullable=False) | ||
attended = Column(Boolean, nullable=False, server_default="TRUE") | ||
created_at = Column( | ||
TIMESTAMP(timezone=True), nullable=False, server_default=text("now()") | ||
) | ||
owner_id = Column( | ||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False | ||
) | ||
|
||
owner = relationship("Users") | ||
|
||
|
||
class Users(Base): | ||
__tablename__ = "users" | ||
|
||
id = Column( | ||
UUID(as_uuid=True), | ||
primary_key=True, | ||
unique=True, | ||
nullable=False, | ||
server_default=text("gen_random_uuid()"), | ||
) | ||
email = Column(String, primary_key=True, unique=True) | ||
password = Column(String, nullable=False) | ||
created_at = Column( | ||
TIMESTAMP(timezone=True), nullable=False, server_default=text("now()") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
from jose import JWTError, jwt | ||
from datetime import datetime, timedelta | ||
from . import schemas, database, models | ||
from fastapi import Depends, HTTPException, status | ||
from fastapi.security import OAuth2PasswordBearer | ||
from sqlalchemy.orm import Session | ||
from .config import settings | ||
|
||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") | ||
|
||
# SECRET KEY | ||
# ALGO FOR HASHING | ||
# Expiration time | ||
|
||
# generated using openssl rand -hex 32 | ||
SECRET_KEY = settings.secret_key | ||
ALGORITHM = settings.algorithm | ||
ACCESS_TOKEN_EXPIRE_MINUTES = settings.expiration_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) | ||
user_id = payload.get("user_id") | ||
if not user_id: | ||
raise credentials_exception | ||
token_data = schemas.TokenData(id=user_id) | ||
except JWTError: | ||
raise credentials_exception | ||
return token_data | ||
|
||
|
||
def get_current_user( | ||
token: str = Depends(oauth2_scheme), db: Session = Depends(database.get_db) | ||
): | ||
credentials_exception = HTTPException( | ||
status_code=status.HTTP_404_NOT_FOUND, | ||
detail="Could not validate credentials", | ||
headers={"WWW-authenticate": "Bearer"}, | ||
) | ||
token = verify_access_token(token, credentials_exception) | ||
user = db.query(models.Users).filter(models.Users.id == token.id).first() | ||
return user.id |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from fastapi import APIRouter, Depends, status, HTTPException, Response | ||
from sqlalchemy.orm import Session | ||
from ..database import get_db | ||
from .. import schemas, models, utils, oauth2 | ||
|
||
|
||
router = APIRouter(tags=["Authentication"]) | ||
|
||
|
||
@router.post("/login", response_model=schemas.Token) | ||
def login(user_credentials: schemas.UserLogin, db: Session = Depends(get_db)): | ||
user = ( | ||
db.query(models.Users) | ||
.filter(models.Users.email == user_credentials.email) | ||
.first() | ||
) | ||
if not user: | ||
raise HTTPException( | ||
status_code=status.HTTP_403_FORBIDDEN, detail="Invalid credentials." | ||
) | ||
if not utils.verify_user(user_credentials.password, user.password): | ||
raise HTTPException( | ||
status_code=status.HTTP_403_FORBIDDEN, detail="Invalid credentials." | ||
) | ||
|
||
# create and return token | ||
encoded_jwt = oauth2.create_access_token(data={"user_id": str(user.id)}) | ||
|
||
return {"access_token": encoded_jwt, "token_type": "bearer"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
from fastapi import HTTPException, status, Depends, APIRouter | ||
from sqlalchemy.orm import Session | ||
from typing import List, Optional | ||
from uuid import UUID | ||
from .. import models, schemas, oauth2 | ||
from ..database import get_db | ||
|
||
router = APIRouter(prefix="/events", tags=["Events"]) | ||
|
||
|
||
@router.get("/", response_model=List[schemas.EventResponse]) | ||
async def get_events( | ||
db: Session = Depends(get_db), | ||
current_user: UUID = Depends(oauth2.get_current_user), | ||
limit: int = 10, | ||
skip: int = 0, | ||
search: Optional[str] = "", | ||
): | ||
events = ( | ||
db.query(models.Events) | ||
.filter(models.Events.title.contains(search)) | ||
.limit(limit) | ||
.offset(skip) | ||
.all() | ||
) | ||
return events | ||
|
||
|
||
@router.get("/{id}", response_model=schemas.EventResponse) | ||
async def get_event_by_id(id: UUID, db: Session = Depends(get_db)): | ||
# cursor.execute("""SELECT * FROM events WHERE id = %s""", str(id)) | ||
# event = cursor.fetchone() | ||
event = db.query(models.Events).filter(models.Events.id == id).first() | ||
if not event: | ||
raise HTTPException( | ||
status_code=status.HTTP_404_NOT_FOUND, detail=f"Event {id} not found." | ||
) | ||
|
||
return event | ||
|
||
|
||
@router.post( | ||
"/", status_code=status.HTTP_201_CREATED, response_model=schemas.EventResponse | ||
) | ||
async def create_event( | ||
event: schemas.EventCreate, | ||
db: Session = Depends(get_db), | ||
current_user: UUID = Depends(oauth2.get_current_user), | ||
): | ||
# cursor.execute( | ||
# """INSERT INTO events (title, description, duration, attended) VALUES (%s,%s,%s,%s) RETURNING *""", | ||
# (event.title, event.description, event.duration, event.attended), | ||
# ) | ||
new_event = models.Events(owner_id=current_user, **event.model_dump()) | ||
db.add(new_event) | ||
db.commit() | ||
db.refresh(new_event) | ||
|
||
return new_event | ||
|
||
|
||
@router.patch("/{id}", response_model=schemas.EventResponse) | ||
async def update_event( | ||
id: UUID, | ||
event: schemas.EventCreate, | ||
db: Session = Depends(get_db), | ||
current_user: UUID = Depends(oauth2.get_current_user), | ||
): | ||
# cursor.execute( | ||
# """UPDATE posts SET title = %s, description = %s, duration = %s, attended = %s RETURNING *""", | ||
# event.title, | ||
# event.description, | ||
# event.duration, | ||
# event.attended, | ||
# ) | ||
# updated_event_query = cursor.fetchone() | ||
# conn.commit() | ||
updated_event_query = db.query(models.Events).filter(models.Events.id == id) | ||
event = updated_event_query.first() | ||
if event and event.owner_id != current_user: | ||
raise HTTPException( | ||
status_code=status.HTTP_403_FORBIDDEN, detail="Unauthorised request." | ||
) | ||
if not event: | ||
raise HTTPException( | ||
status_code=status.HTTP_404_NOT_FOUND, detail=f"Event {id} not found." | ||
) | ||
|
||
updated_event_query.update(event.model_dump(), synchronize_session=False) | ||
db.commit() | ||
return event | ||
|
||
|
||
@router.delete("/{id}") | ||
async def delete_item_by_id( | ||
id: UUID, | ||
db: Session = Depends(get_db), | ||
current_user: UUID = Depends(oauth2.get_current_user), | ||
): | ||
# cursor.execute("""DELETE FROM events WHERE id = %s RETURNING *""", str(id)) | ||
# deleted_post = cursor.fetchone() | ||
# conn.commit() | ||
deleted_event_query = db.query(models.Events).filter(models.Events.id == id) | ||
event = deleted_event_query.first() | ||
if event and event.owner_id != current_user: | ||
raise HTTPException( | ||
status_code=status.HTTP_403_FORBIDDEN, detail="Unauthorised request." | ||
) | ||
if not event: | ||
raise HTTPException( | ||
status_code=status.HTTP_404_NOT_FOUND, detail=f"Event {id} not found." | ||
) | ||
deleted_event_query.delete(synchronize_session=False) | ||
|
||
db.commit() | ||
|
||
return {"status": "Item deleted successfully"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from fastapi import HTTPException, status, Depends, APIRouter | ||
from sqlalchemy.orm import Session | ||
from typing import List | ||
from uuid import UUID | ||
from .. import models, schemas, utils, oauth2 | ||
from ..database import get_db | ||
|
||
router = APIRouter(prefix="/users", tags=["Users"]) | ||
|
||
|
||
@router.post( | ||
"/", | ||
status_code=status.HTTP_201_CREATED, | ||
response_model=schemas.UserCreateResponse, | ||
) | ||
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): | ||
user.password = utils.hashify(user.password) | ||
existing_user = ( | ||
db.query(models.Users).filter(models.Users.email == user.email).first() | ||
) | ||
if existing_user: | ||
raise HTTPException( | ||
status_code=status.HTTP_409_CONFLICT, detail="Account already exists." | ||
) | ||
new_user = models.Users(**user.model_dump()) | ||
db.add(new_user) | ||
db.commit() | ||
db.refresh(new_user) | ||
|
||
return new_user | ||
|
||
|
||
@router.get( | ||
"/{id}", | ||
response_model=schemas.UserCreateResponse, | ||
) | ||
def get_user_by_id(id: UUID, db: Session = Depends(get_db)): | ||
user = db.query(models.Users).filter(models.Users.id == id).first() | ||
if not user: | ||
raise HTTPException( | ||
status_code=status.HTTP_404_NOT_FOUND, details=f"User {id} not found." | ||
) | ||
return user | ||
|
||
|
||
@router.get( | ||
"/", | ||
response_model=List[schemas.UserCreateResponse], | ||
) | ||
def read_users(db: Session = Depends(get_db)): | ||
users = db.query(models.Users).all() | ||
|
||
return users |
Oops, something went wrong.