Coverage for src/auth/services.py: 98%

1 from typing import Any

2 import advanced_alchemy

3 import bcrypt

4 import jwt

5 from advanced_alchemy.repository import SQLAlchemyAsyncRepository

6 from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService

7 from uuid import UUID

8 import msgspec

9 from sqlalchemy.ext.asyncio import AsyncSession

10

11 from ..core.settings import settings

12 from ..core.mail import MailClient

13 from .models import PermissionModel, RoleModel, UserModel

14

15 __all__ = [

16 "UserService",

17 "AuthService",

18 "PermissionService",

19 "RoleService",

20 "LoginUserDTO",

21 "SignupUserDTO",

22 "provide_user_service",

23 "provide_auth_service",

24 "provide_permission_service",

25 "provide_role_service",

26 "InvalidPasswordError",

27 "InvalidEmailError",

28 "DecodeTokenError",

29]

30

31

32 class InvalidPasswordError(Exception):

33 pass

34

35

36 class InvalidEmailError(Exception):

37 pass

38

39

40 class DecodeTokenError(Exception):

41 pass

42

43

44 class LoginUserDTO(msgspec.Struct):

45 email: str

46 password: str

47

48

49 class SignupUserDTO(msgspec.Struct):

50 email: str

51 password: str

52

53

54 class AccountDTO(msgspec.Struct):

55 id: UUID

56 email: str

57

58

59 class UserService(SQLAlchemyAsyncRepositoryService):

60 class Repository(SQLAlchemyAsyncRepository[UserModel]):

61 model_type = UserModel

62

63 repository_type = Repository

64

65

66 class RoleService(SQLAlchemyAsyncRepositoryService):

67 class Repository(SQLAlchemyAsyncRepository[RoleModel]):

68 model_type = RoleModel

69

70 repository_type = Repository

71

72

73 class PermissionService(SQLAlchemyAsyncRepositoryService):

74 class Repository(SQLAlchemyAsyncRepository[PermissionModel]):

75 model_type = PermissionModel

76

77 repository_type = Repository

78

79

80 class AuthService:

81 def __init__(self, session: AsyncSession, mail_client: MailClient):

82 self.session = session

83 self.mail_client = mail_client

84 self.user_service: UserService = provide_user_service(session)

85

86 class Repository(SQLAlchemyAsyncRepository[UserModel]):

87 model_type = UserModel

88

89 repository_type = Repository

90

91 async def signup(self, user: SignupUserDTO) -> UserModel:

92 self.mail_client.send([user.email], "Sign up", "sign up info code")

93 return await self.user_service.create(

94 UserModel(

95 email=user.email,

96 password=self._hash_password(user.password),

97 is_email_verified=False,

98 is_enabled=True,

99 ),

100 auto_commit=True,

101 )

102

103 async def login(self, user: LoginUserDTO) -> str:

104 try:

105 login_user = await self.user_service.get_one(UserModel.email == user.email)

106 except advanced_alchemy.exceptions.NotFoundError:

107 raise InvalidEmailError(f"Email {user.email} not found")

108

109 if not self._check_password(user.password, login_user.password):

110 raise InvalidPasswordError

111

112 return self._encode_token(

113 AccountDTO(

114 id=login_user.id,

115 email=login_user.email,

116 )

117 )

118

119 @staticmethod

120 def _check_password(password: str, hashed_password: str) -> bool:

121 return bcrypt.checkpw(password.encode("utf-8"), hashed_password.encode("utf-8"))

122

123 @staticmethod

124 def _hash_password(password: str) -> str:

125 return bcrypt.hashpw(password.encode("utf8"), bcrypt.gensalt()).decode("utf8")

126

127 async def get_account(self, token: str) -> AccountDTO:

128 try:

129 account_data = self._decode_token(token)

130 return AccountDTO(**account_data)

131 except Exception:

132 raise DecodeTokenError

133

134 def _encode_token(self, account: AccountDTO) -> str:

135 return jwt.encode(msgspec.to_builtins(account), settings.jwt_secret, algorithm="HS256")

136

137 def _decode_token(self, token: str) -> dict[str, Any]:

138 return jwt.decode(token, settings.jwt_secret, algorithms=["HS256"])

139

140

141 def provide_auth_service(db_session: AsyncSession, mail_client: MailClient) -> AuthService:

142 return AuthService(session=db_session, mail_client=mail_client)

143

144

145 def provide_user_service(db_session: AsyncSession) -> UserService:

146 return UserService(session=db_session)

147

148

149 def provide_role_service(db_session: AsyncSession) -> RoleService:

150 return RoleService(session=db_session)

151

152

153 def provide_permission_service(db_session: AsyncSession) -> PermissionService:

154 return PermissionService(session=db_session)