diff --git a/app/core/auth/cruds_auth.py b/app/core/auth/cruds_auth.py index 905e662187..f59a459c1c 100644 --- a/app/core/auth/cruds_auth.py +++ b/app/core/auth/cruds_auth.py @@ -121,3 +121,31 @@ async def revoke_refresh_token_by_user_id( .values(revoked_on=datetime.now(UTC)), ) await db.flush() + + +async def delete_refresh_token_by_user_id( + db: AsyncSession, + user_id: str, +) -> None: + """Delete a refresh token from database""" + + await db.execute( + delete(models_auth.RefreshToken).where( + models_auth.RefreshToken.user_id == user_id, + ), + ) + await db.flush() + + +async def delete_authorization_token_by_user_id( + db: AsyncSession, + user_id: str, +) -> None: + """Delete a refresh token from database""" + + await db.execute( + delete(models_auth.AuthorizationCode).where( + models_auth.AuthorizationCode.user_id == user_id, + ), + ) + await db.flush() diff --git a/app/core/auth/endpoints_auth.py b/app/core/auth/endpoints_auth.py index 96f236c189..d99e1b3728 100644 --- a/app/core/auth/endpoints_auth.py +++ b/app/core/auth/endpoints_auth.py @@ -21,6 +21,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.auth import cruds_auth, models_auth, schemas_auth +from app.core.auth.user_deleter_auth import AuthUserDeleter from app.core.users import cruds_users, models_users from app.core.utils.config import Settings from app.core.utils.security import ( @@ -51,6 +52,7 @@ tag="Auth", router=router, factory=None, + user_deleter=AuthUserDeleter(), ) templates = Jinja2Templates(directory="assets/templates") diff --git a/app/core/auth/user_deleter_auth.py b/app/core/auth/user_deleter_auth.py new file mode 100644 index 0000000000..01b7f59f9c --- /dev/null +++ b/app/core/auth/user_deleter_auth.py @@ -0,0 +1,23 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.auth import cruds_auth +from app.types.module_user_deleter import ModuleUserDeleter + + +class AuthUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + await cruds_auth.delete_authorization_token_by_user_id( + db=db, + user_id=user_id, + ) + await cruds_auth.delete_refresh_token_by_user_id( + db=db, + user_id=user_id, + ) diff --git a/app/core/core_endpoints/endpoints_core.py b/app/core/core_endpoints/endpoints_core.py index d31b44a493..3dbef10a24 100644 --- a/app/core/core_endpoints/endpoints_core.py +++ b/app/core/core_endpoints/endpoints_core.py @@ -6,6 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.core_endpoints import cruds_core, models_core, schemas_core +from app.core.core_endpoints.user_deleter_core import CoreUserDeleter from app.core.groups.groups_type import AccountType, GroupType from app.core.users import models_users from app.core.utils.config import Settings @@ -26,6 +27,7 @@ tag="Core", router=router, factory=None, + user_deleter=CoreUserDeleter(), ) diff --git a/app/core/core_endpoints/user_deleter_core.py b/app/core/core_endpoints/user_deleter_core.py new file mode 100644 index 0000000000..5a279a270d --- /dev/null +++ b/app/core/core_endpoints/user_deleter_core.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class CoreUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/core/google_api/endpoints_google_api.py b/app/core/google_api/endpoints_google_api.py index aaf2e79849..d4d2adf1f3 100644 --- a/app/core/google_api/endpoints_google_api.py +++ b/app/core/google_api/endpoints_google_api.py @@ -4,6 +4,9 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.google_api.google_api import GoogleAPI +from app.core.google_api.user_deleter_google_api import ( + GoogleAPIUserDeleter, +) from app.core.utils.config import Settings from app.dependencies import ( get_db, @@ -18,6 +21,7 @@ tag="GoogleAPI", router=router, factory=None, + user_deleter=GoogleAPIUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/core/google_api/user_deleter_google_api.py b/app/core/google_api/user_deleter_google_api.py new file mode 100644 index 0000000000..e32289f67a --- /dev/null +++ b/app/core/google_api/user_deleter_google_api.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class GoogleAPIUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/core/groups/cruds_groups.py b/app/core/groups/cruds_groups.py index 675f73e27d..ea6507d085 100644 --- a/app/core/groups/cruds_groups.py +++ b/app/core/groups/cruds_groups.py @@ -114,3 +114,15 @@ async def update_group( .values(**group_update.model_dump(exclude_none=True)), ) await db.flush() + + +async def delete_membership_by_user_id( + user_id: str, + db: AsyncSession, +): + await db.execute( + delete(models_groups.CoreMembership).where( + models_groups.CoreMembership.user_id == user_id, + ), + ) + await db.flush() diff --git a/app/core/groups/endpoints_groups.py b/app/core/groups/endpoints_groups.py index 587f7235dc..e1b07afd0e 100644 --- a/app/core/groups/endpoints_groups.py +++ b/app/core/groups/endpoints_groups.py @@ -13,6 +13,7 @@ from app.core.groups import cruds_groups, models_groups, schemas_groups from app.core.groups.factory_groups import CoreGroupsFactory from app.core.groups.groups_type import GroupType +from app.core.groups.user_deleter_groups import GroupsUserDeleter from app.core.notification.utils_notification import get_topics_restricted_to_group_id from app.core.users import cruds_users from app.dependencies import ( @@ -32,6 +33,7 @@ tag="Groups", router=router, factory=CoreGroupsFactory(), + user_deleter=GroupsUserDeleter(), ) hyperion_security_logger = logging.getLogger("hyperion.security") diff --git a/app/core/groups/user_deleter_groups.py b/app/core/groups/user_deleter_groups.py new file mode 100644 index 0000000000..17db017df2 --- /dev/null +++ b/app/core/groups/user_deleter_groups.py @@ -0,0 +1,19 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.groups import cruds_groups +from app.types.module_user_deleter import ModuleUserDeleter + + +class GroupsUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + await cruds_groups.delete_membership_by_user_id( + user_id=user_id, + db=db, + ) diff --git a/app/core/memberships/endpoints_memberships.py b/app/core/memberships/endpoints_memberships.py index fb530c4754..4550633e04 100644 --- a/app/core/memberships/endpoints_memberships.py +++ b/app/core/memberships/endpoints_memberships.py @@ -12,6 +12,9 @@ schemas_memberships, ) from app.core.memberships.factory_memberships import CoreMembershipsFactory +from app.core.memberships.user_deleter_memberships import ( + MembershipsUserDeleter, +) from app.core.memberships.utils_memberships import validate_user_new_membership from app.core.users import cruds_users, models_users, schemas_users from app.dependencies import ( @@ -31,6 +34,7 @@ tag="Memberships", router=router, factory=CoreMembershipsFactory(), + user_deleter=MembershipsUserDeleter(), ) diff --git a/app/core/memberships/user_deleter_memberships.py b/app/core/memberships/user_deleter_memberships.py new file mode 100644 index 0000000000..e70136adb2 --- /dev/null +++ b/app/core/memberships/user_deleter_memberships.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class MembershipsUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass # We keep the memberships for stats and history purposes diff --git a/app/core/myeclpay/endpoints_myeclpay.py b/app/core/myeclpay/endpoints_myeclpay.py index 0a9ad42e99..6ede9b3999 100644 --- a/app/core/myeclpay/endpoints_myeclpay.py +++ b/app/core/myeclpay/endpoints_myeclpay.py @@ -49,6 +49,7 @@ WalletDeviceStatus, WalletType, ) +from app.core.myeclpay.user_deleter_myeclpay import MyECLPayUserDeleter from app.core.myeclpay.utils_myeclpay import ( LATEST_TOS, QRCODE_EXPIRATION, @@ -98,6 +99,7 @@ router=router, payment_callback=validate_transfer_callback, factory=MyECLPayFactory(), + user_deleter=MyECLPayUserDeleter(), ) templates = Jinja2Templates(directory="assets/templates") diff --git a/app/core/myeclpay/user_deleter_myeclpay.py b/app/core/myeclpay/user_deleter_myeclpay.py new file mode 100644 index 0000000000..09ad5c5df9 --- /dev/null +++ b/app/core/myeclpay/user_deleter_myeclpay.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class MyECLPayUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/core/notification/cruds_notification.py b/app/core/notification/cruds_notification.py index c6503bd301..f8881a2730 100644 --- a/app/core/notification/cruds_notification.py +++ b/app/core/notification/cruds_notification.py @@ -272,3 +272,27 @@ async def get_usernames_by_firebase_tokens( .distinct(), ) return [f"{u.firstname} {u.name}" for u in list(result.scalars().all())] + + +async def delete_topic_membership_by_user_id( + user_id: str, + db: AsyncSession, +): + await db.execute( + delete(models_notification.TopicMembership).where( + models_notification.TopicMembership.user_id == user_id, + ), + ) + await db.flush() + + +async def delete_firebase_devices_by_user_id( + user_id: str, + db: AsyncSession, +): + await db.execute( + delete(models_notification.FirebaseDevice).where( + models_notification.FirebaseDevice.user_id == user_id, + ), + ) + await db.flush() diff --git a/app/core/notification/endpoints_notification.py b/app/core/notification/endpoints_notification.py index bc0e1ebc93..1e88dee7ec 100644 --- a/app/core/notification/endpoints_notification.py +++ b/app/core/notification/endpoints_notification.py @@ -10,6 +10,9 @@ models_notification, schemas_notification, ) +from app.core.notification.user_deleter_notification import ( + NotificationUserDeleter, +) from app.core.notification.utils_notification import get_user_notification_topics from app.core.users import models_users from app.dependencies import ( @@ -42,6 +45,7 @@ router=router, registred_topics=[notification_test_topic], factory=None, + user_deleter=NotificationUserDeleter(), ) diff --git a/app/core/notification/user_deleter_notification.py b/app/core/notification/user_deleter_notification.py new file mode 100644 index 0000000000..ec3600ce8f --- /dev/null +++ b/app/core/notification/user_deleter_notification.py @@ -0,0 +1,23 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.notification import cruds_notification +from app.types.module_user_deleter import ModuleUserDeleter + + +class NotificationUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + await cruds_notification.delete_firebase_devices_by_user_id( + db=db, + user_id=user_id, + ) + await cruds_notification.delete_topic_membership_by_user_id( + db=db, + user_id=user_id, + ) diff --git a/app/core/payment/endpoints_payment.py b/app/core/payment/endpoints_payment.py index b471523f6d..36d93df055 100644 --- a/app/core/payment/endpoints_payment.py +++ b/app/core/payment/endpoints_payment.py @@ -10,9 +10,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.payment import cruds_payment, models_payment, schemas_payment -from app.core.payment.types_payment import ( - NotificationResultContent, -) +from app.core.payment.types_payment import NotificationResultContent +from app.core.payment.user_deleter_payment import PaymentUserDeleter from app.dependencies import get_db from app.module import all_modules from app.types.module import CoreModule @@ -24,6 +23,7 @@ tag="Payments", router=router, factory=None, + user_deleter=PaymentUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/core/payment/user_deleter_payment.py b/app/core/payment/user_deleter_payment.py new file mode 100644 index 0000000000..4b159b8776 --- /dev/null +++ b/app/core/payment/user_deleter_payment.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class PaymentUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/core/schools/endpoints_schools.py b/app/core/schools/endpoints_schools.py index abb01febc1..ac8ce14df5 100644 --- a/app/core/schools/endpoints_schools.py +++ b/app/core/schools/endpoints_schools.py @@ -18,6 +18,7 @@ ) from app.core.schools.factory_schools import CoreSchoolsFactory from app.core.schools.schools_type import SchoolType +from app.core.schools.user_deleter_schools import SchoolsUserDeleter from app.core.users import cruds_users, schemas_users from app.dependencies import ( get_db, @@ -32,6 +33,7 @@ tag="Schools", router=router, factory=CoreSchoolsFactory(), + user_deleter=SchoolsUserDeleter(), ) diff --git a/app/core/schools/user_deleter_schools.py b/app/core/schools/user_deleter_schools.py new file mode 100644 index 0000000000..f4bdeffd79 --- /dev/null +++ b/app/core/schools/user_deleter_schools.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class SchoolsUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/core/users/cruds_users.py b/app/core/users/cruds_users.py index 5863711723..27f95e3724 100644 --- a/app/core/users/cruds_users.py +++ b/app/core/users/cruds_users.py @@ -3,7 +3,7 @@ from collections.abc import Sequence from uuid import UUID -from sqlalchemy import ForeignKey, and_, delete, not_, or_, select, update +from sqlalchemy import ForeignKey, and_, delete, func, not_, or_, select, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from sqlalchemy_utils import get_referencing_foreign_keys @@ -96,6 +96,7 @@ async def get_users( *excluded_account_type_condition, *excluded_group_condition, school_condition, + models_users.CoreUser.deactivated.is_(False), ), ), ) @@ -110,7 +111,10 @@ async def get_user_by_id( result = await db.execute( select(models_users.CoreUser) - .where(models_users.CoreUser.id == user_id) + .where( + models_users.CoreUser.id == user_id, + models_users.CoreUser.deactivated.is_(False), + ) .options( # The group relationship need to be loaded selectinload(models_users.CoreUser.groups), @@ -127,7 +131,10 @@ async def get_user_by_email( result = await db.execute( select(models_users.CoreUser) - .where(models_users.CoreUser.email == email) + .where( + models_users.CoreUser.email == email, + models_users.CoreUser.deactivated.is_(False), + ) .options( # The group relationship need to be loaded to be able # to check if the user is a member of a specific group @@ -144,7 +151,10 @@ async def update_user( ): await db.execute( update(models_users.CoreUser) - .where(models_users.CoreUser.id == user_id) + .where( + models_users.CoreUser.id == user_id, + models_users.CoreUser.deactivated.is_(False), + ) .values(**user_update.model_dump(exclude_none=True)), ) @@ -277,7 +287,10 @@ async def update_user_password_by_id( ): await db.execute( update(models_users.CoreUser) - .where(models_users.CoreUser.id == user_id) + .where( + models_users.CoreUser.id == user_id, + models_users.CoreUser.deactivated.is_(False), + ) .values(password_hash=new_password_hash), ) await db.flush() @@ -418,3 +431,71 @@ async def fusion_users( # Delete the user_deleted await delete_user(db, user_deleted_id) + + +async def count_deactivated_users(db: AsyncSession) -> int: + """Return the number of deactivated users in the database""" + + result = ( + await db.execute( + select(func.count()).where( + models_users.CoreUser.deactivated, + ), + ) + ).scalar() + return result or 0 + + +async def deactivate_user( + db: AsyncSession, + user_id: str, +): + """Deactivate a user in the database""" + count = await count_deactivated_users(db) + + await db.execute( + update(models_users.CoreUser) + .where(models_users.CoreUser.id == user_id) + .values( + deactivated=True, + email=f"deleted.user{count}@myecl.fr", + name="Deleted User", + firstname=str(count), + nickname=None, + floor=None, + phone=None, + promo=None, + birthday=None, + school_id=SchoolType.no_school.value, + account_type=AccountType.external, + ), + ) + await db.commit() + + +async def delete_email_migration_code_by_user_id( + db: AsyncSession, + user_id: str, +): + """Delete a user from database by id""" + + await db.execute( + delete(models_users.CoreUserEmailMigrationCode).where( + models_users.CoreUserEmailMigrationCode.user_id == user_id, + ), + ) + await db.commit() + + +async def delete_recover_request_by_user_id( + db: AsyncSession, + user_id: str, +): + """Delete a user from database by id""" + + await db.execute( + delete(models_users.CoreUserRecoverRequest).where( + models_users.CoreUserRecoverRequest.user_id == user_id, + ), + ) + await db.commit() diff --git a/app/core/users/endpoints_users.py b/app/core/users/endpoints_users.py index a3f7cdb39b..3f3795c9cb 100644 --- a/app/core/users/endpoints_users.py +++ b/app/core/users/endpoints_users.py @@ -27,6 +27,7 @@ from app.core.users import cruds_users, models_users, schemas_users from app.core.users.factory_users import CoreUsersFactory from app.core.users.tools_users import get_account_type_and_school_id_from_email +from app.core.users.user_deleter_users import UsersUserDeleter from app.core.utils import security from app.core.utils.config import Settings from app.dependencies import ( @@ -60,6 +61,7 @@ tag="Users", router=router, factory=CoreUsersFactory(), + user_deleter=UsersUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") @@ -311,7 +313,7 @@ async def create_user( # After adding the unconfirmed user to the database, we got an activation token that need to be send by email, # in order to make sure the email address is valid - account_type, school_id = await get_account_type_and_school_id_from_email( + account_type, _ = await get_account_type_and_school_id_from_email( email=email, db=db, ) diff --git a/app/core/users/models_users.py b/app/core/users/models_users.py index b3b4682323..c14e8d5fda 100644 --- a/app/core/users/models_users.py +++ b/app/core/users/models_users.py @@ -38,6 +38,7 @@ class CoreUser(Base): phone: Mapped[str | None] floor: Mapped[FloorsType | None] created_on: Mapped[datetime | None] + deactivated: Mapped[bool] = mapped_column(default=False) # We use list["CoreGroup"] with quotes as CoreGroup is only defined after this class # Defining CoreUser after CoreGroup would cause a similar issue diff --git a/app/core/users/user_deleter_users.py b/app/core/users/user_deleter_users.py new file mode 100644 index 0000000000..2c49f942bd --- /dev/null +++ b/app/core/users/user_deleter_users.py @@ -0,0 +1,32 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.users import cruds_users +from app.types.module_user_deleter import ModuleUserDeleter +from app.utils.tools import delete_file_from_data + + +class UsersUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + await cruds_users.delete_email_migration_code_by_user_id( + db=db, + user_id=user_id, + ) + await cruds_users.delete_recover_request_by_user_id( + db=db, + user_id=user_id, + ) + await cruds_users.deactivate_user( + db=db, + user_id=user_id, + ) + delete_file_from_data( + directory="profile-pictures", + filename=user_id, + ) diff --git a/app/modules/advert/endpoints_advert.py b/app/modules/advert/endpoints_advert.py index ba973aff1d..82267eb7b6 100644 --- a/app/modules/advert/endpoints_advert.py +++ b/app/modules/advert/endpoints_advert.py @@ -23,6 +23,7 @@ schemas_advert, ) from app.modules.advert.factory_advert import AdvertFactory +from app.modules.advert.user_deleter_advert import AdvertUserDeleter from app.types import standard_responses from app.types.content_type import ContentType from app.types.module import Module @@ -40,6 +41,7 @@ tag="Advert", default_allowed_account_types=[AccountType.student, AccountType.staff], factory=AdvertFactory(), + user_deleter=AdvertUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/modules/advert/user_deleter_advert.py b/app/modules/advert/user_deleter_advert.py new file mode 100644 index 0000000000..b5e863a1ed --- /dev/null +++ b/app/modules/advert/user_deleter_advert.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class AdvertUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/amap/endpoints_amap.py b/app/modules/amap/endpoints_amap.py index 62542cdd67..288655324c 100644 --- a/app/modules/amap/endpoints_amap.py +++ b/app/modules/amap/endpoints_amap.py @@ -21,6 +21,7 @@ from app.modules.amap import cruds_amap, models_amap, schemas_amap from app.modules.amap.factory_amap import AmapFactory from app.modules.amap.types_amap import DeliveryStatusType +from app.modules.amap.user_deleter_amap import AmapUserDeleter from app.types.module import Module from app.utils.communication.notifications import NotificationTool from app.utils.redis import locker_get, locker_set @@ -41,6 +42,7 @@ default_allowed_account_types=[AccountType.student, AccountType.staff], registred_topics=[amap_topic], factory=AmapFactory(), + user_deleter=AmapUserDeleter(), ) hyperion_amap_logger = logging.getLogger("hyperion.amap") diff --git a/app/modules/amap/user_deleter_amap.py b/app/modules/amap/user_deleter_amap.py new file mode 100644 index 0000000000..bfb2ba9719 --- /dev/null +++ b/app/modules/amap/user_deleter_amap.py @@ -0,0 +1,40 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.amap import cruds_amap +from app.modules.amap.types_amap import DeliveryStatusType +from app.types.module_user_deleter import ModuleUserDeleter + + +class AmapUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + # Check if the user has any active orders or a negative balance + reasons = [] + user_cash = await cruds_amap.get_cash_by_id(user_id=user_id, db=db) + if user_cash is not None: + if user_cash.balance < 0: + reasons.append("User has negative balance") + orders = await cruds_amap.get_orders_of_user(user_id=user_id, db=db) + for order in orders: + delivery = await cruds_amap.get_delivery_by_id( + db=db, + delivery_id=order.delivery_id, + ) + if delivery is None: + continue + if delivery.status not in [ + DeliveryStatusType.delivered, + DeliveryStatusType.archived, + ]: + reasons.append( + f"User has order in delivery not delivered or archived: {order.delivery_id}", + ) + if reasons: + return "\n - ".join(reasons) + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/booking/endpoints_booking.py b/app/modules/booking/endpoints_booking.py index bfe9f7072a..da6f5f65bb 100644 --- a/app/modules/booking/endpoints_booking.py +++ b/app/modules/booking/endpoints_booking.py @@ -23,6 +23,7 @@ ) from app.modules.booking.factory_booking import BookingFactory from app.modules.booking.types_booking import Decision +from app.modules.booking.user_deleter_booking import BookingUserDeleter from app.types.module import Module from app.utils.communication.notifications import NotificationTool from app.utils.tools import is_group_id_valid, is_user_member_of_any_group @@ -32,6 +33,7 @@ tag="Booking", default_allowed_account_types=[AccountType.student, AccountType.staff], factory=BookingFactory(), + user_deleter=BookingUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/modules/booking/user_deleter_booking.py b/app/modules/booking/user_deleter_booking.py new file mode 100644 index 0000000000..52f13855b5 --- /dev/null +++ b/app/modules/booking/user_deleter_booking.py @@ -0,0 +1,29 @@ +from datetime import UTC, datetime + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.booking import cruds_booking +from app.types.module_user_deleter import ModuleUserDeleter + + +class BookingUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + user_bookings = await cruds_booking.get_applicant_bookings( + db=db, + applicant_id=user_id, + ) + reasons = [ + f"User has booking in future: {booking.id}" + for booking in user_bookings + if booking.end > datetime.now(tz=UTC) + ] + if reasons: + return "\n - ".join(reasons) + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/calendar/endpoints_calendar.py b/app/modules/calendar/endpoints_calendar.py index 3b69bbee15..ecc8d5a3c6 100644 --- a/app/modules/calendar/endpoints_calendar.py +++ b/app/modules/calendar/endpoints_calendar.py @@ -15,6 +15,7 @@ ) from app.modules.calendar.factory_calendar import CalendarFactory from app.modules.calendar.types_calendar import Decision +from app.modules.calendar.user_deleter_calendar import CalendarUserDeleter from app.types.module import Module from app.utils.tools import is_user_member_of_any_group @@ -23,6 +24,7 @@ tag="Calendar", default_allowed_account_types=[AccountType.student, AccountType.staff], factory=CalendarFactory(), + user_deleter=CalendarUserDeleter(), ) ical_file_path = "data/ics/ae_calendar.ics" diff --git a/app/modules/calendar/user_deleter_calendar.py b/app/modules/calendar/user_deleter_calendar.py new file mode 100644 index 0000000000..dff88a6a7d --- /dev/null +++ b/app/modules/calendar/user_deleter_calendar.py @@ -0,0 +1,29 @@ +from datetime import UTC, datetime + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.calendar import cruds_calendar +from app.types.module_user_deleter import ModuleUserDeleter + + +class CalendarUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + user_events = await cruds_calendar.get_applicant_events( + db=db, + applicant_id=user_id, + ) + reasons = [ + f"User has events in future: {event.id}" + for event in user_events + if event.end > datetime.now(tz=UTC) + ] + if reasons: + return "\n - ".join(reasons) + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/campaign/cruds_campaign.py b/app/modules/campaign/cruds_campaign.py index 4a97aab0a9..be1d791cc5 100644 --- a/app/modules/campaign/cruds_campaign.py +++ b/app/modules/campaign/cruds_campaign.py @@ -330,6 +330,19 @@ async def get_has_voted( return result.scalars().all() +async def delete_user_has_voted( + db: AsyncSession, + user_id: str, +) -> None: + """Delete all votes for a given user.""" + await db.execute( + delete(models_campaign.HasVoted).where( + models_campaign.HasVoted.user_id == user_id, + ), + ) + await db.commit() + + async def get_votes(db: AsyncSession) -> Sequence[models_campaign.Votes]: result = await db.execute(select(models_campaign.Votes)) return result.scalars().all() diff --git a/app/modules/campaign/endpoints_campaign.py b/app/modules/campaign/endpoints_campaign.py index a638310927..16006a0677 100644 --- a/app/modules/campaign/endpoints_campaign.py +++ b/app/modules/campaign/endpoints_campaign.py @@ -24,6 +24,7 @@ ) from app.modules.campaign.factory_campaign import CampaignFactory from app.modules.campaign.types_campaign import ListType, StatusType +from app.modules.campaign.user_deleter_campaign import CampaignUserDeleter from app.types import standard_responses from app.types.content_type import ContentType from app.types.module import Module @@ -38,6 +39,7 @@ tag="Campaign", default_allowed_groups_ids=[GroupType.AE], factory=CampaignFactory(), + user_deleter=CampaignUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/modules/campaign/user_deleter_campaign.py b/app/modules/campaign/user_deleter_campaign.py new file mode 100644 index 0000000000..ce7f65b150 --- /dev/null +++ b/app/modules/campaign/user_deleter_campaign.py @@ -0,0 +1,22 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.campaign import cruds_campaign +from app.types.module_user_deleter import ModuleUserDeleter + + +class CampaignUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + status = await cruds_campaign.get_has_voted(db=db, user_id=user_id) + if len(status) > 0: + return " - User has voted in active campaign, wait for reset" + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + await cruds_campaign.delete_user_has_voted( + db=db, + user_id=user_id, + ) diff --git a/app/modules/cdr/endpoints_cdr.py b/app/modules/cdr/endpoints_cdr.py index 66cb82c025..0b23400623 100644 --- a/app/modules/cdr/endpoints_cdr.py +++ b/app/modules/cdr/endpoints_cdr.py @@ -41,6 +41,7 @@ CdrStatus, DocumentSignatureType, ) +from app.modules.cdr.user_deleter_cdr import CdrUserDeleter from app.modules.cdr.utils_cdr import ( check_request_consistency, construct_dataframe_from_users_purchases, @@ -68,6 +69,7 @@ payment_callback=validate_payment, default_allowed_groups_ids=[GroupType.admin_cdr], factory=None, + user_deleter=CdrUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/modules/cdr/user_deleter_cdr.py b/app/modules/cdr/user_deleter_cdr.py new file mode 100644 index 0000000000..9365056ab9 --- /dev/null +++ b/app/modules/cdr/user_deleter_cdr.py @@ -0,0 +1,28 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.cdr.coredata_cdr import CdrYear +from app.modules.cdr.cruds_cdr import get_payments_by_user_id, get_purchases_by_user_id +from app.types.module_user_deleter import ModuleUserDeleter +from app.utils.tools import get_core_data + + +class CdrUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + reasons = "" + year = await get_core_data(CdrYear, db) + purchases = await get_purchases_by_user_id(db, user_id, year.year) + payments = await get_payments_by_user_id(db, user_id, year.year) + if any(not purchase.validated for purchase in purchases): + reasons += "\n - User has pending purchases" + if sum(payment.total for payment in payments) != sum( + purchase.quantity * purchase.product_variant.price for purchase in purchases + ): + reasons += "\n - User has uneven wallet balance" + return reasons + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/centralassociation/endpoints_centralassociation.py b/app/modules/centralassociation/endpoints_centralassociation.py index c3471032ad..4d559d0bf7 100644 --- a/app/modules/centralassociation/endpoints_centralassociation.py +++ b/app/modules/centralassociation/endpoints_centralassociation.py @@ -1,4 +1,7 @@ from app.core.groups.groups_type import AccountType +from app.modules.centralassociation.user_deleter_centralassociation import ( + CentralassociationUserDeleter, +) from app.types.module import Module module = Module( @@ -6,4 +9,5 @@ tag="Centralassociation", default_allowed_account_types=[AccountType.student, AccountType.staff], factory=None, + user_deleter=CentralassociationUserDeleter(), ) diff --git a/app/modules/centralassociation/user_deleter_centralassociation.py b/app/modules/centralassociation/user_deleter_centralassociation.py new file mode 100644 index 0000000000..2c6936764e --- /dev/null +++ b/app/modules/centralassociation/user_deleter_centralassociation.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class CentralassociationUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/centralisation/endpoints_centralisation.py b/app/modules/centralisation/endpoints_centralisation.py index 62338ca6c4..e809628835 100644 --- a/app/modules/centralisation/endpoints_centralisation.py +++ b/app/modules/centralisation/endpoints_centralisation.py @@ -1,4 +1,7 @@ from app.core.groups.groups_type import AccountType +from app.modules.centralisation.user_deleter_centralisation import ( + CentralisationUserDeleter, +) from app.types.module import Module module = Module( @@ -6,4 +9,5 @@ tag="Centralisation", default_allowed_account_types=[AccountType.student, AccountType.staff], factory=None, + user_deleter=CentralisationUserDeleter(), ) diff --git a/app/modules/centralisation/user_deleter_centralisation.py b/app/modules/centralisation/user_deleter_centralisation.py new file mode 100644 index 0000000000..947954fe9c --- /dev/null +++ b/app/modules/centralisation/user_deleter_centralisation.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class CentralisationUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/cinema/endpoints_cinema.py b/app/modules/cinema/endpoints_cinema.py index 94dccf89c3..6711494881 100644 --- a/app/modules/cinema/endpoints_cinema.py +++ b/app/modules/cinema/endpoints_cinema.py @@ -21,6 +21,7 @@ ) from app.modules.cinema import cruds_cinema, schemas_cinema from app.modules.cinema.factory_cinema import CinemaFactory +from app.modules.cinema.user_deleter_cinema import CinemaUserDeleter from app.types import standard_responses from app.types.content_type import ContentType from app.types.module import Module @@ -48,6 +49,7 @@ default_allowed_account_types=[AccountType.student, AccountType.staff], registred_topics=[cinema_topic], factory=CinemaFactory(), + user_deleter=CinemaUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/modules/cinema/user_deleter_cinema.py b/app/modules/cinema/user_deleter_cinema.py new file mode 100644 index 0000000000..54b665e7a5 --- /dev/null +++ b/app/modules/cinema/user_deleter_cinema.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class CinemaUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/flappybird/cruds_flappybird.py b/app/modules/flappybird/cruds_flappybird.py index 90ce8f209f..4c86753697 100644 --- a/app/modules/flappybird/cruds_flappybird.py +++ b/app/modules/flappybird/cruds_flappybird.py @@ -78,6 +78,18 @@ async def delete_flappybird_best_score( ) +async def delete_flappybird_score( + db: AsyncSession, + user_id: str, +): + """Remove a FlappyBirdScore in database""" + await db.execute( + delete(models_flappybird.FlappyBirdScore).where( + models_flappybird.FlappyBirdScore.user_id == user_id, + ), + ) + + async def update_flappybird_best_score( db: AsyncSession, user_id: str, diff --git a/app/modules/flappybird/endpoints_flappybird.py b/app/modules/flappybird/endpoints_flappybird.py index 78a0a7eb59..b1dace378a 100644 --- a/app/modules/flappybird/endpoints_flappybird.py +++ b/app/modules/flappybird/endpoints_flappybird.py @@ -12,6 +12,7 @@ models_flappybird, schemas_flappybird, ) +from app.modules.flappybird.user_deleter_flappybird import FlappybirdUserDeleter from app.types.module import Module module = Module( @@ -19,6 +20,7 @@ tag="Flappy Bird", default_allowed_account_types=[AccountType.student], factory=None, + user_deleter=FlappybirdUserDeleter(), ) diff --git a/app/modules/flappybird/user_deleter_flappybird.py b/app/modules/flappybird/user_deleter_flappybird.py new file mode 100644 index 0000000000..3f9dc1b03a --- /dev/null +++ b/app/modules/flappybird/user_deleter_flappybird.py @@ -0,0 +1,26 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.flappybird.cruds_flappybird import ( + delete_flappybird_best_score, + delete_flappybird_score, +) +from app.types.module_user_deleter import ModuleUserDeleter + + +class FlappybirdUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + await delete_flappybird_best_score( + db=db, + user_id=user_id, + ) + await delete_flappybird_score( + db=db, + user_id=user_id, + ) diff --git a/app/modules/home/endpoints_home.py b/app/modules/home/endpoints_home.py index ed46d295cb..f2294f782c 100644 --- a/app/modules/home/endpoints_home.py +++ b/app/modules/home/endpoints_home.py @@ -1,4 +1,5 @@ from app.core.groups.groups_type import AccountType +from app.modules.home.user_deleter_home import HomeUserDeleter from app.types.module import Module module = Module( @@ -6,4 +7,5 @@ tag="Home", default_allowed_account_types=[AccountType.student, AccountType.staff], factory=None, + user_deleter=HomeUserDeleter(), ) diff --git a/app/modules/home/user_deleter_home.py b/app/modules/home/user_deleter_home.py new file mode 100644 index 0000000000..d02419613d --- /dev/null +++ b/app/modules/home/user_deleter_home.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class HomeUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/loan/endpoints_loan.py b/app/modules/loan/endpoints_loan.py index 9874bd4fb9..2f8967a9a2 100644 --- a/app/modules/loan/endpoints_loan.py +++ b/app/modules/loan/endpoints_loan.py @@ -18,6 +18,7 @@ ) from app.modules.loan import cruds_loan, models_loan, schemas_loan from app.modules.loan.factory_loan import LoanFactory +from app.modules.loan.user_deleter_loan import LoanUserDeleter from app.types.module import Module from app.types.scheduler import Scheduler from app.utils.communication.notifications import NotificationTool @@ -36,6 +37,7 @@ tag="Loans", default_allowed_account_types=[AccountType.student, AccountType.staff], factory=LoanFactory(), + user_deleter=LoanUserDeleter(), ) diff --git a/app/modules/loan/user_deleter_loan.py b/app/modules/loan/user_deleter_loan.py new file mode 100644 index 0000000000..63343c0bdd --- /dev/null +++ b/app/modules/loan/user_deleter_loan.py @@ -0,0 +1,19 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.loan.cruds_loan import get_loans_by_borrower +from app.types.module_user_deleter import ModuleUserDeleter + + +class LoanUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + loans = await get_loans_by_borrower(db, user_id) + if any(not loan.returned for loan in loans): + return "\n - User has pending loans" + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/myeclpay/endpoints_myeclpay.py b/app/modules/myeclpay/endpoints_myeclpay.py index 0b031e976e..47e1e60575 100644 --- a/app/modules/myeclpay/endpoints_myeclpay.py +++ b/app/modules/myeclpay/endpoints_myeclpay.py @@ -1,4 +1,5 @@ from app.core.groups.groups_type import AccountType +from app.modules.myeclpay.user_deleter_myeclpay import MyECLPayUserDeleter from app.types.module import Module module = Module( @@ -6,4 +7,5 @@ tag="MyECLPay", default_allowed_account_types=[AccountType.student, AccountType.staff], factory=None, + user_deleter=MyECLPayUserDeleter(), ) diff --git a/app/modules/myeclpay/user_deleter_myeclpay.py b/app/modules/myeclpay/user_deleter_myeclpay.py new file mode 100644 index 0000000000..09ad5c5df9 --- /dev/null +++ b/app/modules/myeclpay/user_deleter_myeclpay.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class MyECLPayUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/ph/endpoints_ph.py b/app/modules/ph/endpoints_ph.py index b23f87450f..5b7fe55603 100644 --- a/app/modules/ph/endpoints_ph.py +++ b/app/modules/ph/endpoints_ph.py @@ -17,6 +17,7 @@ is_user_in, ) from app.modules.ph import cruds_ph, models_ph, schemas_ph +from app.modules.ph.user_deleter_ph import PHUserDeleter from app.types.content_type import ContentType from app.types.module import Module from app.types.scheduler import Scheduler @@ -43,6 +44,7 @@ default_allowed_account_types=[AccountType.student], registred_topics=[ph_topic], factory=None, + user_deleter=PHUserDeleter(), ) diff --git a/app/modules/ph/user_deleter_ph.py b/app/modules/ph/user_deleter_ph.py new file mode 100644 index 0000000000..87670e7ef5 --- /dev/null +++ b/app/modules/ph/user_deleter_ph.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class PHUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/phonebook/endpoints_phonebook.py b/app/modules/phonebook/endpoints_phonebook.py index f4406569ed..d0d6af07f4 100644 --- a/app/modules/phonebook/endpoints_phonebook.py +++ b/app/modules/phonebook/endpoints_phonebook.py @@ -20,6 +20,7 @@ ) from app.modules.phonebook.factory_phonebook import PhonebookFactory from app.modules.phonebook.types_phonebook import RoleTags +from app.modules.phonebook.user_deleter_phonebook import PhonebookUserDeleter from app.types import standard_responses from app.types.content_type import ContentType from app.types.module import Module @@ -34,6 +35,7 @@ tag="Phonebook", default_allowed_account_types=[AccountType.student, AccountType.staff], factory=PhonebookFactory(), + user_deleter=PhonebookUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/modules/phonebook/user_deleter_phonebook.py b/app/modules/phonebook/user_deleter_phonebook.py new file mode 100644 index 0000000000..ca4e450ad9 --- /dev/null +++ b/app/modules/phonebook/user_deleter_phonebook.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class PhonebookUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/purchases/endpoints_purchases.py b/app/modules/purchases/endpoints_purchases.py index 2891eb86dd..f7f4fdefd8 100644 --- a/app/modules/purchases/endpoints_purchases.py +++ b/app/modules/purchases/endpoints_purchases.py @@ -1,4 +1,7 @@ from app.core.groups.groups_type import AccountType +from app.modules.purchases.user_deleter_purchases import ( + PurchasesUserDeleter, +) from app.types.module import Module module = Module( @@ -6,4 +9,5 @@ tag="Purchases", default_allowed_account_types=[AccountType.student, AccountType.external], factory=None, + user_deleter=PurchasesUserDeleter(), ) diff --git a/app/modules/purchases/user_deleter_purchases.py b/app/modules/purchases/user_deleter_purchases.py new file mode 100644 index 0000000000..dfba9cbcf1 --- /dev/null +++ b/app/modules/purchases/user_deleter_purchases.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class PurchasesUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/raffle/endpoints_raffle.py b/app/modules/raffle/endpoints_raffle.py index 500f106ae1..42ce01b8da 100644 --- a/app/modules/raffle/endpoints_raffle.py +++ b/app/modules/raffle/endpoints_raffle.py @@ -19,6 +19,7 @@ ) from app.modules.raffle import cruds_raffle, models_raffle, schemas_raffle from app.modules.raffle.types_raffle import RaffleStatusType +from app.modules.raffle.user_deleter_raffle import RaffleUserDeleter from app.types import standard_responses from app.types.content_type import ContentType from app.types.module import Module @@ -34,6 +35,7 @@ tag="Raffle", default_allowed_account_types=[AccountType.student, AccountType.staff], factory=None, + user_deleter=RaffleUserDeleter(), ) hyperion_raffle_logger = logging.getLogger("hyperion.raffle") diff --git a/app/modules/raffle/user_deleter_raffle.py b/app/modules/raffle/user_deleter_raffle.py new file mode 100644 index 0000000000..094d986c91 --- /dev/null +++ b/app/modules/raffle/user_deleter_raffle.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class RaffleUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/raid/endpoints_raid.py b/app/modules/raid/endpoints_raid.py index 9b866fd244..9776c92476 100644 --- a/app/modules/raid/endpoints_raid.py +++ b/app/modules/raid/endpoints_raid.py @@ -19,6 +19,7 @@ ) from app.modules.raid import coredata_raid, cruds_raid, models_raid, schemas_raid from app.modules.raid.raid_type import DocumentType, DocumentValidation, Size +from app.modules.raid.user_deleter_raid import RaidUserDeleter from app.modules.raid.utils.utils_raid import ( calculate_raid_payment, get_all_security_files_zip, @@ -48,6 +49,7 @@ payment_callback=validate_payment, default_allowed_account_types=[AccountType.student, AccountType.staff], factory=None, + user_deleter=RaidUserDeleter(), ) diff --git a/app/modules/raid/user_deleter_raid.py b/app/modules/raid/user_deleter_raid.py new file mode 100644 index 0000000000..a36fe12910 --- /dev/null +++ b/app/modules/raid/user_deleter_raid.py @@ -0,0 +1,18 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.raid.cruds_raid import is_user_a_participant +from app.types.module_user_deleter import ModuleUserDeleter + + +class RaidUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + if await is_user_a_participant(user_id, db) is not None: + return "\n - User is a participant in the current edition" + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index ff8b96c447..5ed668273f 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -18,6 +18,9 @@ schemas_recommendation, ) from app.modules.recommendation.factory_recommendation import RecommendationFactory +from app.modules.recommendation.user_deleter_recommendation import ( + RecommendationUserDeleter, +) from app.types import standard_responses from app.types.content_type import ContentType from app.types.module import Module @@ -31,6 +34,7 @@ tag="Recommendation", default_allowed_account_types=[AccountType.student, AccountType.staff], factory=RecommendationFactory(), + user_deleter=RecommendationUserDeleter(), ) diff --git a/app/modules/recommendation/user_deleter_recommendation.py b/app/modules/recommendation/user_deleter_recommendation.py new file mode 100644 index 0000000000..05a522059a --- /dev/null +++ b/app/modules/recommendation/user_deleter_recommendation.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class RecommendationUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/seed_library/endpoints_seed_library.py b/app/modules/seed_library/endpoints_seed_library.py index d979ecf88e..85f9cec071 100644 --- a/app/modules/seed_library/endpoints_seed_library.py +++ b/app/modules/seed_library/endpoints_seed_library.py @@ -19,6 +19,9 @@ ) from app.modules.seed_library.factory_seed_library import SeedLibraryFactory from app.modules.seed_library.types_seed_library import PlantState, SpeciesType +from app.modules.seed_library.user_deleter_seed_library import ( + SeedLibraryUserDeleter, +) from app.types.module import Module from app.utils import tools from app.utils.tools import is_user_member_of_any_group @@ -28,6 +31,7 @@ tag="seed_library", default_allowed_account_types=[AccountType.student, AccountType.staff], factory=SeedLibraryFactory(), + user_deleter=SeedLibraryUserDeleter(), ) diff --git a/app/modules/seed_library/user_deleter_seed_library.py b/app/modules/seed_library/user_deleter_seed_library.py new file mode 100644 index 0000000000..731fbb1e52 --- /dev/null +++ b/app/modules/seed_library/user_deleter_seed_library.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class SeedLibraryUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/sport_competition/endpoints_sport_competition.py b/app/modules/sport_competition/endpoints_sport_competition.py index 55dac2fcc2..08439a84a9 100644 --- a/app/modules/sport_competition/endpoints_sport_competition.py +++ b/app/modules/sport_competition/endpoints_sport_competition.py @@ -28,6 +28,9 @@ ExcelExportParams, ProductSchoolType, ) +from app.modules.sport_competition.user_deleter_sport_competition import ( + SportCompetitionUserDeleter, +) from app.modules.sport_competition.utils.data_exporter import ( construct_users_excel_with_parameters, ) @@ -57,6 +60,7 @@ default_allowed_account_types=get_account_types_except_externals(), payment_callback=validate_payment, factory=None, + user_deleter=SportCompetitionUserDeleter(), ) # region: Sport diff --git a/app/modules/sport_competition/user_deleter_sport_competition.py b/app/modules/sport_competition/user_deleter_sport_competition.py new file mode 100644 index 0000000000..7e68eafb06 --- /dev/null +++ b/app/modules/sport_competition/user_deleter_sport_competition.py @@ -0,0 +1,29 @@ +from datetime import UTC, datetime + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.sport_competition import cruds_sport_competition +from app.types.module_user_deleter import ModuleUserDeleter + + +class SportCompetitionUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + edition = await cruds_sport_competition.load_active_edition(db) + if edition is not None and edition.end_date >= datetime.now(tz=UTC): + competition_user = ( + await cruds_sport_competition.load_competition_user_by_id( + user_id, + edition.id, + db, + ) + ) + if competition_user is not None: + return " - User is registered in an active sport competition edition" + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/types/module.py b/app/types/module.py index 185e3c60fa..262b711811 100644 --- a/app/types/module.py +++ b/app/types/module.py @@ -7,6 +7,7 @@ from app.core.notification.schemas_notification import Topic from app.core.payment import schemas_payment from app.types.factory import Factory +from app.types.module_user_deleter import ModuleUserDeleter class CoreModule: @@ -15,6 +16,7 @@ def __init__( root: str, tag: str, factory: Factory | None, + user_deleter: ModuleUserDeleter, router: APIRouter | None = None, payment_callback: Callable[ [schemas_payment.CheckoutPayment, AsyncSession], @@ -28,12 +30,14 @@ def __init__( :param root: the root of the module, used by Titan :param tag: the tag of the module, used by FastAPI :param factory: a factory to use to create fake data for the module (development purpose) + :param user_deleter: a ModuleUserDeleter to handle user deletion :param router: an optional custom APIRouter :param payment_callback: an optional method to call when a payment is notified by HelloAsso. A CheckoutPayment and the database will be provided during the call :param registred_topics: an optionnal list of Topics that should be registered by the module. Modules can also register topics dynamically. Once the Topic was registred, removing it from this list won't delete it """ self.root = root + self.user_deleter = user_deleter self.router = router or APIRouter(tags=[tag]) self.payment_callback: ( Callable[[schemas_payment.CheckoutPayment, AsyncSession], Awaitable[None]] @@ -49,6 +53,7 @@ def __init__( root: str, tag: str, factory: Factory | None, + user_deleter: ModuleUserDeleter, default_allowed_groups_ids: list[GroupType] | None = None, default_allowed_account_types: list[AccountType] | None = None, router: APIRouter | None = None, @@ -64,6 +69,7 @@ def __init__( :param root: the root of the module, used by Titan :param tag: the tag of the module, used by FastAPI :param factory: a factory to use to create fake data for the module (development purpose) + :param user_deleter: a ModuleUserDeleter to handle user deletion :param default_allowed_groups_ids: list of groups that should be able to see the module by default :param default_allowed_account_types: list of account_types that should be able to see the module by default :param router: an optional custom APIRouter @@ -72,6 +78,7 @@ def __init__( Once the Topic was registred, removing it from this list won't delete it """ self.root = root + self.user_deleter = user_deleter self.default_allowed_groups_ids = default_allowed_groups_ids self.default_allowed_account_types = default_allowed_account_types self.router = router or APIRouter(tags=[tag]) diff --git a/app/types/module_user_deleter.py b/app/types/module_user_deleter.py new file mode 100644 index 0000000000..8d38ff5f4d --- /dev/null +++ b/app/types/module_user_deleter.py @@ -0,0 +1,48 @@ +from abc import ABC, abstractmethod + +from sqlalchemy.ext.asyncio import AsyncSession + + +class ModuleUserDeleter(ABC): + """ + Abstract base class for user deletion functionality. + This class defines the interface for deleting users from the system. + Each module should implement this interface to provide its own user deletion logic. + """ + + @abstractmethod + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + """ + Check if the user can be deleted. + :param user_id: The ID of the user to check. + :return: True if the user can be deleted, False otherwise. + """ + + @abstractmethod + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + """ + Delete the user from the system. + :param user_id: The ID of the user to delete. + """ + + +""" +from app.types.module_user_deleter import ModuleUserDeleter + +from sqlalchemy.ext.asyncio import AsyncSession + + +class CoreUserDeleter(ModuleUserDeleter): + async def can_delete_user(self, user_id: str, db: AsyncSession) -> Literal[True] | str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass + + +() +""" diff --git a/tests/core/test_payment.py b/tests/core/test_payment.py index b670855c0a..e842cfe8ba 100644 --- a/tests/core/test_payment.py +++ b/tests/core/test_payment.py @@ -20,6 +20,7 @@ from app.core.payment.types_payment import HelloAssoConfig, HelloAssoConfigName from app.core.schools import schemas_schools from app.core.users import schemas_users +from app.modules.home.user_deleter_home import HomeUserDeleter from app.types.module import Module from tests.commons import ( MockedPaymentTool, @@ -306,6 +307,7 @@ async def test_webhook_payment_callback( default_allowed_groups_ids=[], payment_callback=callback, factory=None, + user_deleter=HomeUserDeleter(), ) mocker.patch( "app.core.payment.endpoints_payment.all_modules", @@ -348,6 +350,7 @@ async def test_webhook_payment_callback_fail( default_allowed_groups_ids=[], payment_callback=callback, factory=None, + user_deleter=HomeUserDeleter(), ) mocker.patch( "app.core.payment.endpoints_payment.all_modules",