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
36 changes: 36 additions & 0 deletions src/app/application/dto/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Optional

from src.app.application.common.dto.base import AppBaseDTO


@dataclass
class TokenPairDTO(AppBaseDTO):
"""DTO for token pair (access and refresh tokens)."""

access_token: str
refresh_token: str

def to_dict(self) -> dict:
return {
"access": self.access_token,
"refresh": self.refresh_token,
}


@dataclass
class DecodedTokenDTO(AppBaseDTO):
"""DTO for decoded token data."""

uuid: str
sid: str
token_type: str
exp: Optional[datetime] = None

def to_dict(self) -> dict:
return {
"uuid": self.uuid,
"sid": self.sid,
"token_type": self.token_type,
}
16 changes: 16 additions & 0 deletions src/app/application/dto/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,19 @@ class UserShortDTO(AppBaseDTO):
email: Optional[str]
phone: Optional[str]
is_active: bool


@dataclass
class CreateUserByEmailDTO(AppBaseDTO):
"""DTO for creating user by email and password."""

email: str
password: str


@dataclass
class CreateUserByPhoneDTO(AppBaseDTO):
"""DTO for creating user by phone and verification code."""

phone: str
verification_code: str
32 changes: 24 additions & 8 deletions src/app/application/services/auth_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from src.app.application.common.services.base import AbstractBaseApplicationService
from src.app.application.container import container as app_services_container, ApplicationServicesContainer
from src.app.application.dto.auth import DecodedTokenDTO, TokenPairDTO
from src.app.application.dto.user import UserShortDTO
from src.app.domain.auth.container import container as domain_auth_svc_container, DomainAuthServiceContainer
from src.app.domain.auth.value_objects import TokenPair, DecodedToken
from src.app.domain.common.exceptions import ValidationError
from src.app.domain.common.utils.common import mask_string
from src.app.domain.users.container import container as domain_users_svc_container, DomainUsersServiceContainer
Expand Down Expand Up @@ -45,22 +45,38 @@ async def get_auth_user_by_email_password(cls, email: str, password: str) -> Any
return user

@classmethod
def verify_access_token(cls, token: str) -> DecodedToken:
def verify_access_token(cls, token: str) -> DecodedTokenDTO:
"""Verify an access token and return decoded data."""
return cls.dom_auth_svc_container.jwt_service.verify_access_token(token)
decoded_vo = cls.dom_auth_svc_container.jwt_service.verify_access_token(token)
return DecodedTokenDTO(
uuid=decoded_vo.uuid,
sid=decoded_vo.sid,
token_type=decoded_vo.token_type.value,
exp=decoded_vo.exp,
)

@classmethod
def verify_refresh_token(cls, token: str) -> DecodedToken:
def verify_refresh_token(cls, token: str) -> DecodedTokenDTO:
"""Verify a refresh token and return decoded data."""
return cls.dom_auth_svc_container.jwt_service.verify_refresh_token(token)
decoded_vo = cls.dom_auth_svc_container.jwt_service.verify_refresh_token(token)
return DecodedTokenDTO(
uuid=decoded_vo.uuid,
sid=decoded_vo.sid,
token_type=decoded_vo.token_type.value,
exp=decoded_vo.exp,
)

@classmethod
def create_tokens_for_user(cls, uuid: str) -> TokenPair:
def create_tokens_for_user(cls, uuid: str) -> TokenPairDTO:
"""Create access and refresh tokens for a user."""
return cls.dom_auth_svc_container.jwt_service.create_token_pair(uuid)
token_pair_vo = cls.dom_auth_svc_container.jwt_service.create_token_pair(uuid)
return TokenPairDTO(
access_token=token_pair_vo.access_token,
refresh_token=token_pair_vo.refresh_token,
)

@classmethod
async def refresh_tokens(cls, refresh_token: str) -> tuple[UserShortDTO, TokenPair]:
async def refresh_tokens(cls, refresh_token: str) -> tuple[UserShortDTO, TokenPairDTO]:
"""Verify refresh token, get user, and create new token pair."""
decoded = cls.verify_refresh_token(refresh_token)
user = await cls.app_svc_container.users_service.get_first(
Expand Down
41 changes: 30 additions & 11 deletions src/app/application/services/users_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

from src.app.application.common.services.base import BaseApplicationService
from src.app.application.container import container as app_services_container, ApplicationServicesContainer
from src.app.application.dto.user import UserShortDTO
from src.app.application.dto.user import CreateUserByEmailDTO, CreateUserByPhoneDTO, UserShortDTO
from src.app.domain.auth.container import container as domain_auth_svc_container, DomainAuthServiceContainer
from src.app.domain.common.exceptions import AlreadyExistsError
from src.app.domain.common.utils.common import mask_string
from src.app.domain.users.container import container as domain_users_svc_container, DomainUsersServiceContainer
from src.app.domain.users.value_objects.users_vob import EmailPasswordPair, PhoneNumberCodePair
from src.app.domain.users.value_objects.users_vo import EmailPasswordPair, PhoneNumberCodePair
from src.app.infrastructure.repositories.container import container as repo_container


Expand All @@ -28,21 +28,28 @@ class AppUserService(BaseApplicationService):
@classmethod
async def create_user_by_email(cls, email: str, password: str) -> Optional[UserShortDTO]:
_, validated_email = validate_email(email)
validated_data = EmailPasswordPair(email=validated_email, password=password)
password_hashed = domain_auth_svc_container.auth_service.get_password_hashed(password=password)
input_dto = CreateUserByEmailDTO(email=validated_email, password=password)

email_password_vo = EmailPasswordPair(email=input_dto.email, password=input_dto.password)

# Use domain service for password hashing
password_hashed = domain_auth_svc_container.auth_service.get_password_hashed(password=input_dto.password)

# Check business rule: email uniqueness
is_email_exists = await cls.app_svc_container.users_service.is_exists(
filter_data={"email": validated_data.email}
filter_data={"email": email_password_vo.email}
)
if is_email_exists or not email:
raise AlreadyExistsError(
message="Already exists",
details=[
{"key": "email", "value": mask_string(validated_data.email, keep_start=1, keep_end=4)},
{"key": "email", "value": mask_string(email_password_vo.email, keep_start=1, keep_end=4)},
],
)

# Prepare persistence data
data = {
"email": validated_data.email,
"email": email_password_vo.email,
"password_hashed": password_hashed,
}
user_dto = await cls.create(data, is_return_require=True, out_dataclass=UserShortDTO)
Expand All @@ -51,16 +58,28 @@ async def create_user_by_email(cls, email: str, password: str) -> Optional[UserS

@classmethod
async def create_user_by_phone(cls, phone: str, verification_code: str) -> Optional[UserShortDTO]:
validated_data = PhoneNumberCodePair(phone=phone, verification_code=verification_code)
# Create application DTO
input_dto = CreateUserByPhoneDTO(phone=phone, verification_code=verification_code)

# Convert to domain value object for validation
phone_code_vo = PhoneNumberCodePair(phone=input_dto.phone, verification_code=input_dto.verification_code)

# Check business rule: phone uniqueness
is_phone_exists = await cls.app_svc_container.users_service.is_exists(
filter_data={"phone": validated_data.phone}
filter_data={"phone": phone_code_vo.phone}
)
if is_phone_exists:
raise AlreadyExistsError(
message="Already exists",
details=[
{"key": "phone", "value": mask_string(validated_data.phone, keep_start=2, keep_end=2)},
{"key": "phone", "value": mask_string(phone_code_vo.phone, keep_start=2, keep_end=2)},
],
)
user_dto = await cls.create(validated_data.to_dict(), is_return_require=True, out_dataclass=UserShortDTO)

# Prepare persistence data
data = {
"phone": phone_code_vo.phone,
"verification_code": phone_code_vo.verification_code,
}
user_dto = await cls.create(data, is_return_require=True, out_dataclass=UserShortDTO)
return user_dto
2 changes: 1 addition & 1 deletion src/app/domain/auth/value_objects/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from src.app.domain.auth.value_objects.jwt_vob import (
from src.app.domain.auth.value_objects.jwt_vo import (
TokenType,
TokenPayload,
TokenPair,
Expand Down
16 changes: 8 additions & 8 deletions src/app/interfaces/api/v1/endpoints/auth/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
@router.post(path="/sign-up/", response_model=SignupResp, name="sign-up")
async def sign_up(data: Annotated[SignUpReq, Body()]) -> dict:

user = await app_svc_container.users_service.create_user_by_email(
user_dto = await app_svc_container.users_service.create_user_by_email(
email=data.email,
password=data.password,
)
assert user is not None
return asdict(user)
assert user_dto is not None
return asdict(user_dto)


@router.post(path="/tokens/", response_model=TokenResp, name="tokens pair")
Expand All @@ -30,13 +30,13 @@ async def tokens(
) -> dict:
"""Get new access, refresh tokens [Based on email, password]"""

user = await app_svc_container.auth_service.get_auth_user_by_email_password(
user_dto = await app_svc_container.auth_service.get_auth_user_by_email_password(
email=data.email, password=data.password
)

token_pair = app_svc_container.auth_service.create_tokens_for_user(uuid=str(user.uuid))
token_pair = app_svc_container.auth_service.create_tokens_for_user(uuid=str(user_dto.uuid))
tokens_data = {
"user_data": {"uuid": str(user.uuid)},
"user_data": {"uuid": str(user_dto.uuid)},
"access": token_pair.access_token,
"refresh": token_pair.refresh_token,
}
Expand All @@ -48,9 +48,9 @@ async def tokens(
async def tokens_refreshed(auth_api_key: str = Depends(validate_api_key)) -> dict:
"""Get new access, refresh tokens [Granted by refresh token in header]"""

user, token_pair = await app_svc_container.auth_service.refresh_tokens(auth_api_key)
user_dto, token_pair = await app_svc_container.auth_service.refresh_tokens(auth_api_key)
tokens_data = {
"user_data": {"uuid": str(getattr(user, "uuid", ""))},
"user_data": {"uuid": str(getattr(user_dto, "uuid", ""))},
"access": token_pair.access_token,
"refresh": token_pair.refresh_token,
}
Expand Down