Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
582b3fc
feat(parcel): init module structure
Jul 14, 2026
09d0ef2
feat(parcel): add latitude value object
Jul 14, 2026
429d3f3
feat(parcel): add longitude value object
Jul 14, 2026
5f764a3
feat(parcel): add geographic point value object
Jul 14, 2026
b2f630c
feat(parcel): add polygon value object
Jul 14, 2026
9fc6f9a
feat(parcel): add parcel id value object
Jul 14, 2026
261dcf6
feat(parcel): add parcel name value object
Jul 14, 2026
1bb9401
feat(parcel): update init for value objects
Jul 14, 2026
49b0946
feat(parcel): add parcel entity
Jul 14, 2026
257beab
feat(parcel): update domain init
Jul 14, 2026
07c3563
refactor(identity): remove unnessesary value objects
Jul 14, 2026
de5ab4c
refactor(identity): update services to use domain value objects
Jul 14, 2026
1f4150d
refactor(parcel): add annotations from __future__
Jul 14, 2026
81b6830
feat(parcel): add owner id value object
Jul 14, 2026
c9c8213
feat(parcel): add owner id property for parcel entity
Jul 14, 2026
b48afa0
feat(parcel): add application errors
Jul 14, 2026
844bb9f
feat(parcel): add dto commands
Jul 14, 2026
c7cd1a5
feat(parcel): add dto response
Jul 14, 2026
12d6dd5
feat(parcel): add parcel repository port
Jul 14, 2026
4357d49
feat(parcel): add polygon service port
Jul 14, 2026
b0bcebd
feat(parcel): add create parcel use case
Jul 14, 2026
7b40ac4
feat(parcel): add delete parcel use case
Jul 14, 2026
c9efedd
feat(parcel): add get parcel use case
Jul 14, 2026
abdbc2b
feat(parcel): add polygon service implimentation
Jul 14, 2026
061ab1d
feat(parcel): add parcel model
Jul 14, 2026
fa4ded4
feat(parcel): add parcel repository
Jul 14, 2026
78cb401
feat(parcel): update application init file
Jul 14, 2026
19eb1de
feat(parcel): add repository method for getting a list of user parcels
Jul 14, 2026
42bb272
feat(parcel): add list user parcels command
Jul 14, 2026
bff02cd
feat(parcel): add list user parcels use case
Jul 14, 2026
228d1c6
feat(parcel): update delete parcel use case
Jul 14, 2026
d3d03c4
feat(parcel): add parcel schemas
Jul 14, 2026
34461a5
feat(parcel): add parcel controller
Jul 14, 2026
dc3d2aa
feat(parcel): add parcel di
Jul 14, 2026
d3f86ca
feat(parcel): add parcel error mapping
Jul 14, 2026
2cc3de5
feat(interface): register parcel error mapping
Jul 14, 2026
fda61cc
feat(interface): register parcel dependencies
Jul 14, 2026
49a4711
feat(interface): register parcel controller
Jul 14, 2026
a5ad30f
chore: update project dependencies
Jul 14, 2026
04a8090
feat(interface): add guards
Jul 14, 2026
00abdf7
feat: update current user provider
Jul 14, 2026
f5754b6
fix: incorrect type checking usage
Jul 14, 2026
d236f8b
fix: litestar di warnings
Jul 14, 2026
821028f
refactor: configure guards for controllers
Jul 15, 2026
062f221
fix(parcel): incorrect owner id representation
Jul 15, 2026
954bff5
feat(shared): add geojson schema
Jul 15, 2026
aec7e05
feat(parcel): use geojson response format
Jul 15, 2026
eecd95b
feat(platform): add postgis extension for database
Jul 15, 2026
6cf40df
feat(parcel): add migrations
Jul 15, 2026
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
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ dependencies = [
"alembic>=1.18.5",
"asyncpg>=0.31",
"bcrypt>=5",
"geoalchemy2>=0.20",
"granian>=2.7.2",
"litestar>=2.21",
"pydantic>=2.12.5",
"pydantic-settings>=2.12",
"pyjwt>=2.13",
"shapely>=2.1.2",
"sqlalchemy[asyncio]>=2.0.51",
]

Expand Down
4 changes: 3 additions & 1 deletion src/app/interface/http/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from . import asgi, controller, schema, util
from . import asgi, controller, lifespan, middleware, schema, util


__all__ = (
"asgi",
"controller",
"lifespan",
"middleware",
"schema",
"util",
)
32 changes: 4 additions & 28 deletions src/app/interface/http/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,23 @@

from __future__ import annotations

from contextlib import asynccontextmanager
from typing import TYPE_CHECKING

from litestar import Litestar
from litestar.openapi import OpenAPIConfig
from litestar.openapi.plugins import ScalarRenderPlugin

from app import __api_version__
from app.interface.http.controller.system import SystemController
from app.interface.http.lifespan import lifespan
from app.interface.http.middleware import RequestLoggingMiddleware
from app.interface.http.util import (
RequestLoggingMiddleware,
create_exception_handlers,
get_all_application_error_mappings,
get_all_dependencies,
get_all_domain_error_mappings,
)
from app.module.identity.interface.http.controller.auth import AuthController
from app.module.identity.interface.http.controller.user import UserController
from app.platform.config.loaders import load_app_config
from app.platform.database.engine import create_async_engine_from_config, dispose_engine
from app.platform.logging import configure_logging


if TYPE_CHECKING:
from collections.abc import AsyncGenerator


@asynccontextmanager
async def lifespan(app: Litestar) -> AsyncGenerator[None]:
"""Application lifespan: create engine on startup, dispose on shutdown."""
# ── Startup ──────────────────────────────────────────────────────
config = load_app_config()
configure_logging(config.logging)

engine = create_async_engine_from_config(config.database)
app.state.engine = engine

try:
yield
finally:
# ── Shutdown ─────────────────────────────────────────────────
await dispose_engine(engine)
from app.module.parcel.interface.http.controller.parcel import ParcelController


def create_asgi_application() -> Litestar:
Expand All @@ -59,6 +34,7 @@ def create_asgi_application() -> Litestar:
SystemController,
AuthController,
UserController,
ParcelController,
],
dependencies=get_all_dependencies(),
openapi_config=OpenAPIConfig(
Expand Down
32 changes: 32 additions & 0 deletions src/app/interface/http/lifespan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations

from contextlib import asynccontextmanager
from typing import TYPE_CHECKING

from app.platform.config.loaders import load_app_config
from app.platform.database.engine import create_async_engine_from_config, dispose_engine
from app.platform.logging import configure_logging


if TYPE_CHECKING:
from collections.abc import AsyncGenerator

from litestar import Litestar


@asynccontextmanager
async def lifespan(app: Litestar) -> AsyncGenerator[None]:
"""Application lifespan: create engine on startup, dispose on shutdown."""
config = load_app_config()
configure_logging(config.logging)

engine = create_async_engine_from_config(config.database)
app.state.engine = engine

try:
yield
finally:
await dispose_engine(engine)


__all__ = ("lifespan",)
2 changes: 0 additions & 2 deletions src/app/interface/http/util/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from .dependencies import get_all_dependencies
from .error_mappings import get_all_application_error_mappings, get_all_domain_error_mappings
from .exception_handlers import create_exception_handlers
from .middleware import RequestLoggingMiddleware
from .response import make_error_response


__all__ = (
"RequestLoggingMiddleware",
"create_exception_handlers",
"get_all_application_error_mappings",
"get_all_dependencies",
Expand Down
42 changes: 39 additions & 3 deletions src/app/interface/http/util/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,47 @@
"""Dependency assembly for the application."""

from typing import TYPE_CHECKING
from typing import Annotated

from litestar.di import Provide
from litestar.params import HeaderParameter

from app.module.identity.di import identity_dependencies
from app.module.parcel.di import parcel_dependencies
from app.module.shared.application.dto.response import CurrentUser # noqa: TC001
from app.module.shared.application.port import CurrentUserProvider # noqa: TC001
from app.platform.di import platform_dependencies


if TYPE_CHECKING:
from litestar.di import Provide
async def provide_current_user(
current_user_provider: CurrentUserProvider,
authorization: Annotated[str | None, HeaderParameter(name="Authorization")] = None,
) -> CurrentUser:
"""Resolve the current user from the Authorization header.

This dependency is automatically available to all route handlers
that declare a ``current_user`` parameter. It extracts the Bearer
token from the header and delegates to ``CurrentUserProvider`` for
token validation and user resolution.

Parameters
----------
current_user_provider : CurrentUserProvider
Injected port for resolving the current user.
authorization : str | None
The raw Authorization header value, if present.

Returns
-------
CurrentUser
The authenticated user's id and username.

Raises
------
litestar.exceptions.NotAuthorizedException
If the token is missing, invalid, or expired (via exception handler).
"""
token = (authorization or "").removeprefix("Bearer ")
return await current_user_provider.get_current_user(token)


def get_all_dependencies() -> dict[str, Provide]:
Expand All @@ -21,4 +55,6 @@ def get_all_dependencies() -> dict[str, Provide]:
dependencies = {}
dependencies.update(platform_dependencies)
dependencies.update(identity_dependencies)
dependencies.update(parcel_dependencies)
dependencies["current_user"] = Provide(provide_current_user)
return dependencies
8 changes: 3 additions & 5 deletions src/app/interface/http/util/error_mappings.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"""Error mappings assembly for the application."""

from app.module.identity.error_mappings import (
get_identity_application_error_mappings,
get_identity_domain_error_mappings,
)
from app.module.identity.error_mappings import get_identity_application_error_mappings
from app.module.parcel.error_mappings import get_parcel_application_error_mappings
from app.module.shared.interface.http.error_mappings import (
get_shared_application_error_mappings,
get_shared_domain_error_mappings,
Expand All @@ -20,7 +18,6 @@ def get_all_domain_error_mappings() -> dict:
"""
mappings = {}
mappings.update(get_shared_domain_error_mappings())
mappings.update(get_identity_domain_error_mappings())
return mappings


Expand All @@ -35,4 +32,5 @@ def get_all_application_error_mappings() -> dict:
mappings = {}
mappings.update(get_shared_application_error_mappings())
mappings.update(get_identity_application_error_mappings())
mappings.update(get_parcel_application_error_mappings())
return mappings
50 changes: 50 additions & 0 deletions src/app/interface/http/util/exception_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from typing import TYPE_CHECKING, cast

from litestar import Request, Response, status_codes
from litestar.exceptions import HTTPException, NotAuthorizedException, SerializationException

from app.interface.http.util.response import make_error_response
from app.module.shared.application.error import ApplicationError
Expand Down Expand Up @@ -71,6 +72,49 @@ def handler(request: Request, exc: ApplicationError) -> Response:
return cast("ExceptionHandler", handler)


def not_authorized_handler(
request: Request,
exc: NotAuthorizedException,
) -> Response:
"""Handle unauthorized requests."""
logger = get_logger("app.interface.http.exception_handlers")
logger.warning("Unauthorized access attempt: %s %s", request.method, request)
return make_error_response(
status_codes.HTTP_401_UNAUTHORIZED,
exc.detail or "Authorization required",
)


def http_exception_handler(request: Request, exc: HTTPException) -> Response:
"""Handle any HTTP exception."""
logger = get_logger("app.interface.http.exception_handlers")
logger.warning(
"HTTP exception %s: %s while processing %s %s",
exc.status_code,
exc.detail,
request.method,
request.url,
)
return make_error_response(exc.status_code, exc.detail or "HTTP error")


def serialization_exception_handler(
request: Request,
_exc: SerializationException,
) -> Response:
"""Handle malformed JSON."""
logger = get_logger("app.interface.http.exception_handlers")
logger.warning(
"Malformed JSON while processing %s %s",
request.method,
request.url,
)
return make_error_response(
status_codes.HTTP_400_BAD_REQUEST,
"Invalid JSON payload. Please check your request body.",
)


def _internal_server_error_handler(request: Request, exc: Exception) -> Response:
"""Handle unexpected errors with full traceback logging."""
logger = get_logger("app.interface.http.exception_handlers")
Expand Down Expand Up @@ -109,6 +153,12 @@ def create_exception_handlers(
if application_mappings:
handlers[ApplicationError] = _create_application_error_handler(application_mappings)

# HTTP-исключения от Litestar
handlers[NotAuthorizedException] = cast("ExceptionHandler", not_authorized_handler)
handlers[HTTPException] = cast("ExceptionHandler", http_exception_handler)
handlers[SerializationException] = cast("ExceptionHandler", serialization_exception_handler)

# Fallback
handlers[Exception] = _internal_server_error_handler
return handlers

Expand Down
37 changes: 24 additions & 13 deletions src/app/module/identity/application/port/password_hasher.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,52 @@
"""Password hasher port."""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING


if TYPE_CHECKING:
from app.module.identity.domain.value_object import HashedPassword, Password


class PasswordHasher(ABC):
"""Port for password hashing.

Operates on domain :class:`~app.module.identity.domain.value_object.password.Password`
and :class:`~app.module.identity.domain.value_object.hashed_password.HashedPassword`
value objects.

Implementations:
- :class:`app.module.identity.infrastructure.auth.password_hasher.BcryptPasswordHasher`
- :class:`app.module.identity.infrastructure.security.bcrypt_password_hasher.BcryptPasswordHasher`
"""

@abstractmethod
def hash(self, password: str) -> str:
"""Hash a plain-text password.
def hash(self, password: Password) -> HashedPassword:
"""Hash a password.

Parameters
----------
password : str
Plain-text password.
password : Password
Domain password value object.

Returns
-------
str
Hashed password.
HashedPassword
Hashed password value object.
"""
raise NotImplementedError

@abstractmethod
def verify(self, password: str, hashed: str) -> bool:
"""Verify a plain-text password against a hash.
def verify(self, password: Password, hashed: HashedPassword) -> bool:
"""Verify a password against a hash.

Parameters
----------
password : str
Plain-text password to verify.
hashed : str
Stored hash to verify against.
password : Password
Domain password value object to verify.
hashed : HashedPassword
Domain hashed password value object to verify against.

Returns
-------
Expand Down
Loading
Loading