Skip to content

Commit

Permalink
add emails template + soft delete + account view + deletion confirmat…
Browse files Browse the repository at this point in the history
…ion view
  • Loading branch information
OhMaley committed Dec 4, 2024
1 parent 5d7ef98 commit 58c70ad
Show file tree
Hide file tree
Showing 12 changed files with 443 additions and 5 deletions.
3 changes: 3 additions & 0 deletions src/apps/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
path('delete_unused_submissions/', quota.delete_unused_submissions, name="delete_unused_submissions"),
path('delete_failed_submissions/', quota.delete_failed_submissions, name="delete_failed_submissions"),

# User account
path('delete_account/', profiles.delete_account, name="delete_account"),

# Analytics
path('analytics/storage_usage_history/', analytics.storage_usage_history, name='storage_usage_history'),
path('analytics/competitions_usage/', analytics.competitions_usage, name='competitions_usage'),
Expand Down
25 changes: 24 additions & 1 deletion src/apps/api/views/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.http import HttpResponse
from rest_framework.decorators import action
from rest_framework.decorators import action, api_view
from rest_framework.exceptions import ValidationError, PermissionDenied
from rest_framework.generics import GenericAPIView, RetrieveAPIView
from rest_framework import permissions, mixins
Expand All @@ -19,6 +19,8 @@
OrganizationSerializer, MembershipSerializer, SimpleOrganizationSerializer, DeleteMembershipSerializer
from profiles.helpers import send_mail
from profiles.models import Organization, Membership
from profiles.views import send_delete_account_confirmation_mail
from utils.email import codalab_send_mail

User = get_user_model()

Expand Down Expand Up @@ -84,6 +86,27 @@ def _get_data(user):
)


@api_view(['DELETE'])
def delete_account(request):
# Check data
user = request.user
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):
send_delete_account_confirmation_mail(request, user)

return Response({
"success": True,
"message": "A confirmation link has been sent to your email. Follow the instruction to finish the process"
})
else:
return Response({
"success": False,
"error": "Wrong username or password"
})


class OrganizationViewSet(mixins.CreateModelMixin,
mixins.UpdateModelMixin,
GenericViewSet):
Expand Down
41 changes: 38 additions & 3 deletions src/apps/profiles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,45 @@ def delete(self, *args, **kwargs):
self.is_deleted = True
self.deleted_at = now()

# Anonymize personal data
# TODO add all personal data that needs to be anonymized
self.email = f"deleted_{uuid.uuid4()}@domain.com"
# Anonymize or removed personal data

# Github related
self.github_uid = None
self.avatar_url = None
self.url = None
self.html_url = None
self.name = None
self.company = None
self.bio = None
if self.github_info:
self.github_info.login = None
self.github_info.avatar_url = None
self.github_info.gravatar_id = None
self.github_info.html_url = None
self.github_info.name = None
self.github_info.company = None
self.github_info.bio = None
self.github_info.location = None

# Any user attribute
self.username = f"deleted_user_{self.id}"
self.slug = f"deleted_slug_{self.id}"
self.photo = None
self.email = None
self.display_name = None
self.first_name = None
self.last_name = None
self.title = None
self.location = None
self.biography = None
self.personal_url = None
self.linkedin_url = None
self.twitter_url = None
self.github_url = None

# Queues
self.rabbitmq_username = None
self.rabbitmq_password = None

# Save the changes
self.save()
Expand Down
2 changes: 2 additions & 0 deletions src/apps/profiles/urls_accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/<uidb64>/<token>/', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
path('user/<slug:username>/account/', views.UserAccountView.as_view(), name="user_account"),
path('delete/<uidb64>/<token>', views.delete, name='delete'),
]
98 changes: 98 additions & 0 deletions src/apps/profiles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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
Expand All @@ -23,6 +24,10 @@
from .models import User, Organization, Membership
from oidc_configurations.models import Auth_Organization
from .tokens import account_activation_token
from competitions.models import Competition
from datasets.models import Data, DataGroup
from tasks.models import Task
from forums.models import Post


class LoginView(auth_views.LoginView):
Expand Down Expand Up @@ -104,6 +109,87 @@ def activateEmail(request, user, to_email):
messages.error(request, f'Problem sending confirmation email to {to_email}, check if you typed it correctly.')


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', {
'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.')


def send_user_deletion_notice_to_admin(user):
admin_users = User.objects.filter(Q(is_superuser=True) | Q(is_staff=True))
admin_emails = [user.email for user in admin_users]

organizations = user.organizations.all()
competitions_organizer = user.competitions.all()
competitions_participation = Competition.objects.filter(participants__user=user)
submissions = user.submission.all()
data = Data.objects.filter(created_by=user)
data_groups = DataGroup.objects.filter(created_by=user)
tasks = Task.objects.filter(created_by=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', {
'user': user,
'organizations': organizations,
'competitions_organizer': competitions_organizer,
'competitions_participation': competitions_participation,
'submissions': submissions,
'data': data,
'data_groups': data_groups,
'tasks': tasks,
'queues': queues,
'posts': posts
})
email = EmailMessage(mail_subject, message, to=admin_emails)
email.send()


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()


def delete(request, uidb64, token):
try:
uid = force_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(pk=uid)
except User.DoesNotExist:
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):
user_email = user.email
# Send notice email to admins
send_user_deletion_notice_to_admin(user)

# Soft delete the user
user.delete()

# Send notice to user
send_user_deletion_confirmed(user_email)

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')


def sign_up(request):

# If sign up is not enabled then redirect to login
Expand Down Expand Up @@ -339,3 +425,15 @@ def get_context_data(self, **kwargs):

class OrganizationInviteView(LoginRequiredMixin, TemplateView):
template_name = 'profiles/organization_invite.html'


class UserAccountView(LoginRequiredMixin, DetailView):
queryset = User.objects.all()
template_name = 'profiles/user_account.html'
slug_url_kwarg = 'username'
query_pk_and_slug = True

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['serialized_user'] = json.dumps(UserSerializer(self.get_object()).data)
return context
7 changes: 6 additions & 1 deletion src/static/js/ours/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,5 +376,10 @@ CODALAB.api = {
delete_failed_submissions: () => {
return CODALAB.api.request('DELETE', `${URLS.API}delete_failed_submissions/`)
},

/*---------------------------------------------------------------------
User Account
---------------------------------------------------------------------*/
request_delete_account: (data) => {
return CODALAB.api.request('DELETE', `${URLS.API}delete_account/`, data)
},
}
110 changes: 110 additions & 0 deletions src/static/riot/profiles/profile_account.tag
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<profile-account>
<!-- Delete account section -->
<div id="delete-account">
<h2 class="title danger">Delete account</h2>
<div class="ui divider"></div>
<p><b>Warning:</b> Deleting your account is permanent and cannot be undone. All your personal data, settings, and content will be permanently erased, and you will lose access to all services linked to your account. Please make sure to back up any important information before proceeding.</p>
<button type="button" class="ui button delete-button" ref="delete_button" onclick="{show_modal.bind(this, '.delete-account.modal')}">Permanently delete my account</button>
</div>

<!-- Delete account modal -->
<div class="ui delete-account modal tiny" ref="delete_account_modal">
<div class="header">Are you sure you want to do this ?</div>

<div class="ui bottom attached negative message">
<i class="exclamation triangle icon"></i>
This is extremely important.
</div>

<div class="content">
<p>By clicking <b>"Delete my account"</b> you will receive a confirmation email to proceed with your account deletion.
<br><br>
This action is irreversible: all personal data will be permanently deleted or anonymized, <b>except for competitions and submissions</b> retained under the platform's user agreement.
<br><br>
If you wish to delete your submissions or competitions, please do so before deleting your account.
<br><br>
You will also no longer be eligible for any cash prizes in competitions you are participating in.
</p>
<div class="ui divider"></div>

<form class="ui form" id="delete-account-form" onsubmit="{handleDeleteAccountSubmit}">
<div class="required field">
<label for="username">Your username</label>
<input type="text" id="username" name="username" required oninput="{checkFields}" />
</div>

<div class="required field">
<label for="confirmation">Type <i>delete my account</i> to confirm</label>
<input type="text" id="confirmation" name="confirmation" required oninput="{checkFields}" />
</div>

<div class="required field">
<label for="password">Confirm your password</label>
<input type="password" id="password" name="password" required />
</div>

<button class="ui button fluid delete-button" type="submit" disabled="{isDeleteAccountSubmitButtonDisabled}" >Delete my account</button>
</form>
</div>
</div>

<script>
self = this;
self.user = user;

self.isDeleteAccountSubmitButtonDisabled = true;

self.show_modal = selector => $(selector).modal('show');

self.checkFields = function() {
const formValues = $('#delete-account-form').form('get values');
const username = formValues.username;
const confirmation = formValues.confirmation;

if (username === self.user.username && confirmation === "delete my account") {
self.isDeleteAccountSubmitButtonDisabled = false;
} else {
self.isDeleteAccountSubmitButtonDisabled = true;
}

self.update();
}

handleDeleteAccountSubmit = function(event) {
event.preventDefault();

const formValues = $('#delete-account-form').form('get values');

CODALAB.api.request_delete_account(formValues)
.done(function (response) {
const success = response.success;
if (success) {
toastr.success(response.message);
} else {
toastr.error(response.error);
}
})
.fail(function () {
toastr.error("An error occured. Please contact administrators");
})
}
</script>

<style type="text/stylus">
.title {
font-size: 24px;
font-weight: 600;
color: #24292f;
}
.danger {
color: #db2828;
}
.delete-button {
color: #db2828 !important;
}
.delete-button:hover {
background-color: #db2828 !important;
color: white !important;
}
</style>
</profile-account>
4 changes: 4 additions & 0 deletions src/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@
</i><i class="edit icon"></i>
Edit profile
</a>
<a class="item" href="{% url 'accounts:user_account' username=user.username %}">
<i class="cog icon"></i>
Account
</a>
<a class="item" href="{% url 'profiles:user_notifications' username=user.username %}">
</i><i class="bell icon"></i>
Notifications
Expand Down
24 changes: 24 additions & 0 deletions src/templates/profiles/emails/template_delete_account.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% extends 'emails/base_email.html' %}

{% block content %}
<p>We have received your request to delete your account.</p>
<p>To proceed with the deletion of your account, please confirm your request by clicking the link below:</p>
<p><a href="{{ protocol }}://{{ domain }}{% url 'accounts:delete' uidb64=uid token=token %}">{{ protocol }}://{{ domain }}{% url 'accounts:delete' uidb64=uid token=token %}</a></p>

<br>

<p><strong>Important Information:</strong></p>
<ul>
<li>Once confirmed, all your personal data will be permanently deleted or anonymized, except for competitions and submissions retained under our user agreement.</li>
<li>After deletion, you will no longer be eligible for any cash prizes in ongoing or future competitions.</li>
<li>If you wish to delete any submissions, please do so before confirming your account deletion.</li>
</ul>

<br>

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

<br>

<p>Thank you for being part of our community.</p>
{% endblock %}
Loading

0 comments on commit 58c70ad

Please sign in to comment.