Skip to content

Commit

Permalink
Add: REST API
Browse files Browse the repository at this point in the history
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
nxbringr committed Sep 24, 2023
0 parents commit 680fcd7
Show file tree
Hide file tree
Showing 14 changed files with 518 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.vscode
index.html
__pycache__/
.env


Empty file added app/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions app/config.py
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()
41 changes: 41 additions & 0 deletions app/database.py
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)
25 changes: 25 additions & 0 deletions app/main.py
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)
56 changes: 56 additions & 0 deletions app/models.py
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()")
)
51 changes: 51 additions & 0 deletions app/oauth2.py
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
29 changes: 29 additions & 0 deletions app/routers/auth.py
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"}
117 changes: 117 additions & 0 deletions app/routers/event.py
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"}
53 changes: 53 additions & 0 deletions app/routers/user.py
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
Loading

0 comments on commit 680fcd7

Please sign in to comment.