From c6da84c7f8889079ff92c20023b7fdc5d0142023 Mon Sep 17 00:00:00 2001 From: OhMaley Date: Thu, 26 Dec 2024 11:47:39 -0500 Subject: [PATCH 1/6] Set is_active to False when deleting a user --- src/apps/profiles/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apps/profiles/models.py b/src/apps/profiles/models.py index 66918e173..991b4451f 100644 --- a/src/apps/profiles/models.py +++ b/src/apps/profiles/models.py @@ -207,6 +207,7 @@ def delete(self, *args, **kwargs): # Mark the user as deleted self.is_deleted = True self.deleted_at = now() + self.is_active = False # Anonymize or removed personal data user_email = self.email # keep track of the email for the end of the procedure From f1d29789bdd4cb44f49da3f7e61bd79aa766250b Mon Sep 17 00:00:00 2001 From: OhMaley Date: Thu, 26 Dec 2024 12:05:48 -0500 Subject: [PATCH 2/6] change the email sending method and populate the missing txt files --- src/apps/profiles/views.py | 45 ++++--- .../emails/template_delete_account.txt | 24 ++++ .../template_delete_account_confirmed.txt | 7 ++ .../emails/template_delete_account_notice.txt | 118 ++++++++++++++++++ 4 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 src/templates/profiles/emails/template_delete_account.txt create mode 100644 src/templates/profiles/emails/template_delete_account_confirmed.txt create mode 100644 src/templates/profiles/emails/template_delete_account_notice.txt diff --git a/src/apps/profiles/views.py b/src/apps/profiles/views.py index f5b7f5863..f8c090038 100644 --- a/src/apps/profiles/views.py +++ b/src/apps/profiles/views.py @@ -28,6 +28,7 @@ from datasets.models import Data, DataGroup from tasks.models import Task from forums.models import Post +from utils.email import codalab_send_mail class LoginView(auth_views.LoginView): @@ -110,20 +111,21 @@ def activateEmail(request, user, to_email): def send_delete_account_confirmation_mail(request, user): - mail_subject = 'Confirm Your Account Deletion Request' - message = render_to_string('profiles/emails/template_delete_account.html', { + context = { 'user': user, 'domain': get_current_site(request).domain, 'uid': urlsafe_base64_encode(force_bytes(user.pk)), 'token': default_token_generator.make_token(user), 'protocol': 'https' if request.is_secure() else 'http' - }) - email = EmailMessage(mail_subject, message, to=[user.email]) - if email.send(): - messages.success(request, f'Dear {user.username}, please go to your email inbox and click on \ - the link to complete the deletion process. *Note: Check your spam folder.') - else: - messages.error(request, f'Problem sending confirmation email.') + } + codalab_send_mail( + context_data=context, + subject=f'Confirm Your Account Deletion Request', + html_file="profiles/emails/template_delete_account.html", + text_file="profiles/emails/template_delete_account.txt", + to_email=[user.email] + ) + messages.success(request, f'Dear {user.username}, please go to your email inbox and click on the link to complete the deletion process. *Note: Check your spam folder.') def send_user_deletion_notice_to_admin(user): @@ -140,8 +142,7 @@ def send_user_deletion_notice_to_admin(user): queues = user.queues.all() posts = Post.objects.filter(posted_by=user) - mail_subject = f'Notice: user {user.username} removed his account' - message = render_to_string('profiles/emails/template_delete_account_notice.html', { + context = { 'user': user, 'organizations': organizations, 'competitions_organizer': competitions_organizer, @@ -152,16 +153,24 @@ def send_user_deletion_notice_to_admin(user): 'tasks': tasks, 'queues': queues, 'posts': posts - }) - email = EmailMessage(mail_subject, message, to=admin_emails) - email.send() + } + codalab_send_mail( + context_data=context, + subject=f'Notice: user {user.username} removed his account', + html_file="profiles/emails/template_delete_account_notice.html", + text_file="profiles/emails/template_delete_account_notice.txt", + to_email=admin_emails + ) def send_user_deletion_confirmed(email): - mail_subject = f'Codabench: your account has been successfully removed' - message = render_to_string('profiles/emails/template_delete_account_confirmed.html') - email = EmailMessage(mail_subject, message, to=[email]) - email.send() + codalab_send_mail( + context_data={}, + subject=f'Codabench: your account has been successfully removed', + html_file="profiles/emails/template_delete_account_confirmed.html", + text_file="profiles/emails/template_delete_account_confirmed.txt", + to_email=[email] + ) def delete(request, uidb64, token): diff --git a/src/templates/profiles/emails/template_delete_account.txt b/src/templates/profiles/emails/template_delete_account.txt new file mode 100644 index 000000000..536567411 --- /dev/null +++ b/src/templates/profiles/emails/template_delete_account.txt @@ -0,0 +1,24 @@ +{% extends 'emails/base_email.html' %} + +{% block content %} +

We have received your request to delete your account.

+

To proceed with the deletion of your account, please confirm your request by clicking the link below:

+

{{ protocol }}://{{ domain }}{% url 'accounts:delete' uidb64=uid token=token %}

+ +
+ +

Important Information:

+ + +
+ +

If you did not request this action or have changed your mind, you can safely ignore this email, and your account will remain intact.

+ +
+ +

Thank you for being part of our community.

+{% endblock %} diff --git a/src/templates/profiles/emails/template_delete_account_confirmed.txt b/src/templates/profiles/emails/template_delete_account_confirmed.txt new file mode 100644 index 000000000..690b2dff3 --- /dev/null +++ b/src/templates/profiles/emails/template_delete_account_confirmed.txt @@ -0,0 +1,7 @@ +{% extends 'emails/base_email.html' %} + +{% block content %} +

Your account has been successfully removed. Thank you for being part of our community.

+
+

If you change your mind, you can create a new account at any time. We'd be happy to welcome you back!

+{% endblock %} diff --git a/src/templates/profiles/emails/template_delete_account_notice.txt b/src/templates/profiles/emails/template_delete_account_notice.txt new file mode 100644 index 000000000..374511567 --- /dev/null +++ b/src/templates/profiles/emails/template_delete_account_notice.txt @@ -0,0 +1,118 @@ +{% extends 'emails/base_email.html' %} + +{% block content %} +

User account deletion notice

+ +

You are receiving this notice because your are an administrator of the platform.

+ +
+ +

The following user has removed their account:

+ + +
+ +

Summary

+ + + +

Details

+ +

Organizations the user is part of:

+ + +

Competitions the user is the owner:

+ + +

Competitions the user participated in:

+ + +

Submissions from the user:

+ + +

Data created by the user

+ + +

DataGroups created by the user

+ + +

Tasks created by the user

+ + +

Queues created by the user

+ + +

Posts posted by the user

+ + +{% endblock %} From 84b0f4dec8621fbff83761ba0ccf19511336e464 Mon Sep 17 00:00:00 2001 From: OhMaley Date: Thu, 26 Dec 2024 16:45:34 -0500 Subject: [PATCH 3/6] add a checkbox to show/hide deleted users in the list of participant modal --- src/apps/api/views/competitions.py | 2 +- .../riot/competitions/detail/participant_manager.tag | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/apps/api/views/competitions.py b/src/apps/api/views/competitions.py index cf22b444e..793e4efda 100644 --- a/src/apps/api/views/competitions.py +++ b/src/apps/api/views/competitions.py @@ -795,7 +795,7 @@ class CompetitionParticipantViewSet(ModelViewSet): queryset = CompetitionParticipant.objects.all() serializer_class = CompetitionParticipantSerializer filter_backends = (DjangoFilterBackend, SearchFilter) - filter_fields = ('user__username', 'user__email', 'status', 'competition') + filter_fields = ('user__username', 'user__email', 'status', 'competition', 'user__is_deleted') search_fields = ('user__username', 'user__email',) def get_queryset(self): diff --git a/src/static/riot/competitions/detail/participant_manager.tag b/src/static/riot/competitions/detail/participant_manager.tag index 0043b4a26..55cde959e 100644 --- a/src/static/riot/competitions/detail/participant_manager.tag +++ b/src/static/riot/competitions/detail/participant_manager.tag @@ -12,6 +12,10 @@ +
+ + +
Email all participants
@@ -140,6 +144,11 @@ filters.status = status } + let show_deleted_users = self.refs.participant_show_deleted.checked + if (show_deleted_users !== null && show_deleted_users === false) { + filters.user__is_deleted = show_deleted_users + } + CODALAB.api.get_participants(filters) .done(participants => { self.participants = participants From a847d42bfb38f484b4c21f68fd2d1bd11e8d24b5 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Fri, 10 Jan 2025 10:03:54 +0100 Subject: [PATCH 4/6] Fixed the wrong user being named in the greetings in the email sent to admins upon account deletion --- src/apps/profiles/views.py | 6 ++++-- .../profiles/emails/template_delete_account_notice.html | 6 +++--- .../profiles/emails/template_delete_account_notice.txt | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/apps/profiles/views.py b/src/apps/profiles/views.py index f8c090038..5652dc0b1 100644 --- a/src/apps/profiles/views.py +++ b/src/apps/profiles/views.py @@ -141,9 +141,11 @@ def send_user_deletion_notice_to_admin(user): tasks = Task.objects.filter(created_by=user) queues = user.queues.all() posts = Post.objects.filter(posted_by=user) + deleted_user = user context = { - 'user': user, + 'deleted_user' : user, + 'user': "", 'organizations': organizations, 'competitions_organizer': competitions_organizer, 'competitions_participation': competitions_participation, @@ -156,7 +158,7 @@ def send_user_deletion_notice_to_admin(user): } codalab_send_mail( context_data=context, - subject=f'Notice: user {user.username} removed his account', + subject=f'Notice: user {deleted_user.username} removed his account', html_file="profiles/emails/template_delete_account_notice.html", text_file="profiles/emails/template_delete_account_notice.txt", to_email=admin_emails diff --git a/src/templates/profiles/emails/template_delete_account_notice.html b/src/templates/profiles/emails/template_delete_account_notice.html index 374511567..48ff7dcb8 100644 --- a/src/templates/profiles/emails/template_delete_account_notice.html +++ b/src/templates/profiles/emails/template_delete_account_notice.html @@ -9,9 +9,9 @@

User account deletion notice

The following user has removed their account:

    -
  • id: {{ user.id }}
  • -
  • username: {{ user.username }}
  • -
  • email: {{ user.email }}
  • +
  • id: {{ deleted_user.id }}
  • +
  • username: {{ deleted_user.username }}
  • +
  • email: {{ deleted_user.email }}

diff --git a/src/templates/profiles/emails/template_delete_account_notice.txt b/src/templates/profiles/emails/template_delete_account_notice.txt index 374511567..48ff7dcb8 100644 --- a/src/templates/profiles/emails/template_delete_account_notice.txt +++ b/src/templates/profiles/emails/template_delete_account_notice.txt @@ -9,9 +9,9 @@

The following user has removed their account:

    -
  • id: {{ user.id }}
  • -
  • username: {{ user.username }}
  • -
  • email: {{ user.email }}
  • +
  • id: {{ deleted_user.id }}
  • +
  • username: {{ deleted_user.username }}
  • +
  • email: {{ deleted_user.email }}

From 224d0ffdfff4b23ead3430d1306118d022883c8e Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Fri, 10 Jan 2025 10:13:44 +0100 Subject: [PATCH 5/6] Flake8 fixed --- src/apps/profiles/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/profiles/views.py b/src/apps/profiles/views.py index 5652dc0b1..81f616e19 100644 --- a/src/apps/profiles/views.py +++ b/src/apps/profiles/views.py @@ -144,7 +144,7 @@ def send_user_deletion_notice_to_admin(user): deleted_user = user context = { - 'deleted_user' : user, + 'deleted_user': user, 'user': "", 'organizations': organizations, 'competitions_organizer': competitions_organizer, From 626b10ea321ddb0bb0799b52764478d056037f2c Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Thu, 16 Jan 2025 18:50:44 +0500 Subject: [PATCH 6/6] restricted the usage of deletion link more than one time, used a different deletion token to expire it after deletion, account deletion modal now disappears after clickin the delete my account button --- src/apps/api/views/profiles.py | 2 +- src/apps/profiles/tokens.py | 18 +++++++++++++++++- src/apps/profiles/views.py | 12 +++++------- src/static/riot/profiles/profile_account.tag | 2 ++ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/apps/api/views/profiles.py b/src/apps/api/views/profiles.py index d3a4ce289..494b76a46 100644 --- a/src/apps/api/views/profiles.py +++ b/src/apps/api/views/profiles.py @@ -92,7 +92,7 @@ def delete_account(request): is_username_valid = user.username == request.data["username"] is_password_valid = user.check_password(request.data["password"]) - if(is_username_valid and is_password_valid): + if is_username_valid and is_password_valid: send_delete_account_confirmation_mail(request, user) return Response({ diff --git a/src/apps/profiles/tokens.py b/src/apps/profiles/tokens.py index 13439a6b0..2842f2df4 100644 --- a/src/apps/profiles/tokens.py +++ b/src/apps/profiles/tokens.py @@ -4,7 +4,23 @@ class AccountActivationTokenGenerator(PasswordResetTokenGenerator): def _make_hash_value(self, user, timestamp): - return (six.text_type(user.pk) + six.text_type(timestamp) + six.text_type(user.is_active)) + return ( + six.text_type(user.pk) + + six.text_type(timestamp) + + six.text_type(user.is_active) + ) account_activation_token = AccountActivationTokenGenerator() + + +class AccountDeletionTokenGenerator(PasswordResetTokenGenerator): + def _make_hash_value(self, user, timestamp): + return ( + six.text_type(user.pk) + + six.text_type(timestamp) + + six.text_type(user.is_deleted) + ) + + +account_deletion_token = AccountDeletionTokenGenerator() diff --git a/src/apps/profiles/views.py b/src/apps/profiles/views.py index 81f616e19..938a4ef3e 100644 --- a/src/apps/profiles/views.py +++ b/src/apps/profiles/views.py @@ -11,7 +11,6 @@ from django.shortcuts import render, redirect from django.contrib.auth import views as auth_views from django.contrib.auth import forms as auth_forms -from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.mixins import LoginRequiredMixin from django.template.loader import render_to_string from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode @@ -23,7 +22,7 @@ from .forms import SignUpForm, LoginForm, ActivationForm from .models import User, Organization, Membership from oidc_configurations.models import Auth_Organization -from .tokens import account_activation_token +from .tokens import account_activation_token, account_deletion_token from competitions.models import Competition from datasets.models import Data, DataGroup from tasks.models import Task @@ -115,7 +114,7 @@ def send_delete_account_confirmation_mail(request, user): 'user': user, 'domain': get_current_site(request).domain, 'uid': urlsafe_base64_encode(force_bytes(user.pk)), - 'token': default_token_generator.make_token(user), + 'token': account_deletion_token.make_token(user), 'protocol': 'https' if request.is_secure() else 'http' } codalab_send_mail( @@ -183,15 +182,14 @@ def delete(request, uidb64, token): user = None messages.error(request, f"User not found.") return redirect('accounts:user_account') - if user is not None and default_token_generator.check_token(user, token): + if user is not None and account_deletion_token.check_token(user, token): # Soft delete the user user.delete() - - messages.success(request, f'Your account has been removed !') + messages.success(request, f'Your account has been removed!') return redirect('accounts:logout') else: messages.error(request, f"Confirmation link is invalid or expired.") - return redirect('accounts:user_account') + return redirect('pages:home') def sign_up(request): diff --git a/src/static/riot/profiles/profile_account.tag b/src/static/riot/profiles/profile_account.tag index 851293493..920a55ade 100644 --- a/src/static/riot/profiles/profile_account.tag +++ b/src/static/riot/profiles/profile_account.tag @@ -55,6 +55,7 @@ self.isDeleteAccountSubmitButtonDisabled = true; self.show_modal = selector => $(selector).modal('show'); + self.hide_modal = selector => $(selector).modal('hide'); self.checkFields = function() { const formValues = $('#delete-account-form').form('get values'); @@ -80,6 +81,7 @@ const success = response.success; if (success) { toastr.success(response.message); + self.hide_modal('.delete-account.modal') } else { toastr.error(response.error); }