From f998e162b83a538eec8d7e2668f9dd9d3e458589 Mon Sep 17 00:00:00 2001 From: uxairibrar Date: Sat, 22 Feb 2025 00:24:29 +0100 Subject: [PATCH 1/7] Account deletion must be confirmed --- optimap/settings.py | 2 + publications/models.py | 25 +- publications/serializers.py | 12 +- publications/signals.py | 3 +- publications/templates/user_settings.html | 274 +++++++++++++++------- publications/urls.py | 4 +- publications/views.py | 95 +++++++- 7 files changed, 330 insertions(+), 85 deletions(-) diff --git a/optimap/settings.py b/optimap/settings.py index d252053..4bd01b6 100644 --- a/optimap/settings.py +++ b/optimap/settings.py @@ -50,6 +50,8 @@ "sesame.backends.ModelBackend", ] +AUTH_USER_MODEL = "publications.CustomUser" + INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', diff --git a/publications/models.py b/publications/models.py index 520aaae..8bdb97d 100644 --- a/publications/models.py +++ b/publications/models.py @@ -1,6 +1,9 @@ from django.contrib.gis.db import models from django.contrib.postgres.fields import ArrayField from django_currentuser.db.models import CurrentUserField +from django.utils.timezone import now +from django.contrib.auth.models import AbstractUser, Group, Permission +import uuid STATUS_CHOICES = ( ("d", "Draft"), @@ -10,6 +13,25 @@ ("h", "Harvested"), ) +class CustomUser(AbstractUser): + deleted = models.BooleanField(default=False) + deleted_at = models.DateTimeField(null=True, blank=True) + + groups = models.ManyToManyField(Group, related_name="publications_users", blank=True) + user_permissions = models.ManyToManyField(Permission, related_name="publications_users_permissions", blank=True) + + def soft_delete(self): + """Marks the user as deleted instead of removing from the database.""" + self.deleted = True + self.deleted_at = now() + self.save() + + def restore(self): + """Restores a previously deleted user.""" + self.deleted = False + self.deleted_at = None + self.save() + class Publication(models.Model): # required fields doi = models.CharField(max_length=1024, unique=True) @@ -88,7 +110,8 @@ class Meta: from import_export import fields, resources from import_export.widgets import ForeignKeyWidget from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model +User = get_user_model() class PublicationResource(resources.ModelResource): #created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='username') #updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='username') diff --git a/publications/serializers.py b/publications/serializers.py index 00e9f60..fd430ec 100644 --- a/publications/serializers.py +++ b/publications/serializers.py @@ -23,4 +23,14 @@ class Meta: fields = ("search_term","timeperiod_startdate","timeperiod_enddate","user_name") geo_field = "search_area" auto_bbox = True - \ No newline at end of file + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["id", "username", "email"] + + def to_representation(self, instance): + """Ensure deleted users are excluded from serialization.""" + if instance.deleted: + return None + return super().to_representation(instance) diff --git a/publications/signals.py b/publications/signals.py index d54655d..3789f50 100644 --- a/publications/signals.py +++ b/publications/signals.py @@ -3,7 +3,8 @@ from django.db.models.signals import pre_save from django.dispatch import receiver -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model +User = get_user_model() from django.conf import settings @receiver(pre_save, sender=User) diff --git a/publications/templates/user_settings.html b/publications/templates/user_settings.html index 2636c33..24e3b9e 100644 --- a/publications/templates/user_settings.html +++ b/publications/templates/user_settings.html @@ -1,101 +1,217 @@ -{% extends "base.html" %} -{% load static %} - -{% block navbar %} +{% extends "base.html" %} {% load static %} {% block navbar %} -{% endblock %} - - {% block content %} -
-
- -
-
-
-

- -

-
+{% endblock %} {% block content %} +
+
+
+
+
+

+ +

+
-
-
-
- {% csrf_token %} -
- -
- -
-
-
- +
+
+ + {% csrf_token %} +
+ +
+
-
- -
- -
+
+
+ +
+
+ +
+
- -
+
+
+
-
-
-

- -

-
-
-
- -

Deleting account is permenant. It cannot be reversed.

+
+
+

+ +

+
+
+
+
+

+ Deleting your account is permanent. It cannot be reversed. +

- + +
-
+ +
- {% endblock %} \ No newline at end of file + {% if request.session.delete_token %} + + {% endif %} {% endblock %} +
diff --git a/publications/urls.py b/publications/urls.py index 9086216..2165da6 100644 --- a/publications/urls.py +++ b/publications/urls.py @@ -29,6 +29,8 @@ path("usersettings/", views.user_settings, name="usersettings"), path("subscriptions/", views.user_subscriptions, name="subscriptions"), path("addsubscriptions/", views.add_subscriptions, name="addsubscriptions"), - path("delete/", views.delete_account, name="delete"), + path("request_delete/", views.request_delete, name="request_delete"), + path("confirm-delete//", views.confirm_account_deletion, name="confirm_delete"), + path("finalize-delete/", views.finalize_account_deletion, name="finalize_delete"), path("changeuser/", views.change_useremail, name="changeuser"), ] diff --git a/publications/views.py b/publications/views.py index 45c671d..aeb05ce 100644 --- a/publications/views.py +++ b/publications/views.py @@ -14,7 +14,6 @@ from django.contrib import messages from django.contrib.auth import login,logout from django.views.decorators.http import require_GET -from django.contrib.auth.models import User from django.conf import settings from publications.models import Subscription from datetime import datetime @@ -22,6 +21,13 @@ import time from math import floor from django_currentuser.middleware import (get_current_user, get_current_authenticated_user) +from django.urls import reverse +import uuid +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect +from django.utils.timezone import now +from django.contrib.auth import get_user_model +User = get_user_model() LOGIN_TOKEN_LENGTH = 32 LOGIN_TOKEN_TIMEOUT_SECONDS = 10 * 60 @@ -106,7 +112,23 @@ def authenticate_via_magic_link(request: HttpRequest, token: str): } }) - user, is_new = User.objects.get_or_create(username = email, email = email) + user = User.objects.filter(email=email).first() + + if user: + if user.deleted: + # Create a new user if existing record is found + user.deleted = False + user.deleted_at = None + user.is_active = True + user.save() + is_new = False + else: + is_new = False + else: + # Create a new user if no existing record is found + user = User.objects.create_user(username=email, email=email) + is_new = True + login(request, user, backend='django.contrib.auth.backends.ModelBackend') cache.delete(token) @@ -197,3 +219,72 @@ def get_login_link(request, email): cache.set(token, email, timeout = LOGIN_TOKEN_TIMEOUT_SECONDS) logger.info('Created login link for %s with token %s - %s', email, token, link) return link + + +@login_required +def request_delete(request): + user = request.user + token = uuid.uuid4().hex + + cache.set(f"delete_token_{token}", user.id, timeout=3600) + + confirm_url = request.build_absolute_uri(reverse('optimap:confirm_delete', args=[token])) + + send_mail( + 'Confirm Your Account Deletion', + f'Click the link to confirm deletion: {confirm_url}', + 'no-reply@optimap.com', + [user.email], + ) + + return redirect(reverse('optimap:usersettings') + '?message=Check your email for a confirmation link.') + +@login_required +def confirm_account_deletion(request, token): + user_id = cache.get(f"delete_token_{token}") + + if user_id is None: + messages.error(request, "Invalid or expired deletion token.") + return redirect(reverse('optimap:usersettings')) + + user = get_object_or_404(User, id=user_id) + + request.session["delete_token"] = token + + messages.warning(request, "Please confirm your account deletion. Your contributed data will remain on the platform.") + return redirect(reverse('optimap:usersettings')) + + +@login_required +def finalize_account_deletion(request): + token = request.session.get("delete_token") + + if not token: + messages.error(request, "No active deletion request found.") + return redirect(reverse('optimap:usersettings')) + + user_id = cache.get(f"delete_token_{token}") + + if user_id is None: + messages.error(request, "Invalid or expired deletion request.") + return redirect(reverse('optimap:usersettings')) + + user = get_object_or_404(User, id=user_id) + + if user.deleted: + messages.warning(request, "This account has already been deleted.") + return redirect(reverse('optimap:usersettings')) + + user.deleted = True + user.deleted_at = now() + user.save() + cache.delete(f"delete_token_{token}") + + if "delete_token" in request.session: + del request.session["delete_token"] + request.session.modified = True + + logout(request) + + messages.success(request, "Your account has been successfully deleted.") + return redirect(reverse('optimap:main')) \ No newline at end of file From a22ce1e325c8e9f17ee3b6f7c3f744dc7af37bb0 Mon Sep 17 00:00:00 2001 From: uxairibrar Date: Sat, 22 Feb 2025 00:43:39 +0100 Subject: [PATCH 2/7] Fixed error in imports --- publications/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/publications/serializers.py b/publications/serializers.py index fd430ec..3de2566 100644 --- a/publications/serializers.py +++ b/publications/serializers.py @@ -2,6 +2,8 @@ from rest_framework_gis import serializers from .models import Publication +from django.contrib.auth import get_user_model +User = get_user_model() from publications.models import Publication,Subscription From 07c85efa656e95de6b01ff180201d27548b0456d Mon Sep 17 00:00:00 2001 From: uxairibrar Date: Sat, 15 Mar 2025 15:04:01 +0100 Subject: [PATCH 3/7] Code Refactoring and added more checks --- publications/admin.py | 4 - publications/models.py | 10 +-- publications/templates/base.html | 104 +++++++++++----------- publications/templates/menu_snippet.html | 58 +++++++----- publications/templates/user_settings.html | 21 ++++- publications/urls.py | 4 +- publications/views.py | 76 +++++++++++----- 7 files changed, 167 insertions(+), 110 deletions(-) diff --git a/publications/admin.py b/publications/admin.py index 58836a6..5f96f81 100644 --- a/publications/admin.py +++ b/publications/admin.py @@ -10,10 +10,6 @@ from django_q.models import Schedule from datetime import datetime, timedelta - -# Unregister the default User admin -admin.site.unregister(User) - @admin.action(description="Mark selected publications as published") def make_public(modeladmin, request, queryset): queryset.update(status="p") diff --git a/publications/models.py b/publications/models.py index 261762b..f67f0aa 100644 --- a/publications/models.py +++ b/publications/models.py @@ -5,9 +5,6 @@ from django.contrib.auth.models import AbstractUser, Group, Permission import uuid from django.utils.timezone import now -from django.contrib.auth import get_user_model - -User = get_user_model() STATUS_CHOICES = ( ("d", "Draft"), @@ -20,7 +17,6 @@ class CustomUser(AbstractUser): deleted = models.BooleanField(default=False) deleted_at = models.DateTimeField(null=True, blank=True) - groups = models.ManyToManyField(Group, related_name="publications_users", blank=True) user_permissions = models.ManyToManyField(Permission, related_name="publications_users_permissions", blank=True) @@ -110,6 +106,9 @@ class Meta: ordering = ['user_name'] verbose_name = "subscription" +from django.contrib.auth import get_user_model +User = get_user_model() + class EmailLog(models.Model): TRIGGER_CHOICES = [ ("admin", "Admin Panel"), @@ -148,8 +147,7 @@ def log_email(cls, recipient, subject, content, sent_by=None, trigger_source="ma from import_export import fields, resources from import_export.widgets import ForeignKeyWidget from django.conf import settings -from django.contrib.auth import get_user_model -User = get_user_model() + class PublicationResource(resources.ModelResource): #created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='username') #updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='username') diff --git a/publications/templates/base.html b/publications/templates/base.html index bb9b7a8..9749d74 100644 --- a/publications/templates/base.html +++ b/publications/templates/base.html @@ -1,53 +1,57 @@ {% load static %} - - - - - - {% block title %}{% endblock %}OPTIMAP - - - - - - - - - - {% block head %}{% endblock %} - - - - - - - - - -
- {% block alert %}{% endblock %} - - {% block content %}{% endblock %} -
- - {% include 'footer.html' %} - - {% block scripts %}{% endblock %} - - - - \ No newline at end of file + + + + + {% block title %}{% endblock %}OPTIMAP + + + + + + + + + + {% block head %}{% endblock %} + + + + + + + + +
+ {% block alert %}{% endblock %} {% block content %}{% endblock %} +
+ + {% include 'footer.html' %} {% block scripts %}{% endblock %} + + diff --git a/publications/templates/menu_snippet.html b/publications/templates/menu_snippet.html index a89b11b..32f6d30 100644 --- a/publications/templates/menu_snippet.html +++ b/publications/templates/menu_snippet.html @@ -1,23 +1,39 @@ - \ No newline at end of file diff --git a/publications/templates/user_settings.html b/publications/templates/user_settings.html index b8de4a2..8cdec19 100644 --- a/publications/templates/user_settings.html +++ b/publications/templates/user_settings.html @@ -161,7 +161,9 @@

data-parent="#user_settings" >
-

Deleting account is permenant. It cannot be reversed.

+

+ Deleting account is permanent. It cannot be reversed. +

Do you really want to delete this account ?

+{% if delete_token %} + +{% endif %}