Skip to content

Commit

Permalink
Add roles for users.
Browse files Browse the repository at this point in the history
  • Loading branch information
Roman505050 committed Nov 7, 2024
1 parent 52820da commit 1137866
Show file tree
Hide file tree
Showing 22 changed files with 444 additions and 54 deletions.
8 changes: 4 additions & 4 deletions alembic.ini
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ sqlalchemy.url = driver://user:pass@localhost/dbname
# detail and examples

# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
hooks = black
black.type = console_scripts
black.entrypoint = black
black.options = -l 79 REVISION_SCRIPT_FILENAME

# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
Expand Down
34 changes: 33 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ flask-wtf = "^1.2.2"
asyncpg = "^0.30.0"
python-dotenv = "^1.0.1"
gunicorn = "^23.0.0"
loguru = "^0.7.2"


[tool.poetry.group.dev.dependencies]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ SQLAlchemy==2.0.36
typing_extensions==4.12.2
Werkzeug==3.1.2
WTForms==3.2.1
loguru~=0.7.2
31 changes: 31 additions & 0 deletions src/core/application/user/factories/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from uuid import uuid4

from core.application.user.ports.services.cryptography import (
ICryptographyService,
)
from core.domain.user.entities.role import RoleEntity
from core.domain.user.entities.user import UserEntity


class UserFactory:
def __init__(self, cryptography_service: ICryptographyService):
self._cryptography_service = cryptography_service

def create_user(
self,
username: str,
email: str,
password: str,
roles: list[RoleEntity],
) -> UserEntity:
salt = self._cryptography_service.generate_salt()
password_hash = self._cryptography_service.hash_password(
password=password, salt=salt
)
return UserEntity(
user_id=uuid4(),
username=username,
email=email,
password_hash=password_hash,
roles=roles,
)
35 changes: 20 additions & 15 deletions src/core/application/user/use_cases/register.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
from email_validator import validate_email, EmailNotValidError
from pydantic import ValidationError
from loguru import logger

from core.application.user.dto.user import RegisterUserDTO, UserDTO
from core.application.user.ports.services.cryptography import (
ICryptographyService,
)
from core.domain.user.repositories.exceptions import UserAlreadyExistsException
from core.application.user.factories.user import UserFactory
from core.domain.user.entities.role import RoleEntity
from core.domain.user.exceptions import UserAlreadyExistsException
from core.domain.user.repositories.user import IUserRepository
from core.domain.user.entities.user import UserEntity
from core.domain.user.repositories.role import IRoleRepository
from core.shared.exceptions import NotFoundException


class RegisterUserUseCase:
def __init__(
self,
user_repository: IUserRepository,
cryptography_service: ICryptographyService,
role_repository: IRoleRepository,
user_factory: UserFactory,
):
self._user_repository = user_repository
self._cryptography_service = cryptography_service
self._user_factory = user_factory
self._role_repository = role_repository

async def execute(self, register_data: RegisterUserDTO) -> UserDTO:
try:
Expand All @@ -44,18 +46,21 @@ async def execute(self, register_data: RegisterUserDTO) -> UserDTO:
except NotFoundException:
pass

salt = self._cryptography_service.generate_salt()
password_hash = self._cryptography_service.hash_password(
password=register_data.password.get_secret_value(), salt=salt
)
try:
member_role = await self._role_repository.get_by_name("member")
except NotFoundException:
logger.warning(f"Role 'member' not found, creating it")
member_role = RoleEntity.create("member")

user = UserEntity.create(
user = self._user_factory.create_user(
username=register_data.username,
email=register_data.email,
password_hash=password_hash,
password=register_data.password.get_secret_value(),
roles=[member_role],
)

await self._user_repository.save(user)
user = await self._user_repository.save(user)

try:
user_dto = UserDTO(
user_id=user.user_id, username=user.username, email=user.email
Expand All @@ -64,5 +69,5 @@ async def execute(self, register_data: RegisterUserDTO) -> UserDTO:
return user_dto
except ValidationError as e:
# Because if an error occurs, it is not a user error
print(e) # TODO: replace with logger
logger.error(e)
raise Exception("Invalid data") from e
19 changes: 19 additions & 0 deletions src/core/domain/user/entities/role.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from dataclasses import dataclass
from uuid import UUID, uuid4


@dataclass
class RoleEntity:
role_id: UUID
role_name: str

def __post_init__(self):
self._validate()

def _validate(self):
if not 3 <= len(self.role_name) <= 64:
raise ValueError("Role name must be between 3 and 64 characters")

@staticmethod
def create(role_name: str) -> "RoleEntity":
return RoleEntity(role_id=uuid4(), role_name=role_name)
14 changes: 4 additions & 10 deletions src/core/domain/user/entities/user.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
from dataclasses import dataclass
from uuid import UUID, uuid4

from core.domain.user.entities.role import RoleEntity


@dataclass
class UserEntity:
user_id: UUID
username: str
email: str
password_hash: str
roles: list[RoleEntity]

def __post_init__(self):
self._validate()

def _validate(self):
if len(self.email) > 100:
raise ValueError("Email is too long")
if 3 > len(self.username) > 64:
if not 3 <= len(self.username) <= 64:
raise ValueError("Username must be between 3 and 64 characters")

@staticmethod
def create(username: str, email: str, password_hash: str) -> "UserEntity":
return UserEntity(
user_id=uuid4(),
username=username,
email=email,
password_hash=password_hash,
)
11 changes: 11 additions & 0 deletions src/core/domain/user/exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from core.domain.user.exceptions.user_not_found import UserNotFoundException
from core.domain.user.exceptions.user_already_exsist import (
UserAlreadyExistsException,
)
from core.domain.user.exceptions.role_not_found import RoleNotFoundException

__all__ = (
"UserNotFoundException",
"UserAlreadyExistsException",
"RoleNotFoundException",
)
5 changes: 5 additions & 0 deletions src/core/domain/user/exceptions/role_not_found.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from core.shared.exceptions import NotFoundException


class RoleNotFoundException(NotFoundException):
pass
5 changes: 5 additions & 0 deletions src/core/domain/user/exceptions/user_already_exsist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from core.shared.exceptions import AlreadyExistsException


class UserAlreadyExistsException(AlreadyExistsException):
pass
5 changes: 5 additions & 0 deletions src/core/domain/user/exceptions/user_not_found.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from core.shared.exceptions import NotFoundException


class UserNotFoundException(NotFoundException):
pass
9 changes: 0 additions & 9 deletions src/core/domain/user/repositories/exceptions.py

This file was deleted.

21 changes: 21 additions & 0 deletions src/core/domain/user/repositories/role.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from abc import ABC, abstractmethod
from uuid import UUID

from core.domain.user.entities.role import RoleEntity


class IRoleRepository(ABC):
@abstractmethod
async def commit(self) -> None: ...

@abstractmethod
async def save(self, role: RoleEntity) -> RoleEntity: ...

@abstractmethod
async def delete(self, role_id: UUID) -> None: ...

@abstractmethod
async def get_by_id(self, role_id: UUID) -> RoleEntity: ...

@abstractmethod
async def get_by_name(self, name: str) -> RoleEntity: ...
4 changes: 4 additions & 0 deletions src/core/infrastructure/database/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from core.infrastructure.database.models.base import Base
from core.infrastructure.database.models.user import User
from core.infrastructure.database.models.role import Role
from core.infrastructure.database.models.user_roles import UserRoles

__all__ = (
"Base",
"User",
"Role",
"UserRoles",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Role table.
Revision ID: cb78897a4f9b
Revises: 5fad516883d3
Create Date: 2024-11-07 21:40:43.650063
"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "cb78897a4f9b"
down_revision: Union[str, None] = "5fad516883d3"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"roles",
sa.Column("role_id", sa.UUID(), nullable=False),
sa.Column("role_name", sa.String(length=64), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("role_id"),
)
op.create_index(
op.f("ix_roles_role_name"), "roles", ["role_name"], unique=True
)
op.create_table(
"user_roles",
sa.Column("user_id", sa.UUID(), nullable=False),
sa.Column("role_id", sa.UUID(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(
["role_id"],
["roles.role_id"],
onupdate="CASCADE",
ondelete="RESTRICT",
),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
onupdate="CASCADE",
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("user_id", "role_id"),
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("user_roles")
op.drop_index(op.f("ix_roles_role_name"), table_name="roles")
op.drop_table("roles")
# ### end Alembic commands ###
Loading

0 comments on commit 1137866

Please sign in to comment.