Skip to content

Commit

Permalink
initial commit with auth
Browse files Browse the repository at this point in the history
  • Loading branch information
agughalamDavis committed Jun 16, 2024
1 parent 504ac9d commit fe21f26
Show file tree
Hide file tree
Showing 41 changed files with 1,224 additions and 1 deletion.
14 changes: 14 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
PYTHON_ENV=dev
DB_TYPE=mysql
DB_NAME=dbname
DB_USER=user
DB_PASSWORD=password
DB_HOST=127.0.0.1
DB_PORT=3306
MYSQL_DRIVER=pymysql
DB_URL="mysql+pymysql://user:[email protected]:3306/dbname"
SECRET_KEY = ""
ALGORITHM = HS256
ACCESS_TOKEN_EXPIRE_MINUTES = 10
JWT_REFRESH_EXPIRY=5
APP_URL=
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
# hng_boilerplate_python_web
# FASTAPI
FastAPI boilerplate

## Setup

1. Create a virtual environment.
```sh
python3 -m venv .venv
```
2. Activate virtual environment.
```sh
source /path/to/venv/bin/activate`
```
3. Install project dependencies `pip install -r requirements.txt`
4. Create a .env file by copying the .env.sample file
`cp .env.sample .env`

5. Start server.
```sh
python main.py
```

Empty file added __init__.py
Empty file.
1 change: 1 addition & 0 deletions alembic/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Generic single-database configuration.
89 changes: 89 additions & 0 deletions alembic/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@

import os
from alembic import context
from decouple import config
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from api.db.database import Base
from api.v1.models.auth import User, BlackListToken

#from db.database import DATABASE_URL
DATABASE_URL=config('DB_URL')


# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url", DATABASE_URL)
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""

alembic_config = config.get_section(config.config_ini_section)
alembic_config['sqlalchemy.url'] = DATABASE_URL

connectable = engine_from_config(
alembic_config,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)

with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)

with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
26 changes: 26 additions & 0 deletions alembic/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}


def upgrade() -> None:
${upgrades if upgrades else "pass"}


def downgrade() -> None:
${downgrades if downgrades else "pass"}
Empty file added api/core/__init__.py
Empty file.
Empty file added api/core/base/__init__.py
Empty file.
22 changes: 22 additions & 0 deletions api/core/base/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from abc import ABC, abstractmethod

class Service(ABC):
@abstractmethod
def create(self):
pass

@abstractmethod
def fetch(self):
pass

@abstractmethod
def fetch_all(self):
pass

@abstractmethod
def update(self):
pass

@abstractmethod
def delete(self):
pass
Empty file.
36 changes: 36 additions & 0 deletions api/core/dependencies/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Union
from fastapi.security import OAuth2PasswordBearer
from fastapi import Depends, Cookie, HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy.sql import and_
from jose import JWTError, jwt

from api.v1.schemas import auth as user_schema
from api.v1.services.auth import User
from api.v1.models.auth import User as UserModel

from api.db.database import get_db
from api.core import responses

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")

def is_authenticated(
access_token: Union[str, user_schema.Token] = Depends(oauth2_scheme),
db: Session = Depends(get_db),
) -> Union[user_schema.User, JWTError]:

if not access_token:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=responses.INVALID_CREDENTIALS)

userService = User()

access_token_info = userService.verify_access_token(access_token, db)

if type(access_token_info) is JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=responses.INVALID_CREDENTIALS)

user = userService.fetch(id=access_token_info.id,db=db)

return user


7 changes: 7 additions & 0 deletions api/core/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
EMAIL_IN_USE = "This email is already in use."
NOT_FOUND = "Not found!"
ID_OR_UNIQUE_ID_REQUIRED = "ID or Unique ID required!"
INVALID_CREDENTIALS = "Invalid Credentials!"
COULD_NOT_VALIDATE_CRED = "Could not validate credentials."
SUCCESS = "SUCCESS"
EXPIRED="Token expired."
Empty file added api/db/__init__.py
Empty file.
50 changes: 50 additions & 0 deletions api/db/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from decouple import config


def get_db_engine():

DB_TYPE = config("DB_TYPE")
DB_NAME = config("DB_NAME")
DB_USER = config("DB_USER")
DB_PASSWORD = config("DB_PASSWORD")
DB_HOST = config("DB_HOST")
DB_PORT = config("DB_PORT")
MYSQL_DRIVER = config("MYSQL_DRIVER")
DATABASE_URL = ""

if DB_TYPE == "mysql":
DATABASE_URL = f'mysql+{MYSQL_DRIVER}://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
elif DB_TYPE == "postgresql":
DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
else:
DATABASE_URL = "sqlite:///./database.db"

if DB_TYPE == "sqlite":
db_engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
else:
db_engine = create_engine(DATABASE_URL, pool_size=32, max_overflow=64)

return db_engine

db_engine = get_db_engine()


SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=db_engine)

Base = declarative_base()


def create_database():
return Base.metadata.create_all(bind=db_engine)


def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
19 changes: 19 additions & 0 deletions api/db/mongo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from pymongo import MongoClient
from api.utils import settings
from pymongo.mongo_client import MongoClient
from motor.motor_asyncio import AsyncIOMotorClient


def create_nosql_db():

client = MongoClient(settings.MONGO_URI)

try:
client.admin.command("ping")
print("MongoDB Connection Established...")
except Exception as e:
print(e)


client = MongoClient(settings.MONGO_URI)
db = client.get_database(settings.MONGO_DB_NAME)
Empty file added api/utils/__init__.py
Empty file.
9 changes: 9 additions & 0 deletions api/utils/dict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

def clone_object(obj: dict, unwanted_fields=[]):
new_obj = {}
for k in list(obj.keys()):
if k in unwanted_fields:
continue

new_obj[k] = obj[k]
return new_obj
7 changes: 7 additions & 0 deletions api/utils/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from fastapi import HTTPException

class CustomException(HTTPException):

@staticmethod
def PermissionError():
raise HTTPException(status_code=403, detail="User has no permission to perform this action")
7 changes: 7 additions & 0 deletions api/utils/mailer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import resend
from decouple import config

resend.api_key = config('RESEND_API_KEY')

class Mailer(resend.Emails):
pass
47 changes: 47 additions & 0 deletions api/utils/paginator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from sqlalchemy.orm import Session

def total_row_count(model, organization_id, db: Session):
return db.query(model).filter(model.organization_id == organization_id).filter(
model.is_deleted == False).count()

def off_set(page: int, size: int):
return (page-1)*size


def size_validator(size:int):
if size < 0 or size > 100:
return "page size must be between 0 and 100"
return size


def page_urls(page: int, size: int, count: int, endpoint: str):
paging = {}
if (size + off_set(page, size)) >= count:
paging['next'] = None
if page > 1:
paging['previous'] = f"{endpoint}?page={page-1}&size={size}"
else:
paging['previous'] = None
else:
paging['next'] = f"{endpoint}?page={page+1}&size={size}"
if page > 1:
paging['previous'] = f"{endpoint}?page={page-1}&size={size}"
else:
paging['previous'] = None

return paging


def build_paginated_response(
page: int, size: int, total: int, pointers: dict, items
) -> dict:
response = {
"page": page,
"size": size,
"total": total,
"previous_page": pointers["previous"],
"next_page": pointers["next"],
"items": items,
}

return response
4 changes: 4 additions & 0 deletions api/utils/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from decouple import config

MONGO_URI = config("MONGO_URI")
MONGO_DB_NAME = config("MONGO_DB_NAME")
Empty file added api/utils/sql.py
Empty file.
Loading

0 comments on commit fe21f26

Please sign in to comment.