diff --git a/py/core/base/providers/email.py b/py/core/base/providers/email.py index 16fc94304..7a6568460 100644 --- a/py/core/base/providers/email.py +++ b/py/core/base/providers/email.py @@ -17,6 +17,7 @@ class EmailConfig(ProviderConfig): sendgrid_api_key: Optional[str] = None verify_email_template_id: Optional[str] = None reset_password_template_id: Optional[str] = None + password_changed_template_id: Optional[str] = None frontend_url: Optional[str] = None sender_name: Optional[str] = None @@ -74,3 +75,12 @@ async def send_password_reset_email( self, to_email: str, reset_token: str, *args, **kwargs ) -> None: pass + + @abstractmethod + async def send_password_changed_email( + self, + to_email: str, + *args, + **kwargs, + ) -> None: + pass diff --git a/py/core/providers/auth/r2r_auth.py b/py/core/providers/auth/r2r_auth.py index 0c5094d63..5be2a5eff 100644 --- a/py/core/providers/auth/r2r_auth.py +++ b/py/core/providers/auth/r2r_auth.py @@ -296,7 +296,9 @@ async def send_verification_email( ) await self.email_provider.send_verification_email( - user.email, verification_code, {"first_name": first_name} + to_email=user.email, + verification_code=verification_code, + dynamic_template_data={"first_name": first_name} ) return verification_code, expiry @@ -424,6 +426,14 @@ async def change_password( id=user.id, new_hashed_password=hashed_new_password, ) + try: + await self.email_provider.send_password_changed_email( + to_email=user.email, + dynamic_template_data={"first_name": user.name.split(" ")[0] or 'User'} + ) + except Exception as e: + logger.error(f"Failed to send password change notification: {str(e)}") + return {"message": "Password changed successfully"} async def request_password_reset(self, email: str) -> dict[str, str]: @@ -446,7 +456,9 @@ async def request_password_reset(self, email: str) -> dict[str, str]: user.name.split(" ")[0] if user.name else email.split("@")[0] ) await self.email_provider.send_password_reset_email( - email, reset_token, {"first_name": first_name} + to_email=email, + reset_token=reset_token, + dynamic_template_data={"first_name": first_name} ) return { @@ -482,6 +494,19 @@ async def confirm_password_reset( await self.database_provider.users_handler.remove_reset_token( id=user_id ) + # Get the user information + user = await self.database_provider.users_handler.get_user_by_id( + id=user_id + ) + + try: + await self.email_provider.send_password_changed_email( + to_email=user.email, + dynamic_template_data={"first_name": user.name.split(" ")[0] or 'User'} + ) + except Exception as e: + logger.error(f"Failed to send password change notification: {str(e)}") + return {"message": "Password reset successfully"} async def logout(self, token: str) -> dict[str, str]: diff --git a/py/core/providers/email/console_mock.py b/py/core/providers/email/console_mock.py index 7bca0025b..37a3f5880 100644 --- a/py/core/providers/email/console_mock.py +++ b/py/core/providers/email/console_mock.py @@ -56,3 +56,19 @@ async def send_password_reset_email( ----------------------------- """ ) + + async def send_password_changed_email( + self, to_email: str, *args, **kwargs + ) -> None: + logger.info( + f""" + -------- Email Message -------- + To: {to_email} + Subject: Your Password Has Been Changed + Body: + Your password has been successfully changed. + + For security reasons, you will need to log in again on all your devices. + ----------------------------- + """ + ) diff --git a/py/core/providers/email/sendgrid.py b/py/core/providers/email/sendgrid.py index 5845ac234..646c2a767 100644 --- a/py/core/providers/email/sendgrid.py +++ b/py/core/providers/email/sendgrid.py @@ -37,6 +37,10 @@ def __init__(self, config: EmailConfig): config.reset_password_template_id or os.getenv("SENDGRID_RESET_TEMPLATE_ID") ) + self.password_changed_template_id = ( + config.password_changed_template_id + or os.getenv("SENDGRID_PASSWORD_CHANGED_TEMPLATE_ID") + ) self.client = SendGridAPIClient(api_key=self.api_key) self.sender_name = config.sender_name @@ -208,3 +212,43 @@ async def send_password_reset_email( ) logger.error(error_msg) raise RuntimeError(error_msg) from e + + async def send_password_changed_email( + self, + to_email: str, + dynamic_template_data: Optional[dict] = None, + *args, + **kwargs + ) -> None: + try: + if hasattr(self, 'password_changed_template_id') and self.password_changed_template_id: + await self.send_email( + to_email=to_email, + template_id=self.password_changed_template_id, + dynamic_template_data=dynamic_template_data, + ) + else: + subject = "Your Password Has Been Changed" + body = """ + Your password has been successfully changed. + + If you did not make this change, please contact support immediately and secure your account. + + """ + html_body = """ +
Your password has been successfully changed.
+Your password has been successfully changed.
+