added auth service

parents
FROM python:3.10-slim-buster
RUN pip3 install --upgrade pip && pip3 install poetry
WORKDIR /app
COPY pyproject.toml poetry.lock /app/
RUN poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi
COPY . .
from fastapi import APIRouter
from routes import user_router as user_route, auth_router as auth_route
auth_router = APIRouter(prefix='/api', tags=['AUTH'])
user_router = APIRouter(prefix='/api', tags=['USER'])
auth_router.include_router(auth_route)
user_router.include_router(user_route)
import os
SECRET_KEY = os.getenv('SECRET_KEY')
ALGORITHM = os.getenv('ALGORITHM')
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES'))
REFRESH_TOKEN_EXPIRE_HOURS = int(os.getenv('REFRESH_TOKEN_EXPIRE_HOURS'))
from uuid import UUID
from repository import UserRepository
from exceptions import common as common_exc, http as http_exc
class BaseController:
repo = None
async def get_list(self, **kwargs) -> list[dict]:
return await self.repo.get_list(**kwargs)
async def get(self, id: UUID) -> dict:
try:
return await self.repo.get(id)
except common_exc.NotFoundException as e:
raise http_exc.HTTPNotFoundException(detail=str(e))
async def create(self, **data) -> dict:
try:
return await self.repo.create(**data)
except common_exc.CreateException as e:
raise http_exc.HTTPBadRequestException(detail=str(e))
async def update(self, id: UUID, **kwargs) -> dict:
try:
return await self.repo.update(id, **kwargs)
except common_exc.NotFoundException as e:
raise http_exc.HTTPNotFoundException(detail=str(e))
except common_exc.UpdateException as e:
raise http_exc.HTTPBadRequestException(detail=str(e))
async def delete(self, id: UUID) -> None:
try:
return await self.repo.delete(id)
except common_exc.NotFoundException as e:
raise http_exc.HTTPNotFoundException(detail=str(e))
except common_exc.DeleteException as e:
raise http_exc.HTTPBadRequestException(detail=str(e))
class UserController(BaseController):
repo = UserRepository()
import os
from fastapi import FastAPI
from tortoise.contrib.fastapi import register_tortoise
TORTOISE_ORM = {
'connections': {'default': os.getenv('DATABASE_URL')},
'apps': {
'models': {
'models': ['code.models', 'aerich.models'],
'default_connection': 'default',
},
},
}
def init_db(app: FastAPI) -> None:
register_tortoise(
app,
db_url=os.getenv('DATABASE_URL'),
modules={"models": ["models"]},
generate_schemas=False,
add_exception_handlers=True,
)
class NotFoundException(Exception):
pass
class CreateException(Exception):
pass
class UpdateException(Exception):
pass
class DeleteException(Exception):
pass
import fastapi
import pydantic
from starlette import status
from fastapi.responses import JSONResponse
from .http import BaseHTTPException
async def query_params_exc_handler(
request: fastapi.Request, exc: pydantic.ValidationError,
) -> JSONResponse:
return JSONResponse(
{'detail': exc.errors()}, status.HTTP_422_UNPROCESSABLE_ENTITY,
)
async def request_exc_handler(
request: fastapi.Request, exc: BaseHTTPException,
) -> JSONResponse:
return JSONResponse(
{'detail': exc.detail}, exc.status_code,
)
async def internal_exc_handler(
request: fastapi.Request, exc: Exception,
) -> JSONResponse:
return JSONResponse(
{'detail': 'Internal Server Error'},
status.HTTP_500_INTERNAL_SERVER_ERROR,
)
from typing import Any, Dict
from fastapi.exceptions import HTTPException
from starlette import status
class BaseHTTPException(HTTPException):
pass
class HTTPBadRequestException(BaseHTTPException):
def __init__(
self, status_code: int = status.HTTP_400_BAD_REQUEST, detail: Any = None,
headers: Dict[str, str] | None = None
) -> None:
super().__init__(status_code, detail, headers)
class HTTPNotFoundException(BaseHTTPException):
def __init__(
self, status_code: int = status.HTTP_404_NOT_FOUND, detail: Any = None,
headers: Dict[str, str] | None = None
) -> None:
super().__init__(status_code, detail, headers)
import fastapi as fa
from api.router import auth_router, user_router
from db import init_db
def setup():
app = fa.FastAPI()
app.include_router(auth_router)
app.include_router(user_router)
return app
app = setup()
init_db(app)
if __name__ == '__main__':
import uvicorn
uvicorn.run('main:app', host='0.0.0.0', port=8000, reload=True)
from uuid import uuid4
from tortoise import fields, models
class User(models.Model):
id = fields.UUIDField(pk=True, default=uuid4)
username = fields.CharField(max_length=100, unique=True)
password = fields.CharField(max_length=255)
def __str__(self):
return self.username
class Meta:
table = 'users'
from uuid import UUID
import tortoise
from passlib.context import CryptContext
from exceptions import common as common_exc
from models import User
class BaseRepository:
model: tortoise.Model
async def get_list(self, **kwargs) -> list[tortoise.Model]:
return await self.model.filter(**kwargs)
async def get(self, id: UUID) -> tortoise.Model:
try:
return await self.model.get(id=id)
except tortoise.exceptions.DoesNotExist as e:
raise common_exc.NotFoundException(str(e))
async def create(self, **kwargs) -> tortoise.Model:
try:
return await self.model.create(**kwargs)
except tortoise.exceptions.IntegrityError as e:
raise common_exc.CreateException(str(e))
async def update(self, id: UUID, **kwargs) -> tortoise.Model:
try:
instance = await self.get(id=id)
await instance.update_from_dict(kwargs).save()
return instance
except tortoise.exceptions.IntegrityError as e:
raise common_exc.UpdateException(str(e))
async def delete(self, id: UUID) -> None:
try:
instance = await self.get(id=id)
await instance.delete()
except tortoise.exceptions.IntegrityError as e:
raise common_exc.DeleteException(str(e))
class UserRepository(BaseRepository):
model = User
@staticmethod
def get_password_hash(password):
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
return pwd_context.hash(password)
async def create(self, **kwargs):
kwargs['password'] = self.get_password_hash(kwargs['password'])
return await super().create(**kwargs)
import fastapi
from fastapi.security import OAuth2PasswordRequestForm
from schemas import CreateUserSchema, CreatedUserResponseSchema, RefreshTokenSchema
from ctrl import UserController
import services
user_router = fastapi.APIRouter(prefix='/users')
auth_router = fastapi.APIRouter(prefix='/auth')
ctrl = UserController()
@user_router.post('')
async def create_users(
body: CreateUserSchema
) -> CreatedUserResponseSchema:
return await ctrl.create(**body.model_dump(exclude_none=True))
@auth_router.post('/login')
async def login(
form_data: OAuth2PasswordRequestForm = fastapi.Depends(),
):
return await services.auth_service.login(form_data)
@user_router.get('/me')
async def read_user_me(
user: dict = fastapi.Depends(services.auth_service.get_current_user)
):
return user
@auth_router.post('/refresh')
async def refresh_token(
data: RefreshTokenSchema,
user: dict = fastapi.Depends(services.auth_service.get_current_user)
):
return services.token_service.refresh_token(data.refresh_token)
from dataclasses import dataclass
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel
class CreateUserSchema(BaseModel):
username: str
password: str
class CreatedUserResponseSchema(BaseModel):
id: UUID
username: str
class UserAllOptionalSchema(BaseModel):
username: None | str = None
password: None | str = None
class RefreshTokenSchema(BaseModel):
refresh_token: str
@dataclass
class TokenData:
sub: int
exp: datetime | None = None
@dataclass
class LoginRequestModel:
username: str
password: str
from datetime import datetime, timedelta
import fastapi as fa
import fastapi.security as fs
from jose import jwt, JWTError
from passlib.context import CryptContext
import schemas
import const
from repository import UserRepository
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
oauth2_scheme = fs.OAuth2PasswordBearer(tokenUrl='/api/auth/login')
class TokenService:
def create_token(self, data: schemas.TokenData, expires_delta: timedelta) -> str:
payload = {
'sub': str(data.sub),
'exp': datetime.utcnow() + expires_delta,
}
return jwt.encode(payload, const.SECRET_KEY, algorithm=const.ALGORITHM)
def create_tokens(self, data: schemas.TokenData):
access = self.create_token(
data, expires_delta=timedelta(minutes=const.ACCESS_TOKEN_EXPIRE_MINUTES)
)
refresh = self.create_token(
data, expires_delta=timedelta(hours=const.REFRESH_TOKEN_EXPIRE_HOURS)
)
return {"access_token": access, "refresh_token": refresh}
def decode_token(self, token: str) -> schemas.TokenData:
credentials_exception = fa.HTTPException(
status_code=fa.status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
decoded_token_payload = jwt.decode(token, const.SECRET_KEY, algorithms=[const.ALGORITHM])
user_id = decoded_token_payload.get('sub')
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
return schemas.TokenData(**decoded_token_payload)
def refresh_token(self, refresh_token: str):
token_data = self.decode_token(refresh_token)
access = self.create_token(token_data, timedelta(minutes=const.ACCESS_TOKEN_EXPIRE_MINUTES))
return {'access_token': access, 'refresh_token': refresh_token}
token_service = TokenService()
class AuthService:
def __init__(self) -> None:
self.user_repo = UserRepository()
async def verify_password(self, plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
async def authenticate_user(self, username: str, password: str):
users = await self.user_repo.get_list(username=username)
if users and await self.verify_password(password, users[0].password):
return users[0]
async def login(self, data: schemas.LoginRequestModel):
user = await self.authenticate_user(username=data.username, password=data.password)
if not user:
raise fa.HTTPException(
status_code=fa.status.HTTP_404_NOT_FOUND,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
return token_service.create_tokens(schemas.TokenData(sub=user.id))
async def get_current_user(self, token: str = fa.Security(oauth2_scheme)):
token_data = token_service.decode_token(token)
return await self.user_repo.get(id=token_data.sub)
auth_service = AuthService()
version: '3'
services:
core:
build:
context: .
dockerfile: Dockerfile
image: auth
container_name: auth
restart: always
ports:
- "8012:8000"
volumes:
- .:/app
env_file:
- .env
command: bash -c "python3 code/main.py"
db:
image: postgres:14-alpine
container_name: auth-pg
env_file:
- .env
ports:
- "5436:5432"
volumes:
- ./data/postgres:/var/lib/postgresql/data/
networks:
default:
name: fa-2-exam-network
from tortoise import BaseDBAsyncClient
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
CREATE TABLE IF NOT EXISTS "users" (
"id" UUID NOT NULL PRIMARY KEY,
"username" VARCHAR(100) NOT NULL UNIQUE,
"password" VARCHAR(255) NOT NULL
);
CREATE TABLE IF NOT EXISTS "aerich" (
"id" SERIAL NOT NULL PRIMARY KEY,
"version" VARCHAR(255) NOT NULL,
"app" VARCHAR(100) NOT NULL,
"content" JSONB NOT NULL
);"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
"""
This diff is collapsed.
[tool.poetry]
name = "auth"
version = "0.1.0"
description = ""
authors = ["beka <beka1990.30.10@gmail.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
tortoise-orm = "^0.20.0"
aerich = "^0.7.2"
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
fastapi = "^0.109.0"
uvicorn = "^0.25.0"
python-jose = "^3.3.0"
python-multipart = "^0.0.6"
asyncpg = "^0.29.0"
[tool.aerich]
tortoise_orm = "code.db.TORTOISE_ORM"
location = "migrations"
src_folder = "./."
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment