Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,7 @@ dmypy.json
.idea/

# ELK
logstash.db
logstash.db

# DB
db/
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ straightforward as possible.
### Added

- [Initial API & Project Structure](https://github.com/kevencript/embed-backend-devops/commit/5bc6bb790bf645f63f62173fa7534b10e047656a)
Initial project structure (FastAPI & Mongo deployment with Docker-compose and basic requirements)
Initial project structure (FastAPI & Mongo deployment with Docker-compose and basic requirements)
- [ELK Application Logging #1](https://github.com/kevencript/embed-backend-devops/commit/4bfbbb5b8ea410b221f8f8f9d07d6982ebb61a26)
Here we deploy/config the [ELK Stack](https://www.elastic.co/pt/elastic-stack/) in order to store, search, analyze, and visualize data from our Python FastAPI application.
Here we deploy/config the [ELK Stack](https://www.elastic.co/pt/elastic-stack/) in order to store, search, analyze, and visualize data from our Python FastAPI application.
Beside of the ELK stack, we created the logs injection from our app with python-logstash (since that the API already have the logging structure, basically we added the logstash handler in order to inject the data into the Elasticsearch), making the logs available into Kibana.
- [JWT Authentication Method #2](https://github.com/kevencript/embed-backend-devops/CHANGE-ME)
Initial project structure (FastAPI & Mongo deployment with Docker-compose and basic requirements)

### Changed

Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ services:
- .env
ports:
- "27017:27017"
volumes:
- ./db:/data/db:rw
environment:
- "MONGO_INITDB_DATABASE=${MONGO_DB}"
- "MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}"
Expand Down
11 changes: 10 additions & 1 deletion embed/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,27 @@ class Settings(BaseSettings):
instance of Settings

"""

# Env Configs
environment: str = os.getenv("ENVIRONMENT", "local")
testing: str = os.getenv("TESTING", "0")
up: str = os.getenv("UP", "up")
down: str = os.getenv("DOWN", "down")
web_server: str = os.getenv("WEB_SERVER", "web_server")

# MongoDB Configs
db_url: str = os.getenv("MONGO_URL", "")
db_name: str = os.getenv("MONGO_DB", "")
collection: str = os.getenv("MONGO_COLLECTION", "")
test_db_name: str = os.getenv("MONGO_TEST_DB", "")

# JWT Authentication Configs
JWT_PUBLIC_KEY: str = os.getenv("JWT_PUBLIC_KEY", "")
JWT_PRIVATE_KEY: str = os.getenv("JWT_PRIVATE_KEY", "")
REFRESH_TOKEN_EXPIRES_IN: int = os.getenv("REFRESH_TOKEN_EXPIRES_IN", "")
ACCESS_TOKEN_EXPIRES_IN: int = os.getenv("ACCESS_TOKEN_EXPIRES_IN", "")
JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "")
CLIENT_ORIGIN: str = os.getenv("CLIENT_ORIGIN", "")


@lru_cache
def get_settings():
Expand Down
14 changes: 13 additions & 1 deletion embed/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from fastapi import FastAPI

from fastapi.middleware.cors import CORSMiddleware
from embed import config
from embed.routers import router as v1
from embed.services.repository import get_mongo_meta
Expand All @@ -12,6 +12,18 @@

app = FastAPI()

origins = [
global_settings.CLIENT_ORIGIN,
]

app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.include_router(v1, prefix="/api/v1")


Expand Down
70 changes: 70 additions & 0 deletions embed/oauth2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import base64
from typing import List
from fastapi import Depends, HTTPException, status
from fastapi_jwt_auth import AuthJWT
from pydantic import BaseModel
from bson.objectid import ObjectId

from embed.serializers.userSerializers import userEntity
from embed.services.repository import mail_exists, retrieve_document
from embed.utils import get_logger


from embed import config


global_settings = config.get_settings()

class SettingsAuth(BaseModel):
authjwt_algorithm: str = global_settings.JWT_ALGORITHM
authjwt_decode_algorithms: List[str] = [global_settings.JWT_ALGORITHM]
#authjwt_token_location: set = {'cookies', 'headers'}
authjwt_access_cookie_key: str = 'access_token'
authjwt_refresh_cookie_key: str = 'refresh_token'
authjwt_cookie_csrf_protect: bool = False
authjwt_public_key: str = base64.b64decode(
global_settings.JWT_PUBLIC_KEY).decode('utf-8')
authjwt_private_key: str = base64.b64decode(
global_settings.JWT_PRIVATE_KEY).decode('utf-8')


@AuthJWT.load_config
def get_config():
return SettingsAuth()

class NotVerified(Exception):
pass


class UserNotFound(Exception):
pass


async def require_user(Authorize: AuthJWT = Depends()):
try:
Authorize.jwt_required()
user_id = Authorize.get_jwt_subject()

# Validating if user really exists
notSerializedUser = await retrieve_document(ObjectId(str(user_id)), global_settings.collection)
user = userEntity(notSerializedUser)

if not user:
raise UserNotFound('User no longer exist')

except Exception as e:
error = e.__class__.__name__
print(error)
if error == 'MissingTokenError':
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail='You are not logged in')
if error == 'UserNotFound':
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail='User no longer exist')
if error == 'NotVerified':
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail='Please verify your account')
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail='Token is invalid or has expired')
return user_id

9 changes: 9 additions & 0 deletions embed/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from fastapi import APIRouter

from embed.routers.v1.vegs import router as vegs_api
from embed.routers.v1.auth import router as auth_api

router = APIRouter()

router.include_router(vegs_api, prefix="/vegs", tags=["vegetables"])
router.include_router(auth_api, prefix="/auth", tags=["Auth"])
18 changes: 18 additions & 0 deletions embed/routers/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from fastapi import HTTPException, status


class NotFoundHTTPException(HTTPException):
def __init__(self, msg: str):
super().__init__(
status_code=status.HTTP_404_NOT_FOUND,
detail=msg if msg else "Requested resource is not found",
)


class AlreadyExistsHTTPException(HTTPException):
def __init__(self, msg: str):
super().__init__(
status_code=status.HTTP_409_CONFLICT,
detail=msg if msg else "Document with specified id already exists",
)

Empty file added embed/routers/v1/__init__.py
Empty file.
103 changes: 103 additions & 0 deletions embed/routers/v1/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from os import access
from fastapi import APIRouter, Response, status, Depends, HTTPException
from datetime import datetime, timedelta
from bson.objectid import ObjectId

from embed import config

from embed.utils import hash_password, verify_password
from embed.routers.exceptions import NotFoundHTTPException
from embed.services.repository import create_user_db, mail_exists, retrieve_document
from embed.serializers.userSerializers import userResponseEntity, userEntity
from embed.schemas.users import LoginUserSchema,CreateUserSchema, UserResponse
from embed.oauth2 import AuthJWT, require_user

global_settings = config.get_settings()
collection = global_settings.collection

router = APIRouter()

ACCESS_TOKEN_EXPIRES_IN = global_settings.ACCESS_TOKEN_EXPIRES_IN
REFRESH_TOKEN_EXPIRES_IN = global_settings.REFRESH_TOKEN_EXPIRES_IN

@router.post(
'/register',
status_code=status.HTTP_201_CREATED,
response_model=UserResponse
)
async def create_user(payload: CreateUserSchema, Authorize: AuthJWT = Depends()):
"""

:param payload:
:return:
"""
try:
# Check if user already exist
mailAlreadyExist = await mail_exists(payload.email.lower(), collection)
if mailAlreadyExist:
raise HTTPException(status_code=status.HTTP_409_CONFLICT,
detail='Account already exist')

# Compare password and passwordConfirm
if payload.password != payload.passwordConfirm:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail='Passwords do not match')

# Hash the password
payload.password = hash_password(payload.password)
del payload.passwordConfirm
payload.verified = True
payload.email = payload.email.lower()
payload.created_at = datetime.utcnow()

userDict = payload.dict()

# Inserting user into the DB
createdUser = await create_user_db(userDict, collection)
userToReturn = userResponseEntity(createdUser) # Serialized pattern for return

# Create access token
access_token = Authorize.create_access_token(
subject=str(userToReturn["id"]), expires_time=timedelta(minutes=ACCESS_TOKEN_EXPIRES_IN))

return {"status": "success", "access_token":access_token, "user": userToReturn}
except ValueError as exception:
raise NotFoundHTTPException(msg=str(exception))


@router.post('/login', status_code=status.HTTP_200_OK, )
async def login(payload: LoginUserSchema, Authorize: AuthJWT = Depends()):
"""

:param payload:
:param response:
:param Authorize:
:return:
"""
try:

notSerializedUser = await mail_exists(payload.email.lower(), collection)
if not notSerializedUser:
raise NotFoundHTTPException('Incorrect Email or Password')

# Check if the user exist
user = userEntity(notSerializedUser)

# Check if the password is valid
if not verify_password(payload.password, user['password']):
raise NotFoundHTTPException('Incorrect Email or Password')

# Create access token
access_token = Authorize.create_access_token(
subject=str(user["id"]), expires_time=timedelta(minutes=ACCESS_TOKEN_EXPIRES_IN))

# Send both access
return {'status': 'success', 'access_token': access_token}

except ValueError as exception:
raise NotFoundHTTPException(msg=str(exception))

@router.get('/profile', status_code=status.HTTP_200_OK)
async def protected(user_id: str = Depends(require_user)):
userAccount = await retrieve_document(document_id=user_id, collection=collection)
return userResponseEntity(userAccount)
52 changes: 52 additions & 0 deletions embed/routers/v1/vegs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from fastapi import APIRouter
from fastapi.encoders import jsonable_encoder
from starlette.status import HTTP_201_CREATED

from embed import config
from embed.routers.exceptions import NotFoundHTTPException
from embed.schemas.vegs import Document, DocumentResponse, ObjectIdField
from embed.services.repository import create_document, retrieve_document

global_settings = config.get_settings()
collection = global_settings.collection

router = APIRouter()


@router.post(
"",
status_code=HTTP_201_CREATED,
response_description="Document created",
response_model=DocumentResponse,
)
async def add_document(payload: Document):
"""

:param payload:
:return:
"""
try:
payload = jsonable_encoder(payload)
return await create_document(payload, collection)
except ValueError as exception:
raise NotFoundHTTPException(msg=str(exception))


@router.get(
"/{object_id}",
response_description="Document retrieved",
response_model=DocumentResponse,
)
async def get_document(object_id: ObjectIdField):
"""

:param object_id:
:return:
"""
try:
return await retrieve_document(object_id, collection)
except ValueError as exception:
raise NotFoundHTTPException(msg=str(exception))


# TODO: PUT for replace aka set PATCH for update ?
Empty file added embed/schemas/__init__.py
Empty file.
33 changes: 33 additions & 0 deletions embed/schemas/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from datetime import datetime
from pydantic import BaseModel, constr


class UserBaseSchema(BaseModel):
email: str
created_at: datetime | None = None

class Config:
orm_mode = True


class CreateUserSchema(UserBaseSchema):
password: constr(min_length=8)
passwordConfirm: str
verified: bool = False


class LoginUserSchema(BaseModel):
email: str
password: constr(min_length=8)


class UserResponseSchema(UserBaseSchema):
id: str
pass


class UserResponse(BaseModel):
status: str
access_token: str
user: UserResponseSchema

Loading