Commit 02d2b829 authored by Birin Sanchez's avatar Birin Sanchez
Browse files

* Add feature to allow users to delete their account through the


  profile page.

* Add CLI delete_user command to mark accounts for purge or fully
  delete them.
Signed-off-by: Birin Sanchez's avatarBirin Sanchez <birin.sanchez@puri.sm>
parent c86e7540
Pipeline #13658 passed with stage
in 39 seconds
......@@ -7,5 +7,12 @@
{% block byline %}{% trans 'You have been logged out' %}{% endblock %}
{% block content %}
{% if messages %}
<div style="padding: 20px;">
{% for message in messages %}
<p>{{ message }}
{% endfor %}
</div>
{% endif %}
{% endblock %}
......@@ -2,8 +2,9 @@ import logging
import datetime
from django.conf import settings
from django.utils import timezone
from django.contrib.auth.hashers import make_password
from purist.models import get_woo_connection, AccountType
from purist.limitmonitor import ParserContainer
from purist.limitmonitor import ParserContainer, ServicesContainer
from limitmonitor.models import ExternalCredit, ExternalBundle
from limitmonitor.tunnel import TunnelManager
......@@ -218,3 +219,48 @@ def forced_update(user):
else:
logger.info('Subscription {} for user {} status is: {}'.format(
parsed_sub['label'], user.username, parsed_sub['status']))
def delete_account(user, purge_n=0, purge=False):
"""When called with purge=False it will remove user from all LDAP
groups, terminate tunnel account, delete WooCommerce account, mark
the user as inactive, set a gibberish LDAP password and set purge
value to purge_n.
When called with purge=True it will fully delete the user from
LDAP and from Django DB.
"""
# Get lowercase names of LDAP groups available from services
ldap_groups = [s.lower() for i, s in ServicesContainer.choices()]
# Remove undefined as there is no LDAP group with that name
ldap_groups.remove('undefined')
# Remove user from all LDAP groups
for ldap_group in ldap_groups:
user.remove_ldap_group(ldap_group)
# Terminate tunnel account
if user.has_tunnel_account():
try:
user.terminate_tunnel_account()
except TunnelManager.TerminateAccountError as e:
logger.error(repr(e))
# Delete WooCommerce account
user.delete_woocommerce_account()
# Set unusable password for LDAP
user.set_ldap_password(make_password(None))
# Mark user as deleted in Django
user.is_active = False
# Set purge value
user.purge = purge_n
user.save()
if purge:
ldap_user = user.get_ldap()
ldap_user.delete()
user.delete()
......@@ -157,6 +157,7 @@ SPDX-License-Identifier: AGPL-3.0
<ul>
<li><a href="{% url 'password_change' %}">{% trans "Change password" %}</a></li>
<li><a href="{% url 'profile_configure' %}">{% trans "Profile settings" %}</a></li>
<li><a href="{% url 'delete_account' %}">{% trans "Delete Account" %}</a></li>
</ul>
</article>
......
......@@ -52,6 +52,8 @@ urlpatterns = [
limitmonitor.views.new_invitation, name='new_invitation'),
url(r'^accounts/profile/configure',
ProfileConfigureView.as_view(), name='profile_configure'),
url(r'^accounts/profile/delete_account',
purist.views.DeleteAccountView.as_view(), name='delete_account'),
# url(r'^accounts/register/$', RegistrationView.as_view(form_class=RegistrationForm), name='registration_register'),
url(r'^accounts/login/$', LdhLoginView.as_view(), name='auth_login'),
url(r'^accounts/recover/(?P<signature>.+)/$', recover_done,
......
......@@ -108,3 +108,18 @@ class ProfileConfigureForm(forms.ModelForm):
def save(self, commit=True):
self.instance.set_woocommerce_billing_email()
return super(ProfileConfigureForm, self).save(commit)
class DeleteAccountForm(forms.Form):
address = forms.EmailField(
label=_('If you are sure, please type in your {} address and '
'confirm:'.format(settings.SITE_TITLE)),
)
def clean_address(self):
address = self.cleaned_data['address']
if self.user.get_identity() != address:
raise forms.ValidationError(_('The address provided is not the '
'identity of the current user.'))
return address
from django.core.management.base import BaseCommand
from django.core.validators import EmailValidator, ValidationError
from django.conf import settings
from limitmonitor.common import delete_account
from purist.models import User
class Command(BaseCommand):
help = """Removes user from all LDAP groups, terminates tunnel account,
deletes WooCommerce account, marks the user as inactive, sets a
gibberish LDAP password and sets purge value to default value 0."""
def add_arguments(self, parser):
parser.add_argument('email', type=str,
help='This is the email of the user that will get \
deleted.')
parser.add_argument('--purge_n',
type=int,
help='Purge number to be used for the user. \
Default is 0.',
default=0)
parser.add_argument('--full',
action='store_true',
help='Also deletes the user from middleware DB \
and LDAP.')
def handle(self, *args, **options):
email = options['email']
purge_n = options['purge_n']
full = options['full']
ev = EmailValidator()
try:
ev(email)
except ValidationError as e:
self.stdout.write(repr(e))
return
(username, domain) = email.split('@')
if domain != settings.SITE_DOMAIN:
msg = '{} does not belong to {} domain'
self.stdout.write(msg.format(email, settings.SITE_DOMAIN))
return
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
msg = 'User {} does not exist.'.format(email)
self.stdout.write(msg)
return
msg = 'Deleting user {}...'.format(email)
self.stdout.write(msg)
delete_account(user, purge_n=purge_n, purge=full)
if full:
msg = 'User {} fully deleted.'.format(email)
else:
msg = 'User {} deleted and marked for purge {}.'.format(email,
purge_n)
self.stdout.write(msg)
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-05-27 08:39
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('purist', '0006_email_not_unique'),
]
operations = [
migrations.AddField(
model_name='user',
name='purge',
field=models.IntegerField(default=None, null=True),
),
]
......@@ -87,6 +87,7 @@ class User(AbstractUser):
account_type = EnumIntegerField(enum=AccountType,
default=AccountType.UNDEFINED)
billing_email = models.EmailField(default='', null=False)
purge = models.IntegerField(default=None, null=True)
def clean(self):
super().clean()
......@@ -123,7 +124,11 @@ class User(AbstractUser):
def set_ldap_group(self, group_id):
ldap_person = self.get_ldap()
ldap_group = LdapGroup.objects.get(cn=group_id)
try:
ldap_group = LdapGroup.objects.get(cn=group_id)
except LdapGroup.DoesNotExist:
logging.exception('LDAP group {} does not exist.'.format(group_id))
return
u_dn = ldap_person.build_dn()
if u_dn not in ldap_group.members:
ldap_group.members.append(u_dn)
......@@ -131,7 +136,11 @@ class User(AbstractUser):
def remove_ldap_group(self, group_id):
ldap_person = self.get_ldap()
ldap_group = LdapGroup.objects.get(cn=group_id)
try:
ldap_group = LdapGroup.objects.get(cn=group_id)
except LdapGroup.DoesNotExist:
logging.exception('LDAP group {} does not exist.'.format(group_id))
return
u_dn = ldap_person.build_dn()
if u_dn in ldap_group.members:
ldap_group.members.remove(ldap_person.build_dn())
......
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% trans 'Delete Account' %}{% endblock %}
{% block header %}{% trans 'Delete Account' %}{% endblock %}
{% block byline %}{% trans 'Are you sure you want to delete your account?' %}{% endblock %}
{% block extra_css %}
<style>
.wait {
display: block;
text-align: center;
font-size: 5em;
}
.hidden {
display: none;
}
.spinner-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
background: white;
}
.spinner {
display: block;
position: relative;
left: 50%;
top: 50%;
width: 150px;
height: 150px;
margin: -75px 0 0 -75px;
border: 10px transparent solid;
border-top-color:black;
border-radius: 50%;
border-bottom: 10px transparent solid;
z-index: 1500;
/* Animation stuff below */
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
.spinner:after {
content: "";
position: absolute;
top: 15px;
left: 15px;
right: 15px;
bottom: 15px;
border: 10px transparent solid;
border-top-color:black;
border-radius: 50%;
/* Animation stuff below */
-webkit-animation: spin 3s linear infinite;
animation: spin 3s linear infinite;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg); /* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(0deg); /* IE 9 */
transform: rotate(0deg); /* Firefox 16+, IE 10+, Opera */
}
100% {
-webkit-transform: rotate(360deg); /* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(360deg); /* IE 9 */
transform: rotate(360deg); /* Firefox 16+, IE 10+, Opera */
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg); /* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(0deg); /* IE 9 */
transform: rotate(0deg); /* Firefox 16+, IE 10+, Opera */
}
100% {
-webkit-transform: rotate(360deg); /* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(360deg); /* IE 9 */
transform: rotate(360deg); /* Firefox 16+, IE 10+, Opera */
}
}
</style>
{% endblock %}
{% block content %}
<div style="padding: 20px;">
<p><strong>WARNING!</strong></p>
Deleting your account will permanently delete all your data, and your username will no longer be available.
<form id="delete-acc" action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Confirm" />
<a href="{% url 'profile' %}" class="button">No thanks</a>
</form>
</div>
<div id="spinner-wrap" class="hidden">
<div class="spinner"></div>
<p class="wait">Please wait ...</p>
</div>
{% endblock %}
{% block extra_js %}
<script>
const frm = document.getElementById('delete-acc'),
spin = document.getElementById('spinner-wrap');
frm.addEventListener('submit', enable_spinner);
function enable_spinner() {
spin.setAttribute('class', 'spinner-wrapper');
}
</script>
{% endblock %}
......@@ -9,14 +9,16 @@ from password_reset.views import Recover, Reset
from .serializers import UserSerializer
from .forms import CaptchaPasswordRecoveryForm, PasswordChangeForm, \
ProfileConfigureForm, RecoveryPasswordResetForm
ProfileConfigureForm, RecoveryPasswordResetForm, DeleteAccountForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import PasswordChangeView \
as BasePasswordChangeView
from django.contrib.auth.views import PasswordChangeDoneView \
as BasePasswordChangeDoneView
from django.contrib import messages
from django.urls import reverse_lazy
from django.views.generic.edit import UpdateView
from django.views.generic.edit import UpdateView, FormView
from limitmonitor.common import delete_account
class UserDetail(APIView):
......@@ -95,6 +97,25 @@ class ProfileConfigureView(LoginRequiredMixin, UpdateView):
"billing_email": self.object.get_woocommerce_billing_email()}
class DeleteAccountView(LoginRequiredMixin, FormView):
form_class = DeleteAccountForm
template_name = 'purist/account_confirm_delete.html'
success_url = reverse_lazy('auth_logout')
# Override get_form() to pass user object to the form
def get_form(self, *args, **kwargs):
form = super(DeleteAccountView, self).get_form(*args, **kwargs)
form.user = self.request.user
return form
def form_valid(self, form):
delete_account(self.request.user)
messages.add_message(self.request, messages.INFO,
'Your account has been permanently deleted.')
return super(DeleteAccountView, self).form_valid(form)
def home(request):
render_data = {
"username": request.user.get_username(),
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment