From bdf74c2dbd22d969c2044ad36c6922beec2e3d0b Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Mon, 24 Aug 2026 00:12:29 +0900 Subject: [PATCH] Update users to use the new CRUD types --- src/infra/users.py | 62 ++++++++++++++--------------------- src/services/users/repo.py | 15 ++------- src/services/users/service.py | 16 ++++++--- 3 files changed, 37 insertions(+), 56 deletions(-) diff --git a/src/infra/users.py b/src/infra/users.py index f358acb..a01e4bc 100644 --- a/src/infra/users.py +++ b/src/infra/users.py @@ -6,7 +6,7 @@ from src.config.email import EmailAddress from sqlalchemy import String, VARCHAR, Engine, select from sqlalchemy.orm import mapped_column, Mapped, Session, relationship from argon2 import PasswordHasher -from src.infra.db import Base +from src.infra.db import Base, CRUDRepo from src.services.users.data import UserDTO, User from src.services.users.repo import UserRepo @@ -33,27 +33,31 @@ def model_to_user(db_user: UserModel) -> User: ) -class UserRepoImpl(UserRepo): +def mutate_user(db_user: UserModel, user: User): + # These fields can be set directly + db_user.email_confirmed = user.email_confirmed + db_user.password_hash = user.password_hash.expose_secret() + + # If the email is being updated then it should not be considered confirmed. + if db_user.email != user.email: + db_user.email = user.email + db_user.email_confirmed = False + + +def make_create_user(hasher: PasswordHasher): + def create_user(user: UserDTO) -> UserModel: + pw = hasher.hash(user.raw_password.expose_secret()) + return UserModel(email=user.email, password_hash=pw) + + return create_user + + +class UserRepoImpl(CRUDRepo[UserModel, User, UserDTO], UserRepo): def __init__(self, db: Engine): - self._db = db - self._hasher = PasswordHasher() + hasher = PasswordHasher() + super().__init__(db, UserModel, model_to_user, mutate_user, make_create_user(hasher)) self._logger = logging.getLogger('UserRepoImpl') - - def create_user(self, user: UserDTO) -> User: - pw = self._hasher.hash(user.raw_password.expose_secret()) - db_user = UserModel(email=user.email, password_hash=pw) - with Session(self._db) as session: - session.add(db_user) - session.commit() - - return model_to_user(db_user) - - def get_user_by_id(self, user_id: int) -> User | None: - with Session(self._db) as session: - user = session.get(UserModel, user_id) - - if user: - return model_to_user(user) + self._hasher = hasher def get_user_by_email(self, email: EmailAddress) -> User | None: with Session(self._db) as session: @@ -62,24 +66,6 @@ class UserRepoImpl(UserRepo): if user: return model_to_user(user) - def update_user(self, user: User): - with Session(self._db) as session: - db_user = session.get(UserModel, user.id) - if db_user is None: - self._logger.warning(f'Attempted to update non-existing user {user.id}') - return - - # These fields can be set directly - db_user.email_confirmed = user.email_confirmed - db_user.password_hash = user.password_hash.expose_secret() - - # If the email is being updated then it should not be considered confirmed. - if db_user.email != user.email: - db_user.email = user.email - db_user.email_confirmed = False - - session.commit() - def auth_as_user(self, user: UserDTO) -> User | None: full_user = self.get_user_by_email(user.email) if full_user is None: diff --git a/src/services/users/repo.py b/src/services/users/repo.py index f50836b..6f8f40c 100644 --- a/src/services/users/repo.py +++ b/src/services/users/repo.py @@ -1,26 +1,15 @@ +from src.infra.db import CRUD 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 - +class UserRepo(CRUD[User, UserDTO]): @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 diff --git a/src/services/users/service.py b/src/services/users/service.py index d481cc0..ca916b6 100644 --- a/src/services/users/service.py +++ b/src/services/users/service.py @@ -65,14 +65,20 @@ class UserService: if not claim or claim.sub != user_id: raise EmailConfirmationTokenInvalid - user = self._repo.get_user_by_id(user_id) + user = self._repo.get(user_id) # If there is no user or the users emails don't match or the user is already confirmed then consider # the token invalid for this request. if not user or user.email != claim.email or user.email_confirmed: raise EmailConfirmationTokenInvalid - user.email_confirmed = True - self._repo.update_user(user) + def update(user: User) -> User: + user.email_confirmed = True + return user + + self._repo.update( + user_id, + update, + ) def _send_confirmation_email(self, user: User): env = Environment(loader=PackageLoader('src'), autoescape=select_autoescape()) @@ -96,13 +102,13 @@ class UserService: raise SignupError('The given password was not acceptable') # Create a user in persistence - created_user = self._repo.create_user(user) + created_user = self._repo.create(user, None) # send a confirmation email self._send_confirmation_email(created_user) return created_user.to_profile() def get_user_by_id(self, user_id: int) -> UserProfile | None: - user = self._repo.get_user_by_id(user_id) + user = self._repo.get(user_id) if user: return user.to_profile()