service_packages 4.0.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
auth/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .api import auth_router
2
+ from .plugin import AuthPlugin
3
+
4
+ __all__ = ["auth_router", "AuthPlugin"]
auth/api/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ from litestar import Router
2
+
3
+ from .account_controller import AccountController
4
+ from .permission_controller import PermissionController
5
+ from .role_controller import RoleController
6
+ from .user_controller import UserController
7
+
8
+ auth_router = Router(
9
+ path="/api/auth",
10
+ route_handlers=[
11
+ UserController,
12
+ RoleController,
13
+ PermissionController,
14
+ AccountController,
15
+ ],
16
+ )
17
+
18
+ __all__ = ["auth_router"]
@@ -0,0 +1,120 @@
1
+ from typing import Any
2
+ from uuid import UUID
3
+
4
+ from litestar import Controller, Request, get, post
5
+ from litestar.datastructures import State
6
+ from litestar.di import Provide
7
+ from litestar.exceptions import HTTPException
8
+ from litestar.status_codes import HTTP_400_BAD_REQUEST
9
+ import msgspec
10
+
11
+ from auth.services import (
12
+ AuthService,
13
+ AccountDTO,
14
+ LoginRequestDTO,
15
+ LogoutRequestDTO,
16
+ SignUpRequestDTO,
17
+ UserNotEnabledError,
18
+ provide_auth_service,
19
+ provide_user_service,
20
+ InvalidEmailError,
21
+ InvalidPasswordError,
22
+ )
23
+ from auth.services.auth_service import AccountUserDTO
24
+
25
+
26
+ class SignUpRequestScheme(msgspec.Struct):
27
+ email: str
28
+ password: str
29
+
30
+
31
+ class SignUpResponseScheme(msgspec.Struct):
32
+ message: str
33
+
34
+
35
+ class LoginResponseScheme(AccountDTO): ...
36
+
37
+
38
+ class ActivateAccountResponseScheme(AccountDTO): ...
39
+
40
+
41
+ class LoginRequestScheme(msgspec.Struct):
42
+ email: str
43
+ password: str
44
+
45
+
46
+ class ActivateAccountRequestScheme(msgspec.Struct):
47
+ code: str
48
+
49
+
50
+ class AccountMeResponseScheme(msgspec.Struct):
51
+ session_id: UUID
52
+ user: AccountUserDTO
53
+
54
+
55
+ class AccountController(Controller):
56
+ path = "/account"
57
+
58
+ dependencies = {
59
+ "user_service": Provide(provide_user_service),
60
+ "auth_service": Provide(provide_auth_service),
61
+ }
62
+
63
+ @get("/me")
64
+ async def account(self, request: Request[AccountDTO, Any, State]) -> AccountMeResponseScheme:
65
+ return AccountMeResponseScheme(session_id=request.user.session_id, user=request.user.user)
66
+
67
+ @post("/login", exclude_from_auth=True)
68
+ async def login(self, request: Request, data: LoginRequestScheme, auth_service: AuthService) -> LoginResponseScheme:
69
+ device = request.headers.get("User-Agent")
70
+ try:
71
+ login_user = await auth_service.login(
72
+ LoginRequestDTO(
73
+ email=data.email,
74
+ password=data.password,
75
+ device=device,
76
+ )
77
+ )
78
+ except UserNotEnabledError:
79
+ raise HTTPException(f"User {data.email} is not enabled", status_code=HTTP_400_BAD_REQUEST)
80
+ except InvalidEmailError:
81
+ raise HTTPException(f"User with email {data.email} not found", status_code=HTTP_400_BAD_REQUEST)
82
+ except InvalidPasswordError:
83
+ raise HTTPException("Invalid password", status_code=HTTP_400_BAD_REQUEST)
84
+
85
+ return LoginResponseScheme(
86
+ token=login_user.token,
87
+ session_id=login_user.session_id,
88
+ user=login_user.user,
89
+ )
90
+
91
+ @post("/signup", exclude_from_auth=True)
92
+ async def sign_up(self, data: SignUpRequestScheme, auth_service: AuthService) -> SignUpResponseScheme:
93
+ await auth_service.signup(SignUpRequestDTO(email=data.email, password=data.password))
94
+ return SignUpResponseScheme(message="success")
95
+
96
+ @post("/logout")
97
+ async def logout(self, auth_service: AuthService, request: Request[AccountDTO, Any, State]) -> None:
98
+ await auth_service.logout(
99
+ LogoutRequestDTO(
100
+ user_id=request.user.user.id,
101
+ session_id=request.user.session_id,
102
+ device=request.headers.get("User-Agent"),
103
+ )
104
+ )
105
+
106
+ @post("/activate", exclude_from_auth=True)
107
+ async def activate(
108
+ self,
109
+ data: ActivateAccountRequestScheme,
110
+ auth_service: AuthService,
111
+ request: Request,
112
+ ) -> ActivateAccountResponseScheme:
113
+ device = request.headers.get("User-Agent")
114
+ activated_account = await auth_service.verify_user_email(data.code, device)
115
+
116
+ return ActivateAccountResponseScheme(
117
+ token=activated_account.token,
118
+ session_id=activated_account.session_id,
119
+ user=activated_account.user,
120
+ )
@@ -0,0 +1,16 @@
1
+ from litestar import Controller, get
2
+ import msgspec
3
+
4
+ from auth.guards import is_debug_guard
5
+
6
+
7
+ class E2EActivateCodeSchemeResponse(msgspec.Struct):
8
+ code: str
9
+
10
+ class E2EController(Controller):
11
+ guards = [is_debug_guard]
12
+ path = "e2e"
13
+
14
+ @get('/activate-code')
15
+ async def get_activate_code(self) -> E2EActivateCodeSchemeResponse:
16
+ return E2EActivateCodeSchemeResponse(code='super code')
@@ -0,0 +1,50 @@
1
+ from typing import Annotated
2
+
3
+ from advanced_alchemy.extensions.litestar.dto import SQLAlchemyDTO, SQLAlchemyDTOConfig
4
+ from advanced_alchemy.extensions.litestar.providers import create_service_dependencies
5
+ from advanced_alchemy.filters import FilterTypes
6
+ from advanced_alchemy.service import OffsetPagination
7
+ from litestar import delete, get, post
8
+ from litestar.controller import Controller
9
+ from litestar.params import Dependency
10
+ import msgspec
11
+
12
+ from ..models import PermissionModel
13
+ from ..services import PermissionService
14
+
15
+
16
+ class PermissionCreateRequest(msgspec.Struct):
17
+ name: str
18
+
19
+
20
+ class PermissionDTO(SQLAlchemyDTO[PermissionModel]):
21
+ config = SQLAlchemyDTOConfig()
22
+
23
+
24
+ class PermissionController(Controller):
25
+ dependencies = create_service_dependencies(
26
+ PermissionService,
27
+ key="service",
28
+ filters={
29
+ "created_at": True,
30
+ "updated_at": True,
31
+ },
32
+ )
33
+ return_dto = PermissionDTO
34
+
35
+ @get(operation_id="ListPermissions", path="/permissions")
36
+ async def list_permissions(
37
+ self,
38
+ service: PermissionService,
39
+ filters: Annotated[list[FilterTypes], Dependency(skip_validation=True)],
40
+ ) -> OffsetPagination[PermissionModel]:
41
+ results, total = await service.list_and_count(*filters)
42
+ return service.to_schema(data=results, total=total, filters=filters)
43
+
44
+ @post(operation_id="CreatePermission", path="/permissions")
45
+ async def create_permission(self, service: PermissionService, data: PermissionCreateRequest) -> PermissionModel:
46
+ return await service.create(PermissionModel(**msgspec.to_builtins(data)), auto_commit=True)
47
+
48
+ @delete(operation_id="DeletePermission", path="/permissions/{item_id:str}")
49
+ async def delete_permission(self, service: PermissionService, item_id: str) -> None:
50
+ await service.delete(item_id, auto_commit=True)
@@ -0,0 +1,50 @@
1
+ from typing import Annotated
2
+
3
+ from advanced_alchemy.extensions.litestar.dto import SQLAlchemyDTO, SQLAlchemyDTOConfig
4
+ from advanced_alchemy.extensions.litestar.providers import create_service_dependencies
5
+ from advanced_alchemy.filters import FilterTypes
6
+ from advanced_alchemy.service import OffsetPagination
7
+ from litestar import delete, get, post
8
+ from litestar.controller import Controller
9
+ from litestar.params import Dependency
10
+ import msgspec
11
+
12
+ from ..models import RoleModel
13
+ from ..services import RoleService
14
+
15
+
16
+ class RoleCreateRequest(msgspec.Struct):
17
+ name: str
18
+
19
+
20
+ class RoleDTO(SQLAlchemyDTO[RoleModel]):
21
+ config = SQLAlchemyDTOConfig()
22
+
23
+
24
+ class RoleController(Controller):
25
+ dependencies = create_service_dependencies(
26
+ RoleService,
27
+ key="service",
28
+ filters={
29
+ "created_at": True,
30
+ "updated_at": True,
31
+ },
32
+ )
33
+ return_dto = RoleDTO
34
+
35
+ @get(operation_id="ListRoles", path="/roles")
36
+ async def list_roles(
37
+ self,
38
+ service: RoleService,
39
+ filters: Annotated[list[FilterTypes], Dependency(skip_validation=True)],
40
+ ) -> OffsetPagination[RoleModel]:
41
+ results, total = await service.list_and_count(*filters)
42
+ return service.to_schema(data=results, total=total, filters=filters)
43
+
44
+ @post(operation_id="CreateRole", path="/roles")
45
+ async def create_role(self, service: RoleService, data: RoleCreateRequest) -> RoleModel:
46
+ return await service.create(RoleModel(**msgspec.to_builtins(data)), auto_commit=True)
47
+
48
+ @delete(operation_id="DeleteRole", path="/roles/{item_id:str}")
49
+ async def delete_role(self, service: RoleService, item_id: str) -> None:
50
+ await service.delete(item_id, auto_commit=True)
@@ -0,0 +1,55 @@
1
+ from typing import Annotated
2
+
3
+ from advanced_alchemy.extensions.litestar.dto import SQLAlchemyDTO, SQLAlchemyDTOConfig
4
+ from advanced_alchemy.extensions.litestar.providers import create_service_dependencies
5
+ from advanced_alchemy.filters import FilterTypes
6
+ from advanced_alchemy.service import OffsetPagination
7
+ from litestar import delete, get, post
8
+ from litestar.controller import Controller
9
+ from litestar.params import Dependency
10
+ import msgspec
11
+
12
+ from ..models import UserModel
13
+ from ..services import UserService
14
+
15
+
16
+ class UserCreateRequest(msgspec.Struct):
17
+ email: str
18
+ password: str
19
+ is_email_verified: bool
20
+ is_enabled: bool
21
+
22
+
23
+ class UserDTO(SQLAlchemyDTO[UserModel]):
24
+ config = SQLAlchemyDTOConfig(exclude={"password"})
25
+
26
+
27
+ class UserController(Controller):
28
+ dependencies = create_service_dependencies(
29
+ UserService,
30
+ key="service",
31
+ filters={
32
+ "created_at": True,
33
+ "updated_at": True,
34
+ "sort_field": "email",
35
+ "search": "email",
36
+ },
37
+ )
38
+ return_dto = UserDTO
39
+
40
+ @get(operation_id="ListUsers", path="/users")
41
+ async def list_users(
42
+ self,
43
+ service: UserService,
44
+ filters: Annotated[list[FilterTypes], Dependency(skip_validation=True)],
45
+ ) -> OffsetPagination[UserModel]:
46
+ results, total = await service.list_and_count(*filters)
47
+ return service.to_schema(data=results, total=total, filters=filters)
48
+
49
+ @post(operation_id="CreateUser", path="/users")
50
+ async def create_user(self, service: UserService, data: UserCreateRequest) -> UserModel:
51
+ return await service.create(UserModel(**msgspec.to_builtins(data)), auto_commit=True)
52
+
53
+ @delete(operation_id="DeleteUser", path="/users/{item_id:str}")
54
+ async def delete_user(self, service: UserService, item_id: str) -> None:
55
+ await service.delete(item_id, auto_commit=True)
auth/factories.py ADDED
@@ -0,0 +1,19 @@
1
+ from faker import Faker
2
+ from polyfactory.factories.sqlalchemy_factory import SQLAlchemyFactory
3
+ from polyfactory.fields import Use
4
+
5
+ from .models import PermissionModel, RoleModel, UserModel
6
+
7
+ faker = Faker()
8
+
9
+
10
+ class UserFactory(SQLAlchemyFactory[UserModel]):
11
+ email = Use(faker.unique.email)
12
+
13
+
14
+ class RoleFactory(SQLAlchemyFactory[RoleModel]):
15
+ name = Use(faker.unique.word)
16
+
17
+
18
+ class PermissionFactory(SQLAlchemyFactory[PermissionModel]):
19
+ name = Use(faker.unique.word)
auth/fixtures.yaml ADDED
@@ -0,0 +1,3 @@
1
+ users:
2
+ - email: admin@mail.com
3
+ password: password
auth/guards.py ADDED
@@ -0,0 +1,9 @@
1
+ from litestar.connection import ASGIConnection
2
+ from litestar.exceptions import HTTPException
3
+ from litestar.handlers.base import BaseRouteHandler
4
+ from litestar.status_codes import HTTP_403_FORBIDDEN
5
+
6
+
7
+ def is_debug_guard(connection: ASGIConnection, _: BaseRouteHandler) -> None:
8
+ if not connection.app.debug:
9
+ raise HTTPException(detail="only in debug mode", status_code=HTTP_403_FORBIDDEN)
auth/loaders.py ADDED
@@ -0,0 +1,30 @@
1
+ from typing import Any
2
+
3
+ from auth.models import UserModel
4
+ from auth.services import AuthService, PermissionService, RoleService, UserService
5
+
6
+
7
+ class AuthLoader:
8
+ def __init__(
9
+ self,
10
+ user_service: UserService,
11
+ role_service: RoleService,
12
+ permission_service: PermissionService,
13
+ ):
14
+ self.user_service = user_service
15
+ self.role_service = role_service
16
+ self.permission_service = permission_service
17
+
18
+ async def load(self, data: dict[str, Any]):
19
+ await self.user_service.create_many(
20
+ [
21
+ UserModel(
22
+ email=user["email"],
23
+ password=AuthService.hash_password(user["password"]),
24
+ is_enabled=True,
25
+ is_email_verified=True,
26
+ )
27
+ for user in data.get("users", [])
28
+ ],
29
+ auto_commit=True,
30
+ )
auth/middleware.py ADDED
@@ -0,0 +1,43 @@
1
+ from litestar.connection import ASGIConnection
2
+ from litestar.exceptions import NotAuthorizedException
3
+ from litestar.middleware import (
4
+ AbstractAuthenticationMiddleware,
5
+ AuthenticationResult,
6
+ )
7
+
8
+ from core.storage import Storage
9
+
10
+ from .services import provide_auth_service, provide_user_service
11
+ from .services.auth_service import TOKEN_PREFIX, AuthService
12
+
13
+
14
+ class JWTAuthMiddleware(AbstractAuthenticationMiddleware):
15
+ async def authenticate_request(self, connection: ASGIConnection) -> AuthenticationResult:
16
+ auth_header = connection.headers.get("Authorization")
17
+ if not auth_header:
18
+ raise NotAuthorizedException()
19
+
20
+ token = auth_header.replace(f"{TOKEN_PREFIX} ", "")
21
+ auth_service: AuthService = await self._get_auth_service(connection)
22
+
23
+ return AuthenticationResult(
24
+ user=await auth_service.check_token(token, connection.headers.get("User-Agent")),
25
+ auth=token,
26
+ )
27
+
28
+ @staticmethod
29
+ async def _get_auth_service(connection: ASGIConnection) -> AuthService:
30
+ storage: Storage = await connection.app.dependencies.get("storage")()
31
+ db_session = await connection.app.dependencies.get("db_session")(
32
+ state=connection.app.state,
33
+ scope=connection.scope,
34
+ )
35
+ mail_client = await connection.app.dependencies.get("mail_client")()
36
+ user_service = await provide_user_service(db_session=db_session)
37
+ async for auth_service in provide_auth_service(
38
+ db_session=db_session,
39
+ mail_client=mail_client,
40
+ storage=storage,
41
+ user_service=user_service,
42
+ ):
43
+ return auth_service
@@ -0,0 +1,16 @@
1
+ from .role_permission_model import RolePermissionModel
2
+ from .user_role_model import UserRoleModel
3
+
4
+ from .auth_code_model import AuthCodeModel
5
+ from .permission_model import PermissionModel
6
+ from .role_model import RoleModel
7
+ from .user_model import UserModel
8
+
9
+ __all__ = [
10
+ "AuthCodeModel",
11
+ "RoleModel",
12
+ "PermissionModel",
13
+ "RolePermissionModel",
14
+ "UserModel",
15
+ "UserRoleModel",
16
+ ]
@@ -0,0 +1,10 @@
1
+ from advanced_alchemy.base import UUIDAuditBase
2
+ from sqlalchemy import ForeignKey
3
+ from sqlalchemy.orm import Mapped, mapped_column
4
+
5
+
6
+ class AuthCodeModel(UUIDAuditBase):
7
+ __tablename__ = "auth_codes"
8
+
9
+ code: Mapped[str] = mapped_column()
10
+ user_id = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), unique=True)
@@ -0,0 +1,8 @@
1
+ from advanced_alchemy.base import UUIDAuditBase
2
+ from sqlalchemy.orm import Mapped, mapped_column
3
+
4
+
5
+ class PermissionModel(UUIDAuditBase):
6
+ __tablename__ = "permissions"
7
+
8
+ name: Mapped[str] = mapped_column(unique=True)
@@ -0,0 +1,14 @@
1
+ from advanced_alchemy.base import UUIDAuditBase
2
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
3
+
4
+
5
+ class RoleModel(UUIDAuditBase):
6
+ __tablename__ = "roles"
7
+
8
+ name: Mapped[str] = mapped_column(unique=True)
9
+ users = relationship(
10
+ "UserModel",
11
+ secondary="users_roles",
12
+ back_populates="roles",
13
+ lazy="selectin",
14
+ )
@@ -0,0 +1,10 @@
1
+ from advanced_alchemy.base import UUIDAuditBase
2
+ from sqlalchemy import ForeignKey
3
+ from sqlalchemy.orm import Mapped, mapped_column
4
+
5
+
6
+ class RolePermissionModel(UUIDAuditBase):
7
+ __tablename__ = "roles_permissions"
8
+
9
+ role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"), primary_key=True)
10
+ permission_id: Mapped[int] = mapped_column(ForeignKey("permissions.id"), primary_key=True)
@@ -0,0 +1,18 @@
1
+ from advanced_alchemy.base import UUIDAuditBase
2
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
3
+
4
+
5
+ class UserModel(UUIDAuditBase):
6
+ __tablename__ = "users"
7
+
8
+ email: Mapped[str] = mapped_column(unique=True)
9
+ password: Mapped[str]
10
+ is_email_verified: Mapped[bool]
11
+ is_enabled: Mapped[bool]
12
+
13
+ roles = relationship(
14
+ "RoleModel",
15
+ secondary="users_roles",
16
+ back_populates="users",
17
+ lazy="selectin",
18
+ )
@@ -0,0 +1,10 @@
1
+ from advanced_alchemy.base import UUIDAuditBase
2
+ from sqlalchemy import ForeignKey
3
+ from sqlalchemy.orm import Mapped, mapped_column
4
+
5
+
6
+ class UserRoleModel(UUIDAuditBase):
7
+ __tablename__ = "users_roles"
8
+
9
+ role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"), primary_key=True)
10
+ user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), primary_key=True)
auth/plugin.py ADDED
@@ -0,0 +1,50 @@
1
+ from pathlib import Path
2
+
3
+ import click
4
+ from click import Group
5
+ from litestar.plugins import CLIPlugin
6
+ import yaml
7
+
8
+ from core.cli import coro
9
+ from core.database import session_maker
10
+ from core.mail import MailClient
11
+ from core.settings import settings
12
+
13
+ from .loaders import AuthLoader
14
+ from .services import provide_permission_service, provide_role_service, provide_user_service
15
+
16
+
17
+ class AuthPlugin(CLIPlugin):
18
+ def on_cli_init(self, cli: Group) -> None:
19
+ @cli.group(help="Manage auth, load data with ``load`` command")
20
+ @click.version_option(prog_name="auth")
21
+ def auth(): ...
22
+
23
+ @auth.command(help="load auth data")
24
+ @coro
25
+ async def load():
26
+ async with session_maker() as session:
27
+ click.echo("Loading auth data... ")
28
+ loader = AuthLoader(
29
+ user_service=await provide_user_service(session),
30
+ role_service=await provide_role_service(session),
31
+ permission_service=await provide_permission_service(session),
32
+ )
33
+
34
+ with open(f"{Path(__file__).parent.resolve()}/fixtures.yaml") as f:
35
+ data = yaml.safe_load(f)
36
+ await loader.load(data)
37
+
38
+ @auth.command(help="load auth data")
39
+ @coro
40
+ async def clear():
41
+ async with session_maker() as session:
42
+ user_service = await provide_user_service(session)
43
+ await user_service.delete_where()
44
+ click.echo("Clear auth data")
45
+
46
+ @auth.command(help="Send test mail")
47
+ @click.argument("recipient")
48
+ def send_mail(recipient):
49
+ mail_controller = MailClient(settings.mail_config)
50
+ mail_controller.send([recipient], "test", "test")
@@ -0,0 +1,45 @@
1
+ from .auth_service import (
2
+ API_KEY_HEADER,
3
+ TOKEN_PREFIX,
4
+ AccountDTO,
5
+ AuthService,
6
+ DecodeTokenError,
7
+ InvalidEmailError,
8
+ InvalidPasswordError,
9
+ LoginRequestDTO,
10
+ LogoutRequestDTO,
11
+ SignUpRequestDTO,
12
+ UserNotEnabledError,
13
+ UserEmailNotVerifiedError,
14
+ WrongAuthCodeError,
15
+ provide_auth_service,
16
+ )
17
+ from .permission_service import PermissionService, provide_permission_service
18
+ from .role_service import RoleService, provide_role_service
19
+ from .user_service import UserService, provide_user_service
20
+
21
+ __all__ = [
22
+ "UserService",
23
+ "AuthService",
24
+ "PermissionService",
25
+ "RoleService",
26
+ "AccountDTO",
27
+ "LogoutRequestDTO",
28
+ "LoginRequestDTO",
29
+ "SignUpRequestDTO",
30
+ # providers
31
+ "provide_user_service",
32
+ "provide_auth_service",
33
+ "provide_permission_service",
34
+ "provide_role_service",
35
+ # errors
36
+ "InvalidPasswordError",
37
+ "UserNotEnabledError",
38
+ "UserEmailNotVerifiedError",
39
+ "InvalidEmailError",
40
+ "DecodeTokenError",
41
+ "WrongAuthCodeError",
42
+ # constants
43
+ "API_KEY_HEADER",
44
+ "TOKEN_PREFIX",
45
+ ]
@@ -0,0 +1,309 @@
1
+ import random
2
+ from typing import Any, AsyncGenerator, List
3
+ from uuid import UUID, uuid4
4
+
5
+ import advanced_alchemy
6
+ from advanced_alchemy.repository import SQLAlchemyAsyncRepository
7
+ import bcrypt
8
+ import jwt
9
+ from litestar.exceptions import NotAuthorizedException
10
+ import msgspec
11
+ from nats.js.errors import KeyNotFoundError
12
+ from sqlalchemy.ext.asyncio import AsyncSession
13
+
14
+ from core.mail import MailClient
15
+ from core.settings import settings
16
+ from core.storage import Storage
17
+
18
+ from ..models import AuthCodeModel, UserModel
19
+ from .user_service import UserService
20
+
21
+ __all__ = [
22
+ "UserService",
23
+ "AuthService",
24
+ "LoginRequestDTO",
25
+ "LogoutRequestDTO",
26
+ "SignUpRequestDTO",
27
+ "AccountCacheDTO",
28
+ "AccountCacheSessionDTO",
29
+ "provide_auth_service",
30
+ "InvalidPasswordError",
31
+ "InvalidEmailError",
32
+ "DecodeTokenError",
33
+ "UserNotEnabledError",
34
+ "UserEmailNotVerifiedError",
35
+ "API_KEY_HEADER",
36
+ "TOKEN_PREFIX",
37
+ "AccountDTO",
38
+ "WrongAuthCodeError",
39
+ ]
40
+
41
+
42
+ API_KEY_HEADER = "Authorization"
43
+ TOKEN_PREFIX = "Bearer"
44
+
45
+
46
+ class InvalidPasswordError(Exception):
47
+ pass
48
+
49
+
50
+ class InvalidEmailError(Exception):
51
+ pass
52
+
53
+
54
+ class UserNotEnabledError(Exception):
55
+ pass
56
+
57
+
58
+ class UserEmailNotVerifiedError(Exception):
59
+ pass
60
+
61
+
62
+ class DecodeTokenError(Exception):
63
+ pass
64
+
65
+
66
+ class WrongAuthCodeError(Exception):
67
+ pass
68
+
69
+
70
+ class AccountRoleDTO(msgspec.Struct):
71
+ id: UUID
72
+ name: str
73
+
74
+
75
+ class AccountUserDTO(msgspec.Struct):
76
+ id: UUID
77
+ email: str
78
+ is_email_verified: bool
79
+ is_enabled: bool
80
+ roles: list[AccountRoleDTO] = msgspec.field(default_factory=list)
81
+
82
+
83
+ class AccountDTO(msgspec.Struct):
84
+ token: str
85
+ session_id: UUID
86
+ user: AccountUserDTO
87
+
88
+
89
+ class TokenAccountDataDTO(msgspec.Struct):
90
+ session_id: UUID
91
+ user: AccountUserDTO
92
+
93
+
94
+ class LoginRequestDTO(msgspec.Struct):
95
+ email: str
96
+ password: str
97
+ device: str
98
+
99
+
100
+ class LogoutRequestDTO(msgspec.Struct):
101
+ user_id: UUID
102
+ session_id: UUID
103
+ device: str
104
+
105
+
106
+ class SignUpRequestDTO(msgspec.Struct):
107
+ email: str
108
+ password: str
109
+
110
+
111
+ class AccountCacheSessionDTO(msgspec.Struct):
112
+ device: str
113
+ session_id: UUID
114
+
115
+
116
+ class AccountCacheDTO(msgspec.Struct):
117
+ sessions: list[AccountCacheSessionDTO]
118
+ user: AccountUserDTO
119
+
120
+
121
+ class LoginResponseDTO(AccountDTO): ...
122
+
123
+
124
+ class ActivateUserResponseDTO(AccountDTO): ...
125
+
126
+
127
+ class AuthCodeRepository(SQLAlchemyAsyncRepository[AuthCodeModel]):
128
+ model_type = AuthCodeModel
129
+
130
+
131
+ class AuthService:
132
+ def __init__(
133
+ self,
134
+ session: AsyncSession,
135
+ mail_client: MailClient,
136
+ storage: Storage,
137
+ user_service: UserService,
138
+ ):
139
+ self.session = session
140
+ self.mail_client = mail_client
141
+ self.storage = storage
142
+ self.user_service = user_service
143
+ self.auth_code_repository = AuthCodeRepository(session=session)
144
+
145
+ async def signup(self, user: SignUpRequestDTO) -> AuthCodeModel:
146
+ signup_user = await self.user_service.create(
147
+ UserModel(
148
+ email=user.email,
149
+ password=self.hash_password(user.password),
150
+ is_email_verified=False,
151
+ is_enabled=True,
152
+ ),
153
+ auto_commit=True,
154
+ )
155
+ auth_code = await self.auth_code_repository.add(
156
+ AuthCodeModel(
157
+ user_id=signup_user.id,
158
+ code=self.generate_activate_code(),
159
+ ),
160
+ auto_commit=True,
161
+ )
162
+
163
+ self.mail_client.send([user.email], "Sign up", auth_code.code)
164
+ return auth_code
165
+
166
+ async def verify_user_email(self, code: str, device: str) -> LoginResponseDTO:
167
+ auth_code = await self.auth_code_repository.get_one_or_none(AuthCodeModel.code == code)
168
+ if not auth_code:
169
+ raise WrongAuthCodeError
170
+
171
+ user = await self.user_service.update(
172
+ UserModel(id=auth_code.user_id, is_enabled=True, is_email_verified=True),
173
+ auto_commit=True,
174
+ )
175
+ return await self._add_account_session(user, device)
176
+
177
+ async def login(self, login_data: LoginRequestDTO) -> LoginResponseDTO:
178
+ try:
179
+ login_user = await self.user_service.get_one(UserModel.email == login_data.email)
180
+
181
+ except advanced_alchemy.exceptions.NotFoundError:
182
+ raise InvalidEmailError(f"Email {login_data.email} not found")
183
+
184
+ if not login_user.is_enabled:
185
+ raise UserNotEnabledError(f"User {login_data.email} not enabled")
186
+
187
+ if not login_user.is_email_verified:
188
+ raise UserEmailNotVerifiedError(f"User {login_data.email} not verified")
189
+
190
+ if not self._check_password(login_data.password, login_user.password):
191
+ raise InvalidPasswordError
192
+
193
+ return await self._add_account_session(login_user, login_data.device)
194
+
195
+ async def _add_account_session(self, user: UserModel, device: str) -> LoginResponseDTO:
196
+ login_session = AccountCacheSessionDTO(
197
+ session_id=uuid4(),
198
+ device=device,
199
+ )
200
+
201
+ account_user = AccountUserDTO(
202
+ id=user.id,
203
+ email=user.email,
204
+ is_email_verified=user.is_email_verified,
205
+ is_enabled=user.is_enabled,
206
+ roles=[AccountRoleDTO(id=role.id, name=role.name) for role in user.roles],
207
+ )
208
+
209
+ try:
210
+ cached_account = await self.storage.get("sessions", str(user.id), model_type=AccountCacheDTO)
211
+ cached_account.sessions.append(login_session)
212
+ except KeyNotFoundError:
213
+ cached_account = AccountCacheDTO(user=account_user, sessions=[login_session])
214
+
215
+ jwt_token = self.encode_token(
216
+ TokenAccountDataDTO(
217
+ session_id=login_session.session_id,
218
+ user=account_user,
219
+ )
220
+ )
221
+ await self.storage.save("sessions", str(cached_account.user.id), cached_account)
222
+
223
+ return LoginResponseDTO(
224
+ token=jwt_token,
225
+ session_id=login_session.session_id,
226
+ user=account_user,
227
+ )
228
+
229
+ async def start_reset_password(self, user_id: UUID):
230
+ raise NotImplementedError()
231
+
232
+ async def logout(self, logout_request: LogoutRequestDTO) -> None:
233
+ cached_user = await self.storage.get("sessions", str(logout_request.user_id), model_type=AccountCacheDTO)
234
+ cached_user.sessions = [
235
+ session
236
+ for session in cached_user.sessions
237
+ if not (session.session_id == logout_request.session_id and session.device == logout_request.device)
238
+ ]
239
+ await self.storage.save("sessions", str(logout_request.user_id), cached_user)
240
+
241
+ async def check_token(self, token: str, user_agent: str) -> AccountDTO:
242
+ try:
243
+ account = await self.get_account(token)
244
+ except DecodeTokenError:
245
+ raise NotAuthorizedException("Decode token fail")
246
+
247
+ await self.check_session(account, user_agent)
248
+ return account
249
+
250
+ async def check_session(self, account: AccountDTO, device: str):
251
+ try:
252
+ cached_account = await self.storage.get("sessions", str(account.user.id), model_type=AccountCacheDTO)
253
+ for session in cached_account.sessions:
254
+ if session.session_id == account.session_id:
255
+ if session.device != device:
256
+ raise NotAuthorizedException("Wrong device")
257
+ return
258
+ raise NotAuthorizedException("Session not found")
259
+ except KeyNotFoundError:
260
+ raise NotAuthorizedException()
261
+
262
+ @staticmethod
263
+ def _check_password(password: str, hashed_password: str) -> bool:
264
+ return bcrypt.checkpw(password.encode("utf-8"), hashed_password.encode("utf-8"))
265
+
266
+ @staticmethod
267
+ def hash_password(password: str) -> str:
268
+ return bcrypt.hashpw(password.encode("utf8"), bcrypt.gensalt()).decode("utf8")
269
+
270
+ async def get_account(self, token: str) -> AccountDTO:
271
+ try:
272
+ account_data = self.decode_token(token)
273
+ token_account_data = msgspec.convert(account_data, type=TokenAccountDataDTO)
274
+ return AccountDTO(
275
+ token=token,
276
+ session_id=token_account_data.session_id,
277
+ user=token_account_data.user,
278
+ )
279
+ except Exception:
280
+ raise DecodeTokenError
281
+
282
+ @staticmethod
283
+ def encode_token(token_account_data: TokenAccountDataDTO) -> str:
284
+ return jwt.encode(msgspec.to_builtins(token_account_data), settings.jwt_secret, algorithm="HS256")
285
+
286
+ @staticmethod
287
+ def decode_token(token: str) -> dict[str, Any]:
288
+ try:
289
+ return jwt.decode(token, settings.jwt_secret, algorithms=["HS256"])
290
+ except jwt.exceptions.InvalidSignatureError:
291
+ raise DecodeTokenError
292
+
293
+ @staticmethod
294
+ def generate_activate_code() -> str:
295
+ return "".join(str(random.randint(0, 9)) for _ in range(8))
296
+
297
+
298
+ async def provide_auth_service(
299
+ db_session: AsyncSession,
300
+ mail_client: MailClient,
301
+ storage: Storage,
302
+ user_service: UserService,
303
+ ) -> AsyncGenerator[AuthService, None]:
304
+ yield AuthService(
305
+ session=db_session,
306
+ mail_client=mail_client,
307
+ storage=storage,
308
+ user_service=user_service,
309
+ )
@@ -0,0 +1,18 @@
1
+ from advanced_alchemy.repository import SQLAlchemyAsyncRepository
2
+ from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+
5
+ from ..models import PermissionModel
6
+
7
+ __all__ = ["PermissionService", "provide_permission_service"]
8
+
9
+
10
+ class PermissionService(SQLAlchemyAsyncRepositoryService):
11
+ class Repository(SQLAlchemyAsyncRepository[PermissionModel]):
12
+ model_type = PermissionModel
13
+
14
+ repository_type = Repository
15
+
16
+
17
+ async def provide_permission_service(db_session: AsyncSession) -> PermissionService:
18
+ return PermissionService(session=db_session)
@@ -0,0 +1,21 @@
1
+ from advanced_alchemy.repository import SQLAlchemyAsyncRepository
2
+ from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+
5
+ from ..models import RoleModel
6
+
7
+ __all__ = [
8
+ "RoleService",
9
+ "provide_role_service",
10
+ ]
11
+
12
+
13
+ class RoleService(SQLAlchemyAsyncRepositoryService):
14
+ class Repository(SQLAlchemyAsyncRepository[RoleModel]):
15
+ model_type = RoleModel
16
+
17
+ repository_type = Repository
18
+
19
+
20
+ async def provide_role_service(db_session: AsyncSession) -> RoleService:
21
+ return RoleService(session=db_session)
@@ -0,0 +1,21 @@
1
+ from advanced_alchemy.repository import SQLAlchemyAsyncRepository
2
+ from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+
5
+ from ..models import UserModel
6
+
7
+ __all__ = [
8
+ "UserService",
9
+ "provide_user_service",
10
+ ]
11
+
12
+
13
+ class UserService(SQLAlchemyAsyncRepositoryService):
14
+ class Repository(SQLAlchemyAsyncRepository[UserModel]):
15
+ model_type = UserModel
16
+
17
+ repository_type = Repository
18
+
19
+
20
+ async def provide_user_service(db_session: AsyncSession) -> UserService:
21
+ return UserService(session=db_session)
core/cli.py ADDED
@@ -0,0 +1,10 @@
1
+ import asyncio
2
+ from functools import wraps
3
+
4
+
5
+ def coro(f):
6
+ @wraps(f)
7
+ def wrapper(*args, **kwargs):
8
+ return asyncio.run(f(*args, **kwargs))
9
+
10
+ return wrapper
core/database.py ADDED
@@ -0,0 +1,16 @@
1
+ from advanced_alchemy.base import UUIDAuditBase
2
+ from litestar.plugins.sqlalchemy import SQLAlchemyAsyncConfig, SQLAlchemyPlugin
3
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
4
+
5
+ from .settings import settings
6
+
7
+ config = SQLAlchemyAsyncConfig(
8
+ connection_string=settings.db_url,
9
+ metadata=UUIDAuditBase.metadata,
10
+ create_all=settings.db_create_all,
11
+ )
12
+ sqlalchemy_plugin = SQLAlchemyPlugin(config=config)
13
+
14
+
15
+ engine = create_async_engine(settings.db_url, echo=False)
16
+ session_maker = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
core/mail.py ADDED
@@ -0,0 +1,31 @@
1
+ from email.message import EmailMessage
2
+ import smtplib
3
+
4
+ from .settings import MailSettings, settings
5
+
6
+
7
+ class MailClient:
8
+ def __init__(self, config: MailSettings):
9
+ self.settings = config
10
+
11
+ def send(self, recipients: list[str], subject: str, body: str):
12
+ msg = EmailMessage()
13
+ msg.set_content(body)
14
+ server = smtplib.SMTP(self.settings.host, self.settings.port, timeout=5)
15
+ server.starttls()
16
+ server.login(self.settings.login, self.settings.password)
17
+
18
+ msg = EmailMessage()
19
+ msg["Subject"] = subject
20
+ msg.set_content(body)
21
+ msg["From"] = self.settings.login
22
+
23
+ for recipient in recipients:
24
+ msg["To"] = recipient
25
+ server.send_message(msg)
26
+
27
+ server.quit()
28
+
29
+
30
+ async def provide_mail_client():
31
+ return MailClient(settings.mail_config)
core/openapi.py ADDED
@@ -0,0 +1,26 @@
1
+ from litestar.openapi import OpenAPIConfig
2
+ from litestar.openapi.plugins import ScalarRenderPlugin
3
+ from litestar.openapi.spec import Components, SecurityScheme
4
+
5
+ from .settings import settings
6
+
7
+ openapi_config = OpenAPIConfig(
8
+ components=[
9
+ Components(
10
+ security_schemes={
11
+ "JWT": SecurityScheme(
12
+ type="http",
13
+ scheme="Bearer",
14
+ name="Authorization",
15
+ security_scheme_in="cookie",
16
+ bearer_format="JWT",
17
+ description="Authorization",
18
+ )
19
+ }
20
+ )
21
+ ],
22
+ title=settings.open_api_config.title,
23
+ version=settings.open_api_config.version,
24
+ path=settings.open_api_config.path,
25
+ render_plugins=[ScalarRenderPlugin()],
26
+ )
core/settings.py ADDED
@@ -0,0 +1,46 @@
1
+ from pydantic import Field
2
+ from pydantic_settings import BaseSettings
3
+
4
+
5
+ class StorageSettings(BaseSettings):
6
+ url: str = Field(default="nats://localhost:4222")
7
+ buckets: list[str] = Field()
8
+
9
+ class Config:
10
+ env_prefix = "STORAGE_"
11
+
12
+
13
+ class MailSettings(BaseSettings):
14
+ host: str = Field(default="smtp.yandex.com")
15
+ port: int = Field(default=587)
16
+ login: str = Field(default="info@service-laboratory.online")
17
+ password: str = Field(default="superPassword")
18
+
19
+ class Config:
20
+ env_prefix = "MAIL_"
21
+
22
+
23
+ class OpenApiSettings(BaseSettings):
24
+ title: str = Field(deafult="title")
25
+ version: str = Field(deafult="0.1.2")
26
+ path: str = Field(deafult="/api/docs")
27
+
28
+ class Config:
29
+ env_prefix = "OPEN_API_"
30
+
31
+
32
+ class Settings(BaseSettings):
33
+ debug: bool = Field(default=False)
34
+ jwt_secret: str = Field(default="secret_for_jwt")
35
+
36
+ # database config
37
+ db_url: str = Field(default="postgresql+asyncpg://user:password@localhost:5432/db")
38
+ db_create_all: bool = Field(default=False)
39
+
40
+ # services config
41
+ open_api_config: OpenApiSettings = Field(default_factory=OpenApiSettings)
42
+ mail_config: MailSettings = Field(default_factory=MailSettings)
43
+ storage_config: StorageSettings = Field(default_factory=StorageSettings)
44
+
45
+
46
+ settings = Settings()
core/storage.py ADDED
@@ -0,0 +1,54 @@
1
+ from typing import TypeVar
2
+
3
+ from faststream import FastStream
4
+ from faststream.nats import NatsBroker
5
+ from litestar import Litestar
6
+ import msgspec
7
+
8
+ from .settings import StorageSettings, settings
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ class Storage:
14
+ def __init__(self, config: StorageSettings):
15
+ self.broker = NatsBroker(servers=config.url)
16
+ self.app = FastStream(self.broker)
17
+ self.settings = config
18
+ self.buckets = {}
19
+ self.is_connected = False
20
+
21
+ async def connect(self):
22
+ if not self.is_connected:
23
+ await self.broker.connect()
24
+ for bucket in self.settings.buckets:
25
+ await self.init_bucket(bucket)
26
+ self.is_connected = True
27
+
28
+ async def init_bucket(self, name: str):
29
+ self.buckets[name] = await self.broker.key_value(name)
30
+
31
+ async def save(self, bucket: str, key: str, data: msgspec.Struct):
32
+ await self.buckets[bucket].put(key, msgspec.json.encode(data))
33
+
34
+ async def get(self, bucket: str, key: str, model_type: T) -> T:
35
+ data = await self.buckets[bucket].get(key)
36
+ return msgspec.json.decode(data.value, type=model_type)
37
+
38
+ async def delete(self, bucket: str, key: str):
39
+ await self.buckets[bucket].delete(key)
40
+
41
+ async def disconnect(self):
42
+ await self.broker.stop()
43
+ self.is_connected = False
44
+
45
+
46
+ async def provide_storage() -> Storage:
47
+ storage = Storage(settings.storage_config)
48
+ await storage.connect()
49
+ return storage
50
+
51
+
52
+ async def close_storage(app: Litestar) -> None:
53
+ storage: Storage = await app.dependencies.get("storage")()
54
+ await storage.disconnect()
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: service_packages
3
+ Version: 4.0.22
4
+ Summary: Service laboratory packages
5
+ Author-email: Alexei Rysev <alexeirysev@gmail.com>
6
+ Maintainer-email: Alexei Rysev <alexeirysev@gmail.com>
7
+ License: Copyright (c) 2018 The Python Packaging Authority
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+ License-File: LICENSE.txt
27
+ Keywords: development,prototype,setuptools
28
+ Classifier: Development Status :: 3 - Alpha
29
+ Classifier: Intended Audience :: Developers
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Programming Language :: Python :: 3
32
+ Classifier: Programming Language :: Python :: 3 :: Only
33
+ Classifier: Programming Language :: Python :: 3.12
34
+ Classifier: Programming Language :: Python :: 3.13
35
+ Classifier: Topic :: Software Development :: Build Tools
36
+ Requires-Python: >=3.12
37
+ Requires-Dist: advanced-alchemy==1.4.4
38
+ Requires-Dist: asyncpg>=0.30.0
39
+ Requires-Dist: bcrypt>=5.0.0
40
+ Requires-Dist: faststream[nats]>=0.6.3
41
+ Requires-Dist: litestar[standard]>=2.15.2
42
+ Requires-Dist: pydantic-settings>=2.8.1
43
+ Requires-Dist: pydantic>=2.11.3
44
+ Requires-Dist: pyjwt>=2.10.1
45
+ Requires-Dist: pytest-asyncio>=0.24.0
46
+ Requires-Dist: pytest-cov>=7.0.0
47
+ Requires-Dist: pytest-html>=4.1.1
48
+ Requires-Dist: pytest>=8.4.2
49
+ Requires-Dist: pyyaml>=6.0.3
50
+ Requires-Dist: sqlalchemy>=2.0.40
51
+ Requires-Dist: timer-context>=1.1.1
52
+ Requires-Dist: uvicorn>=0.34.1
53
+ Provides-Extra: dev
54
+ Requires-Dist: check-manifest; extra == 'dev'
55
+ Provides-Extra: test
56
+ Requires-Dist: coverage; extra == 'test'
57
+ Requires-Dist: pytest-asyncio==0.24.0; extra == 'test'
58
+ Requires-Dist: pytest>=8.2.1; extra == 'test'
59
+ Description-Content-Type: text/markdown
60
+
61
+ # service laboratory package
62
+
63
+ - auth module
@@ -0,0 +1,35 @@
1
+ auth/__init__.py,sha256=a9OT6y15U8RGLH5CQRkb4irAOdZAjoTzC6-pCW1ArG8,101
2
+ auth/factories.py,sha256=aBAd50v_wfFVsBw70J78C0CCfxmjRI0tzYw5UtSWlOM,475
3
+ auth/fixtures.yaml,sha256=K4b2cfy7lsshSqATMte_HYCACqSVzOyZTQkW4BuQzKQ,55
4
+ auth/guards.py,sha256=hjpjDYr19I57VM-kQ2Kh5M81Pxr5yUIoiwkfth75gnY,399
5
+ auth/loaders.py,sha256=GiiHdKqNGNyUIYbWSHFMTQZfOjX_lP9oP4JGuvg2lSI,906
6
+ auth/middleware.py,sha256=-fYkpWcjWWGNr0FCWWoiv6d571flCqCvqDzDJo_bXuU,1677
7
+ auth/plugin.py,sha256=tYsX_HdDumN1IBrUewsQRLC7xvVK74d2tu87V9MpERE,1780
8
+ auth/api/__init__.py,sha256=5b8giem7j-uIRZQleEtW3kM4yuMj3xPay0j6ussw0_c,430
9
+ auth/api/account_controller.py,sha256=4xXKeNDJLSoDn7OGv2VKCopMru4s1e7yncjgmSEEj_Y,3756
10
+ auth/api/e2e_controller.py,sha256=EcvStn8i-Qtj563nifEFXOyUIqHqU7_O5uJQx--ntWs,406
11
+ auth/api/permission_controller.py,sha256=c0Tm4Df054_1DT_vGaHzlRY4OT2WGWhSqVAAKF5LAPM,1865
12
+ auth/api/role_controller.py,sha256=vjH9KaDWTAFFRhHAxW3h17iOib2KgAohkHJm9BC7z_8,1721
13
+ auth/api/user_controller.py,sha256=hGlK81Bop0TUSWO4n1x5eEi1OgOkq7DF3HFMTQ-9mjk,1875
14
+ auth/models/__init__.py,sha256=Kg2UEpg_8k3mIJH5TpLTwh6om92BajUTdOX1TGgJa3Q,397
15
+ auth/models/auth_code_model.py,sha256=tqyhGKfsZxtnKxPw08yi7RZPpwjurNQXnSKYPY50sjQ,328
16
+ auth/models/permission_model.py,sha256=ZRRkU62yXWYGZQZEwjcXiA1RBlSv3H0KtG-aWv2gE78,223
17
+ auth/models/role_model.py,sha256=jnRWHc1TQCLSWpd_bVRyYoowy4NboEkEbjzpAvzFJ1k,368
18
+ auth/models/role_permission_model.py,sha256=qFGiuhrWWSFGaiXZ3oF2gZz2KZrFiOp9r5IxgVeCTCY,394
19
+ auth/models/user_model.py,sha256=mo7prCyxJmw-E0USACdzyt-gVGWnDczEOOuyF3zBg04,461
20
+ auth/models/user_role_model.py,sha256=bq82Ml1evYnz87QTGkHK3CTAIadqBW462MEHK6cBqUU,370
21
+ auth/services/__init__.py,sha256=BofpESzGDllOtdB64gf5A04YDv6ZX2hdFay0SVBKebU,1095
22
+ auth/services/auth_service.py,sha256=gCKU5OnWJ7ra_rMJhQWN6ZYQXRF-VOCZzZKFko8peWY,9178
23
+ auth/services/permission_service.py,sha256=k6xb_decsFH_PVj1PBOaB75Xht2qusqRHMP9FjnJWXU,619
24
+ auth/services/role_service.py,sha256=dNU2Dm3rwoPoytWSxWMPtSOp9gxGQzwbeWvRXxhLe0w,576
25
+ auth/services/user_service.py,sha256=VxEBUckhUpiFXcM1-53rFTYyH0hwMtup6CAgedzS4iw,576
26
+ core/cli.py,sha256=bblQvTjmn5wiYsDSBdyqwN9xnsuRwJrlFenjStHJJ5s,173
27
+ core/database.py,sha256=QUsr9TDVfjnIPAHq8UtnWmRVfQz228KYZis6fSjDVzw,599
28
+ core/mail.py,sha256=50LWGDhN8w2JNZJQMdKsefoCWxmGEdCVEIbl5Q6kMEs,844
29
+ core/openapi.py,sha256=IsmcepeUgIT7Rk9qY2jCDkurOGeJmf0bmnJhDnvye_k,805
30
+ core/settings.py,sha256=CbRF-uUFF6DCKtpvFGyICzEL61rQ6iiwyq309S1DjN0,1300
31
+ core/storage.py,sha256=EMANUfmr14foq7Oq1qkBRWfynNFayQjLDzPa3T0UCSY,1617
32
+ service_packages-4.0.22.dist-info/METADATA,sha256=M3ZwU87zy8pmJ8OSroqFEksdLLTLs1texJhNqzcN8PI,2785
33
+ service_packages-4.0.22.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
+ service_packages-4.0.22.dist-info/licenses/LICENSE.txt,sha256=2bm9uFabQZ3Ykb_SaSU_uUbAj2-htc6WJQmS_65qD00,1073
35
+ service_packages-4.0.22.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2018 The Python Packaging Authority
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.