Move users service into a module
This commit is contained in:
parent
72ece8af72
commit
395164f630
5 changed files with 115 additions and 98 deletions
|
|
@ -1,98 +0,0 @@
|
|||
from email.headerregistry import Address
|
||||
import abc
|
||||
from dataclasses import dataclass
|
||||
from src.utils.secret import SecretBox
|
||||
from src.config.email import EmailAddress
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserProfile:
|
||||
id: int
|
||||
email: EmailAddress
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
id: int
|
||||
email: EmailAddress
|
||||
email_confirmed: bool
|
||||
password_hash: SecretBox[str]
|
||||
|
||||
def to_profile(self) -> UserProfile:
|
||||
return UserProfile(id=self.id, email=self.email)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserDTO:
|
||||
email: EmailAddress
|
||||
raw_password: SecretBox[str]
|
||||
|
||||
|
||||
class LoginError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SignupError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UserRepo(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def create_user(self, user: UserDTO) -> User:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_user_by_id(self, user_id: int) -> User | None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_user_by_email(self, email: EmailAddress) -> User | None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_user(self, user: User):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def auth_as_user(self, user: UserDTO) -> User | None:
|
||||
pass
|
||||
|
||||
|
||||
def is_valid_email(email: str) -> bool:
|
||||
try:
|
||||
parsed = Address(addr_spec=email)
|
||||
return bool(parsed.username and parsed.domain and '.' in parsed.domain)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
def is_valid_password(password: str) -> bool:
|
||||
# TODO: Enforce other or saner rules?
|
||||
long_enough = len(password) > 8
|
||||
short_enough = len(password) < 32
|
||||
has_symbol = any([s in password for s in list('@#$%^&*!?/')])
|
||||
return long_enough and short_enough and has_symbol
|
||||
|
||||
|
||||
class UserService:
|
||||
def __init__(self, repo: UserRepo):
|
||||
self._repo = repo
|
||||
|
||||
def login(self, user: UserDTO) -> UserProfile | None:
|
||||
full_user = self._repo.auth_as_user(user)
|
||||
if full_user:
|
||||
return full_user.to_profile()
|
||||
|
||||
def signup(self, user: UserDTO) -> UserProfile:
|
||||
if not is_valid_email(user.email):
|
||||
raise SignupError(f'{user.email} was not an acceptable email address')
|
||||
|
||||
if not is_valid_password(user.raw_password.expose_secret()):
|
||||
raise SignupError('The given password was not acceptable')
|
||||
|
||||
return self._repo.create_user(user).to_profile()
|
||||
|
||||
def get_user_by_id(self, user_id: int) -> UserProfile | None:
|
||||
user = self._repo.get_user_by_id(user_id)
|
||||
if user:
|
||||
return user.to_profile()
|
||||
5
src/services/users/__init__.py
Normal file
5
src/services/users/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .data import User, UserProfile, UserDTO, SignupError, LoginError
|
||||
from .service import UserService
|
||||
from .repo import UserRepo
|
||||
|
||||
__all__ = ['User', 'UserProfile', 'UserDTO', 'SignupError', 'LoginError', 'UserService', 'UserRepo']
|
||||
34
src/services/users/data.py
Normal file
34
src/services/users/data.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from src.config.email import EmailAddress
|
||||
from dataclasses import dataclass
|
||||
from src.utils.secret import SecretBox
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserProfile:
|
||||
id: int
|
||||
email: EmailAddress
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
id: int
|
||||
email: EmailAddress
|
||||
email_confirmed: bool
|
||||
password_hash: SecretBox[str]
|
||||
|
||||
def to_profile(self) -> UserProfile:
|
||||
return UserProfile(id=self.id, email=self.email)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserDTO:
|
||||
email: EmailAddress
|
||||
raw_password: SecretBox[str]
|
||||
|
||||
|
||||
class LoginError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SignupError(Exception):
|
||||
pass
|
||||
26
src/services/users/repo.py
Normal file
26
src/services/users/repo.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import abc
|
||||
from src.config.email import EmailAddress
|
||||
|
||||
from .data import User, UserDTO
|
||||
|
||||
|
||||
class UserRepo(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def create_user(self, user: UserDTO) -> User:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_user_by_id(self, user_id: int) -> User | None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_user_by_email(self, email: EmailAddress) -> User | None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_user(self, user: User):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def auth_as_user(self, user: UserDTO) -> User | None:
|
||||
pass
|
||||
50
src/services/users/service.py
Normal file
50
src/services/users/service.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from src.services.email import EmailService
|
||||
from email.headerregistry import Address
|
||||
from .data import UserDTO, UserProfile, SignupError, LoginError
|
||||
from .repo import UserRepo
|
||||
|
||||
|
||||
def is_valid_email(email: str) -> bool:
|
||||
try:
|
||||
parsed = Address(addr_spec=email)
|
||||
return bool(parsed.username and parsed.domain and '.' in parsed.domain)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
def is_valid_password(password: str) -> bool:
|
||||
# TODO: Enforce other or saner rules?
|
||||
long_enough = len(password) > 8
|
||||
short_enough = len(password) < 32
|
||||
has_symbol = any([s in password for s in list('@#$%^&*!?/')])
|
||||
return long_enough and short_enough and has_symbol
|
||||
|
||||
|
||||
class UserService:
|
||||
def __init__(self, repo: UserRepo, email_service: EmailService):
|
||||
self._repo = repo
|
||||
self._email_service = email_service
|
||||
|
||||
def login(self, user: UserDTO) -> UserProfile:
|
||||
full_user = self._repo.auth_as_user(user)
|
||||
if full_user:
|
||||
return full_user.to_profile()
|
||||
else:
|
||||
raise LoginError('No user found for that email or password')
|
||||
|
||||
def signup(self, user: UserDTO) -> UserProfile:
|
||||
if not is_valid_email(user.email):
|
||||
raise SignupError(f'{user.email} was not an acceptable email address')
|
||||
|
||||
if not is_valid_password(user.raw_password.expose_secret()):
|
||||
raise SignupError('The given password was not acceptable')
|
||||
|
||||
# Create a user in persistence
|
||||
created_user = self._repo.create_user(user).to_profile()
|
||||
|
||||
return created_user
|
||||
|
||||
def get_user_by_id(self, user_id: int) -> UserProfile | None:
|
||||
user = self._repo.get_user_by_id(user_id)
|
||||
if user:
|
||||
return user.to_profile()
|
||||
Loading…
Add table
Add a link
Reference in a new issue