Skip to content

Commit

Permalink
Feature/db (#7)
Browse files Browse the repository at this point in the history
* Updated requirements.txt.

* Delete unnecessary directory.

* Created User model.

* Cleanup unnecessary code.

* Fixed /register. Refactoring.

* Cleanup.

* Refactor /login.

* Use bcrypt for password hashing.

* Cleanup.

* Updated README.md.
  • Loading branch information
Airiinnn authored Jun 9, 2024
1 parent 04605a1 commit 7db2fec
Show file tree
Hide file tree
Showing 15 changed files with 207 additions and 186 deletions.
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
# apm-service
Back-end service for APM.

## Brief Description
## Description
This is a brief template that provides the basic setup to create the backend portion using FastAPI. The documentation of the API is done using SwaggerUI, and Pydantic is used for data validation and setting management.

(To be updated)

## Dependencies
- Python 3.12.3

## 🏃 Running Locally
Preparation:
1. Setup a virtual environment (conda, venv, etc).
2. Run `pip install -r requirements.txt` to install necessary packages.
3. Run service with `uvicorn app.main:app --reload`.
4. Run tests with `to be configured`.
5. Check code style with `flake8`.
6. Check for static typing with `mypy .`.
3. Create an _env file_ named `.env` in the project root path and add the following environment variables:
```
ACCESS_TOKEN_SECRET_KEY
ACCESS_TOKEN_VALIDITY_MINUTES
DB_NAME
DB_HOST
DB_PORT
DB_USERNAME
DB_PASSWORD
```

Running:
1. Run service with `uvicorn app.main:app --reload`.
2. Run tests with `to be configured`.
3. Check code style with `flake8`.
4. Check for static typing with `mypy .`.
75 changes: 75 additions & 0 deletions app/api/auth_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from fastapi import Depends, HTTPException, APIRouter
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session

from app.db.database import get_db
from app.schemas.auth import LoginUserSchema, RegisterUserSchema
from app.services.auth_service import login_user, register_user

router = APIRouter()


@router.post("/login")
def login(returning_user: LoginUserSchema, db: Session = Depends(get_db)) -> JSONResponse:
try:
# Todo: Refresh token.
access_token = login_user(returning_user, db)

response_payload = {
"access_token": access_token,
"token_type": "bearer"
}

return JSONResponse(response_payload)

except HTTPException:
raise

except Exception:
# Todo: Handle exceptions.
raise HTTPException(status_code=500, detail="Internal Server Error")


@router.post("/register")
def register(new_user: RegisterUserSchema, db: Session = Depends(get_db)) -> JSONResponse:
try:
register_user(new_user, db)

# Todo: Return token.
return JSONResponse({})

except HTTPException:
raise

except Exception:
# Todo: Handle exceptions.
raise HTTPException(status_code=500, detail="Internal Server Error")


# async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_db)) -> User:
# credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
# detail="Could not validate credentials",
# headers={"WWW-Authenticate": "Bearer"},)
# try:
# payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
# username: str = payload.get("sub")
# if username is None:
# raise credentials_exception
# token_data = TokenData(username=username)
# except InvalidTokenError:
# raise credentials_exception
# user = db.query(User).filter(User.username == token_data.username).first() # type: ignore
# if user is None:
# raise credentials_exception
# return user
#
#
# async def get_current_active_user(current_user: Annotated[User, Depends(get_current_user)],) -> User:
# if current_user.disabled:
# raise HTTPException(status_code=400, detail="Inactive user.")
# return current_user
#
#
# @router.get("/users/me/")
# async def read_users_me(current_user: Annotated[User, Depends(get_current_active_user)]) -> User:
# return current_user
91 changes: 0 additions & 91 deletions app/api/authentication.py

This file was deleted.

33 changes: 0 additions & 33 deletions app/api/models/auth_models.py

This file was deleted.

10 changes: 7 additions & 3 deletions app/db/database.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import os
from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from dotenv import load_dotenv

load_dotenv()

url = os.getenv("DATABASE_POSTGRE_URL")
db_name = os.getenv("DB_NAME")
db_host = os.getenv("DB_HOST")
db_port = os.getenv("DB_PORT")
db_username = os.getenv("DB_USERNAME")
db_password = os.getenv("DB_PASSWORD")

engine = create_engine(url) # type: ignore
engine = create_engine(f"postgresql+psycopg2://{db_username}:{db_password}@{db_host}:{db_port}/{db_name}")
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

Expand Down
11 changes: 0 additions & 11 deletions app/db/models/models.py

This file was deleted.

13 changes: 13 additions & 0 deletions app/db/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import uuid
from sqlalchemy.orm import Mapped, mapped_column # type: ignore[attr-defined]
from sqlalchemy.dialects.postgresql import UUID
from app.db.database import Base


class User(Base): # type: ignore[misc]
__tablename__ = "user"

id: Mapped[str] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4())
username: Mapped[str] = mapped_column(unique=True, nullable=True)
email: Mapped[str] = mapped_column(unique=True, nullable=False)
password: Mapped[str] = mapped_column(nullable=False)
File renamed without changes.
15 changes: 15 additions & 0 deletions app/db/repositories/user_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from sqlalchemy.orm import Session

from app.db.models.user import User


class UserRepository:
def __init__(self, db: Session):
self.db = db

def get_user_by_email(self, email: str) -> User | None:
return self.db.query(User).filter(User.email == email).first()

def insert_user(self, user: User) -> None:
self.db.add(user)
self.db.commit()
8 changes: 6 additions & 2 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from dotenv import load_dotenv
from fastapi import FastAPI

from app.api.v1.router import api_router
from app.api import authentication
from app.api import auth_router

load_dotenv()

app = FastAPI()
app.include_router(api_router)
app.include_router(authentication.router)
app.include_router(auth_router.router)
File renamed without changes.
12 changes: 12 additions & 0 deletions app/schemas/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from pydantic import BaseModel, EmailStr


class LoginUserSchema(BaseModel): # type: ignore[misc]
email: EmailStr
password: str


class RegisterUserSchema(BaseModel): # type: ignore[misc]
email: EmailStr
username: str
password: str
60 changes: 60 additions & 0 deletions app/services/auth_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import bcrypt
import jwt
import os
import time
from fastapi import HTTPException
from sqlalchemy.orm import Session

from app.db.models.user import User
from app.db.repositories.user_repository import UserRepository
from app.schemas.auth import LoginUserSchema, RegisterUserSchema


def verify_password(plain_password: str, hashed_password: str) -> bool:
return bcrypt.checkpw(plain_password.encode(), hashed_password.encode())


def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()


def generate_jwt(user: User) -> str:
time_now = int(time.time())
claims = {
"sub": str(user.id),
"iss": "apm-service",
"iat": time_now,
"exp": time_now + (int(os.getenv("ACCESS_TOKEN_VALIDITY_MINUTES", 15)) * 60)
}

# Todo: Consolidate env vars. Ensure not None.
token = jwt.encode(payload=claims, key=os.getenv("ACCESS_TOKEN_SECRET_KEY"), algorithm="HS256")

return token


def login_user(returning_user: LoginUserSchema, db: Session) -> str:
user_repository = UserRepository(db)

existing_user = user_repository.get_user_by_email(returning_user.email)
if not existing_user or not verify_password(returning_user.password, existing_user.password):
raise HTTPException(status_code=401, detail="Incorrect email or password.")

access_token = generate_jwt(existing_user)

return access_token


def register_user(new_user: RegisterUserSchema, db: Session) -> None:
user_repository = UserRepository(db)

existing_user = user_repository.get_user_by_email(new_user.email)
if existing_user:
raise HTTPException(status_code=409, detail="Email already in use.")

user = User(
username=new_user.username,
email=new_user.email,
password=hash_password(new_user.password)
)
user_repository.insert_user(user)
Loading

0 comments on commit 7db2fec

Please sign in to comment.