auth next part
This commit is contained in:
@@ -1,17 +1,22 @@
|
||||
from fastapi import FastAPI
|
||||
from sqlalchemy.ext.asyncio import (AsyncEngine, AsyncSession,
|
||||
async_sessionmaker)
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
|
||||
|
||||
from api.application.abstractions.uow import UnitOfWork
|
||||
from api.application.protocols.date_time import DateTimeProvider
|
||||
from api.application.protocols.password_hasher import PasswordHasher
|
||||
from api.application.usecase.auth.create_user import CreateUser
|
||||
from api.domain.user.repository import UserRepository
|
||||
from api.infrastructure.dependencies.adapters import (create_engine,
|
||||
create_session_maker,
|
||||
new_session,
|
||||
new_unit_of_work)
|
||||
from api.infrastructure.dependencies.adapters import (
|
||||
create_engine,
|
||||
create_session_maker,
|
||||
new_session,
|
||||
new_unit_of_work,
|
||||
)
|
||||
from api.infrastructure.dependencies.configs import app_settings
|
||||
from api.infrastructure.dependencies.protocols import get_password_hasher
|
||||
from api.infrastructure.dependencies.protocols import (
|
||||
get_date_time_provider,
|
||||
get_password_hasher,
|
||||
)
|
||||
from api.infrastructure.dependencies.repositories import get_user_repository
|
||||
from api.infrastructure.dependencies.usecases import provide_create_user
|
||||
from api.infrastructure.settings import Settings
|
||||
@@ -26,6 +31,7 @@ def init_dependencies(app: FastAPI) -> None:
|
||||
|
||||
app.dependency_overrides[UnitOfWork] = new_unit_of_work
|
||||
|
||||
app.dependency_overrides[DateTimeProvider] = get_date_time_provider
|
||||
app.dependency_overrides[PasswordHasher] = get_password_hasher
|
||||
|
||||
app.dependency_overrides[UserRepository] = get_user_repository
|
||||
|
@@ -1,7 +1,6 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from api.presentation.routers import (auth_router, healthcheck_router,
|
||||
user_router)
|
||||
from api.presentation.routers import auth_router, healthcheck_router, user_router
|
||||
|
||||
|
||||
def init_routers(app: FastAPI) -> None:
|
||||
|
@@ -1,3 +1,3 @@
|
||||
from .auth_request import UserCreateRequest
|
||||
from .auth_request import LoginRequest, UserCreateRequest
|
||||
|
||||
__all__ = ("UserCreateRequest",)
|
||||
__all__ = ("UserCreateRequest", "LoginRequest")
|
||||
|
@@ -6,3 +6,9 @@ class UserCreateRequest:
|
||||
name: str
|
||||
email: str
|
||||
password: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LoginRequest:
|
||||
email: str
|
||||
password: str
|
||||
|
9
api/application/contracts/auth/auth_response.py
Normal file
9
api/application/contracts/auth/auth_response.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AuthenticationResponse:
|
||||
id: UUID
|
||||
name: str
|
||||
email: str
|
7
api/application/protocols/date_time.py
Normal file
7
api/application/protocols/date_time.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from datetime import datetime
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class DateTimeProvider(Protocol):
|
||||
def get_current_time(self) -> datetime:
|
||||
raise NotImplementedError
|
11
api/application/protocols/jwt.py
Normal file
11
api/application/protocols/jwt.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from typing import Protocol
|
||||
|
||||
from api.domain.user.model import UserId
|
||||
|
||||
|
||||
class JwtTokenProcessor(Protocol):
|
||||
def generate_token(self, user_id: UserId) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def validate_token(self, token: str) -> UserId | None:
|
||||
raise NotImplementedError
|
30
api/application/usecase/auth/auth_user.py
Normal file
30
api/application/usecase/auth/auth_user.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from api.application.contracts.auth.auth_request import LoginRequest
|
||||
from api.application.contracts.auth.auth_response import AuthenticationResponse
|
||||
from api.application.protocols.password_hasher import PasswordHasher
|
||||
from api.domain.user.error import UserInvalidCredentialsError
|
||||
from api.domain.user.repository import UserRepository
|
||||
|
||||
|
||||
class LoginUser:
|
||||
def __init__(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
password_hasher: PasswordHasher,
|
||||
) -> None:
|
||||
self.user_repository = user_repository
|
||||
self.hasher = password_hasher
|
||||
|
||||
async def __call__(self, request: LoginRequest) -> AuthenticationResponse:
|
||||
user = await self.user_repository.get_user(filter={"email": request.email})
|
||||
error = UserInvalidCredentialsError("Email or password is incorrect")
|
||||
if user is None:
|
||||
raise error
|
||||
|
||||
if not self.hasher.verify_password(request.password, user.hashed_password):
|
||||
raise error
|
||||
|
||||
return AuthenticationResponse(
|
||||
id=user.id.value,
|
||||
name=user.name.value,
|
||||
email=user.email.value,
|
||||
)
|
0
api/infrastructure/auth/__init__.py
Normal file
0
api/infrastructure/auth/__init__.py
Normal file
37
api/infrastructure/auth/jwt_processor.py
Normal file
37
api/infrastructure/auth/jwt_processor.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from datetime import timedelta
|
||||
from uuid import UUID
|
||||
|
||||
from jose import JWTError
|
||||
from jose.jwt import decode, encode
|
||||
|
||||
from api.application.protocols.date_time import DateTimeProvider
|
||||
from api.application.protocols.jwt import JwtTokenProcessor
|
||||
from api.domain.user.model import UserId
|
||||
from api.infrastructure.auth.jwt_settings import JwtSettings
|
||||
|
||||
|
||||
class JoseJwtTokenProcessor(JwtTokenProcessor):
|
||||
def __init__(self, jwt_options: JwtSettings, date_time_provider: DateTimeProvider) -> None:
|
||||
self.jwt_options = jwt_options
|
||||
self.date_time_provider = date_time_provider
|
||||
|
||||
def generate_token(self, user_id: UserId) -> str:
|
||||
issued_at = self.date_time_provider.get_current_time()
|
||||
expiration_time = issued_at + timedelta(hours=self.jwt_options.expires_in)
|
||||
|
||||
claims = {
|
||||
"iat": issued_at,
|
||||
"exp": expiration_time,
|
||||
"sub": str(user_id.value),
|
||||
}
|
||||
|
||||
return encode(claims, self.jwt_options.secret, self.jwt_options.algorithm)
|
||||
|
||||
def validate_token(self, token: str) -> UserId | None:
|
||||
try:
|
||||
payload = decode(token, self.jwt_options.secret, [self.jwt_options.algorithm])
|
||||
|
||||
return UserId(UUID(payload["sub"]))
|
||||
|
||||
except (JWTError, ValueError, KeyError):
|
||||
return None
|
8
api/infrastructure/auth/jwt_settings.py
Normal file
8
api/infrastructure/auth/jwt_settings.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JwtSettings:
|
||||
secret: str
|
||||
expires_in: int = field(default=2)
|
||||
algorithm: str = field(default="HS256")
|
26
api/infrastructure/date_time.py
Normal file
26
api/infrastructure/date_time.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from enum import Enum
|
||||
|
||||
from api.application.protocols.date_time import DateTimeProvider
|
||||
|
||||
|
||||
class Timezone(Enum):
|
||||
UTC = timezone.utc
|
||||
GMT = timezone(timedelta(hours=0))
|
||||
CET = timezone(timedelta(hours=1))
|
||||
EET = timezone(timedelta(hours=2))
|
||||
MSK = timezone(timedelta(hours=3))
|
||||
IST = timezone(timedelta(hours=5, minutes=30))
|
||||
WIB = timezone(timedelta(hours=7))
|
||||
CST = timezone(timedelta(hours=8))
|
||||
JST = timezone(timedelta(hours=9))
|
||||
AEST = timezone(timedelta(hours=10))
|
||||
NZST = timezone(timedelta(hours=12))
|
||||
|
||||
|
||||
class SystemDateTimeProvider(DateTimeProvider):
|
||||
def __init__(self, tz: Timezone) -> None:
|
||||
self.tz = tz
|
||||
|
||||
def get_current_time(self) -> datetime:
|
||||
return datetime.now(tz=self.tz.value)
|
@@ -3,6 +3,7 @@ from functools import lru_cache
|
||||
|
||||
import yaml # type: ignore
|
||||
|
||||
from api.infrastructure.auth.jwt_settings import JwtSettings
|
||||
from api.infrastructure.persistence.db_setings import DBSettings
|
||||
from api.infrastructure.settings import Settings
|
||||
|
||||
@@ -27,4 +28,9 @@ def app_settings() -> Settings:
|
||||
pg_port=int(config_data["db"]["port"]),
|
||||
pg_db=config_data["db"]["database"],
|
||||
),
|
||||
jwt=JwtSettings(
|
||||
secret=config_data["jwt"]["secret_key"],
|
||||
expires_in=int(config_data["jwt"]["expires_in"]),
|
||||
algorithm=config_data["jwt"]["algorithm"],
|
||||
),
|
||||
)
|
||||
|
@@ -1,6 +1,12 @@
|
||||
from api.application.protocols.date_time import DateTimeProvider
|
||||
from api.application.protocols.password_hasher import PasswordHasher
|
||||
from api.infrastructure.date_time import SystemDateTimeProvider, Timezone
|
||||
from api.infrastructure.security.password_hasher import Pbkdf2PasswordHasher
|
||||
|
||||
|
||||
def get_password_hasher() -> PasswordHasher:
|
||||
return Pbkdf2PasswordHasher()
|
||||
|
||||
|
||||
def get_date_time_provider() -> DateTimeProvider:
|
||||
return SystemDateTimeProvider(Timezone.UTC)
|
||||
|
@@ -14,6 +14,4 @@ def provide_create_user(
|
||||
uow: Annotated[UnitOfWork, Depends(Stub(UnitOfWork))],
|
||||
password_hasher: Annotated[PasswordHasher, Depends(Stub(PasswordHasher))],
|
||||
) -> CreateUser:
|
||||
return CreateUser(
|
||||
uow=uow, user_repository=user_repository, password_hasher=password_hasher
|
||||
)
|
||||
return CreateUser(uow=uow, user_repository=user_repository, password_hasher=password_hasher)
|
||||
|
@@ -2,7 +2,6 @@ from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.domain.user import User, UserRepository
|
||||
from api.infrastructure.persistence.models.user import UserModel
|
||||
|
||||
|
||||
class SqlAlchemyUserRepository(UserRepository):
|
||||
@@ -10,12 +9,6 @@ class SqlAlchemyUserRepository(UserRepository):
|
||||
self.session = session
|
||||
|
||||
async def create_user(self, user: User) -> None:
|
||||
# stmt = insert(UserModel).values(
|
||||
# id=user.id.value,
|
||||
# name=user.name.value,
|
||||
# email=user.email.value,
|
||||
# hashed_password=user.hashed_password,
|
||||
# )
|
||||
stmt = text(
|
||||
"""INSERT INTO users (id, name, email, hashed_password)
|
||||
VALUES(:id, :name, :email, :hashed_password)
|
||||
|
@@ -1,8 +1,10 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from api.infrastructure.auth.jwt_settings import JwtSettings
|
||||
from api.infrastructure.persistence.db_setings import DBSettings
|
||||
|
||||
|
||||
@dataclass()
|
||||
class Settings:
|
||||
db: DBSettings
|
||||
jwt: JwtSettings
|
||||
|
@@ -1,9 +1,14 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Response
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
from api.application.contracts.auth import UserCreateRequest
|
||||
from api.application.contracts.auth import LoginRequest, UserCreateRequest
|
||||
from api.application.contracts.auth.auth_response import AuthenticationResponse
|
||||
from api.application.protocols.jwt import JwtTokenProcessor
|
||||
from api.application.usecase.auth.auth_user import LoginUser
|
||||
from api.application.usecase.auth.create_user import CreateUser
|
||||
from api.domain.user.model import UserId
|
||||
from api.infrastructure.dependencies.stub import Stub
|
||||
|
||||
auth_router = APIRouter(prefix="/auth", tags=["Auth"])
|
||||
@@ -15,3 +20,22 @@ async def create_user(
|
||||
usecase: Annotated[CreateUser, Depends(Stub(CreateUser))],
|
||||
) -> None:
|
||||
return await usecase.execute(request)
|
||||
|
||||
|
||||
@auth_router.post("/login", response_model=AuthenticationResponse)
|
||||
async def login(
|
||||
response: Response,
|
||||
login_request: Annotated[OAuth2PasswordRequestForm, Depends()],
|
||||
login_interactor: Annotated[LoginUser, Depends(Stub(LoginUser))],
|
||||
token_processor: Annotated[JwtTokenProcessor, Depends(Stub(JwtTokenProcessor))],
|
||||
) -> AuthenticationResponse:
|
||||
user = await login_interactor(
|
||||
LoginRequest(
|
||||
email=login_request.username,
|
||||
password=login_request.password,
|
||||
)
|
||||
)
|
||||
token = token_processor.generate_token(UserId(user.id))
|
||||
response.set_cookie(key="access_token", value=f"Bearer {token}", httponly=True)
|
||||
|
||||
return user
|
||||
|
@@ -1,9 +1,6 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter
|
||||
|
||||
from api.application.contracts.user import UserResponse
|
||||
from api.infrastructure.dependencies.stub import Stub
|
||||
|
||||
user_router = APIRouter(prefix="/users", tags=["Users"])
|
||||
|
||||
|
Reference in New Issue
Block a user