Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions home/management/commands/delete_expired_accounts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from django.contrib.auth.models import User
from home.models import UserDeletionRequest

class Command(BaseCommand):
help = "Deletes user accounts that have been scheduled for deletion after 30 days"

def handle(self, *args, **kwargs):
now = timezone.now()
expired_requests = UserDeletionRequest.objects.filter(
scheduled_for__lte=now,
is_executed=False
)
if not expired_requests.exists():
self.stdout.write("No expired deletion requests to process.")
return

for deletion_request in expired_requests:
user = deletion_request.user
self.stdout.write(
f"Deleting user {user.username} (scheduled for {deletion_request.scheduled_for})"
)

user.delete() #user is deleted

deletion_request.is_executed = True
deletion_request.executed_at = now
deletion_request.save()

self.stdout.write(self.style.SUCCESS("Expired accounts deletion task complete."))
21 changes: 21 additions & 0 deletions home/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,4 +609,25 @@ class Migration(migrations.Migration):
'ordering': ['-published_at'],
},
),

migrations.CreateModel(
name='UserDeletionRequest',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('requested_at', models.DateTimeField(default=django.utils.timezone.now)),
('scheduled_for', models.DateTimeField()),
('is_executed', models.BooleanField(default=False)),
('executed_at', models.DateTimeField(blank=True, null=True)),
('user', models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name='deletion_request',
to=settings.AUTH_USER_MODEL
)),
],
),





]
81 changes: 73 additions & 8 deletions home/templates/accounts/confirm_delete.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,73 @@
<h2>Are you sure you want to delete your account?</h2>
<p>This action cannot be undone.</p>

<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Yes, delete my account</button>
<a href="{% url 'profile' %}" class="btn btn-secondary">Cancel</a>
</form>
{% load static %}

{% block content %}
<div class="d-flex justify-content-center align-items-center vh-100" style="background: #f0f2f5;">
<div class="card shadow-lg p-5" style="max-width: 600px; border-radius: 1.5rem; background: linear-gradient(145deg, #ffffffcc, #f1f5f9cc);">
<h2 class="mb-4 text-center text-primary fw-bold">Confirm Account Action</h2>

{% if messages %}
{% for message in messages %}
<div class="alert {% if message.tags %}alert-{{ message.tags }}{% else %}alert-info{% endif %} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}

<form method="post" action="{% url 'delete-account' %}">
{% csrf_token %}

<div class="mb-4">
<label for="password" class="form-label fw-semibold">Password:</label>
<input type="password" id="password" name="password" required class="form-control shadow-sm" placeholder="Your password" style="border-radius: 0.75rem; border: 1px solid #ced4da;">
</div>

<p class="fw-bold mb-3">Select an action:</p>

<div class="form-check mb-3 p-3 rounded hover-shadow" style="background-color: #f8f9fa; transition: all 0.3s;">
<input class="form-check-input" type="radio" id="deactivate" name="choice" value="deactivate" required>
<label class="form-check-label fw-medium" for="deactivate">
Deactivate account temporarily
</label>
</div>

<div class="form-check mb-3 p-3 rounded hover-shadow" style="background-color: #fff3f3; border: 1px solid #ffcccc; transition: all 0.3s;">
<input class="form-check-input" type="radio" id="delete_now" name="choice" value="delete_now">
<label class="form-check-label fw-medium text-danger" for="delete_now">
Delete account immediately
</label>
</div>

<div class="form-check mb-4 p-3 rounded hover-shadow" style="background-color: #fff8e6; border: 1px solid #ffe5b4; transition: all 0.3s;">
<input class="form-check-input" type="radio" id="delete_30days" name="choice" value="delete_30days">
<label class="form-check-label fw-medium text-warning" for="delete_30days">
Schedule account deletion in 30 days
</label>
</div>

<div class="d-flex justify-content-between mt-4">
<a href="{% url 'profile' %}" class="btn btn-secondary px-5 shadow-sm" style="border-radius: 1rem;">Cancel</a>
<button type="submit" class="btn btn-danger px-5 shadow-sm" style="border-radius: 1rem;">Confirm</button>
</div>
</form>
</div>
</div>

<style>
/* Hover effect for radio option cards */
.hover-shadow:hover {
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
background-color: #e2e6ea !important;
cursor: pointer;
transform: translateY(-2px);
transition: all 0.3s;
}

/* Smooth focus effect on inputs */
input:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.2rem rgba(13,110,253,0.25);
outline: none;
}
</style>
{% endblock %}
64 changes: 58 additions & 6 deletions home/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@

from .models import Article, Student, Project, Contact, Smishingdetection_join_us, Projects_join_us, Webpage, Profile, User, Course, Skill, Experience, Job, JobAlert, UserBlogPage, VaultDocument #Feedback


from django.contrib.auth.hashers import check_password
from django.contrib.auth import get_user_model
from .models import User
from django.utils import timezone
from datetime import timedelta
from django.core.mail import send_mail
from django.core.exceptions import ValidationError
from django.utils.http import urlsafe_base64_encode
Expand Down Expand Up @@ -173,6 +174,8 @@ def wrapped_view(request, *args, **kwargs):

from .forms import PenTestingRequestForm, SecureCodeReviewRequestForm
from .models import AppAttackReport
from .models import UserDeletionRequest



from home.models import TeamMember
Expand Down Expand Up @@ -3631,11 +3634,60 @@ def secure_code_review_form_view(request):
def delete_account(request):
if request.method == 'POST':
user = request.user
user.delete()
logout(request)
messages.success(request, "Your account has been deleted.")
return redirect('login')
return HttpResponseNotAllowed(['POST'])
choice = request.POST.get("choice")
password = request.POST.get("password")

if not password:
messages.error(request, "Password Required")
return render(request, "accounts/confirm_delete.html")

if not check_password(password, user.password):
messages.error(request, "Incorrect password. Please try again.")
return redirect('delete-account')

if choice == "deactivate":
logout(request)
send_mail(
"Account Deactivated",
"Your account has been temporarily deactivated. Log in again to reactivate.",
"admin@example.com",
[user.email],
)
messages.success(request, "Your account has been deactivated.")
return redirect('login')

elif choice == "delete_now":
email = user.email
user.delete()
send_mail(
"Account Deleted",
"Your account has been permanently deleted.",
"admin@example.com",
[email],
)
messages.success(request, "Your account has been permanently deleted.")
return redirect('login')

elif choice == "delete_30days":
UserDeletionRequest.objects.update_or_create(
user=user,
defaults={"scheduled_for": timezone.now() + timedelta(days=30)}
)
logout(request)
send_mail(
"Account Scheduled for Deletion",
"Your account is deactivated and will be permanently deleted in 30 days unless reactivated.",
"admin@example.com",
[user.email],
)
messages.success(request, "Your account is scheduled for deletion in 30 days.")
return redirect('login')

else:
messages.error(request, "Invalid option.")
return redirect('delete-account')

return render(request, "accounts/confirm_delete.html")

def tools_home(request):
return render(request, 'pages/pt_gui/tools/index.html')
Expand Down
Loading