Skip to content
Open
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
1 change: 1 addition & 0 deletions core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
),
# utils
path("resend-confirmation/", views.resend_confirmation_email, name="resend_confirmation"),
path("delete-account/", views.delete_account, name="delete_account"),
# payments
path("pricing", views.PricingView.as_view(), name="pricing"),
path(
Expand Down
83 changes: 83 additions & 0 deletions core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,89 @@ def get_context_data(self, **kwargs):
return context


@login_required
def delete_account(request):
"""Delete user account and all associated data."""
if request.method != "POST":
messages.error(request, "Invalid request method.")
return redirect(reverse("settings"))

user = request.user
profile = user.profile

logger.info(
"[DeleteAccount] User initiated account deletion",
user_id=user.id,
profile_id=profile.id,
email=user.email,
)
Comment on lines +230 to +235
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove PII from logs to ensure compliance.

The email address is being logged, which violates privacy best practices. Email addresses are considered PII and should not be retained in logs due to GDPR/CCPA compliance concerns.

As per coding guidelines.

Apply this diff:

 logger.info(
     "[DeleteAccount] User initiated account deletion",
     user_id=user.id,
     profile_id=profile.id,
-    email=user.email,
 )
🤖 Prompt for AI Agents
In core/views.py around lines 230 to 235, the logger call is including the
user's email (PII); remove the email field from the log and instead log non-PII
identifiers only (e.g., user_id and profile_id) or, if needed for
troubleshooting, log a redacted/hashed email (apply a one-way hash or mask) so
raw email addresses are never written to logs; update the logger.info invocation
to omit the email key or replace it with a hashed_email/masked_email value
generated server-side before logging.


# Cancel Stripe subscription if it exists
if profile.subscription and profile.subscription.status in ["active", "trialing"]:
try:
subscription_id = profile.subscription.id
stripe.Subscription.delete(subscription_id)
logger.info(
"[DeleteAccount] Cancelled Stripe subscription",
user_id=user.id,
subscription_id=subscription_id,
)
except stripe.error.StripeError as e:
logger.error(
"[DeleteAccount] Failed to cancel Stripe subscription",
user_id=user.id,
error=str(e),
exc_info=True,
)
Comment on lines +247 to +253
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: catching broad stripe.error.StripeError instead of specific exceptions - violates error handling guideline to avoid catching Exception or overly broad exceptions

Context Used: Context from dashboard - .cursor/rules/backend-error-handling.mdc (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: core/views.py
Line: 247:253

Comment:
**style:** catching broad `stripe.error.StripeError` instead of specific exceptions - violates error handling guideline to avoid catching `Exception` or overly broad exceptions

**Context Used:** Context from `dashboard` - .cursor/rules/backend-error-handling.mdc ([source](https://app.greptile.com/review/custom-context?memory=9343d853-8bca-46ca-b37d-fd4327d3e3d2))

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +237 to +253
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Account deletion proceeds even if Stripe cancellation fails.

If the Stripe subscription cancellation fails (line 241), the error is logged but the function continues to delete the account. This could leave active subscriptions orphaned in Stripe, leading to billing issues and potential compliance problems.

Additionally, the error log includes the user's email, which is PII and should not be logged.

As per coding guidelines.

Consider one of these approaches:

Option 1: Halt deletion on Stripe failure (recommended for active subscriptions)

-        except stripe.error.StripeError as e:
+        except stripe.error.StripeError as e:
             logger.error(
                 "[DeleteAccount] Failed to cancel Stripe subscription",
                 user_id=user.id,
-                error=str(e),
+                error=str(e),
                 exc_info=True,
             )
+            messages.error(
+                request,
+                "Unable to cancel your subscription. Please contact support before deleting your account."
+            )
+            return redirect(reverse("settings"))

Option 2: Continue but warn user

         except stripe.error.StripeError as e:
             logger.error(
                 "[DeleteAccount] Failed to cancel Stripe subscription",
                 user_id=user.id,
-                error=str(e),
                 exc_info=True,
             )
+            messages.warning(
+                request,
+                "Your account was deleted but we couldn't cancel your subscription. Please contact support."
+            )

Also remove the email from logs:

-            logger.error(
-                "[DeleteAccount] Failed to cancel Stripe subscription",
-                user_id=user.id,
-                error=str(e),
-                exc_info=True,
-            )
+            logger.error(
+                "[DeleteAccount] Failed to cancel Stripe subscription",
+                user_id=user.id,
+                subscription_id=subscription_id,
+                error=str(e),
+                exc_info=True,
+            )


# Track deletion event before deleting
if settings.POSTHOG_API_KEY:
async_task(
track_event,
profile_id=profile.id,
event_name="account_deleted",
properties={
"$set": {
"email": user.email,
"username": user.username,
},
},
source_function="delete_account",
group="Track Event",
)
Comment on lines +255 to +269
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical race condition: PostHog tracking will likely fail.

The async_task for PostHog tracking is queued before the user deletion, but it will execute asynchronously. The track_event function (from core/tasks.py lines 337-367) attempts to fetch the Profile:

profile = Profile.objects.get(id=profile_id)

However, user.delete() on line 287 will cascade-delete the Profile. When the async task executes, the Profile will likely already be deleted, causing the tracking to fail with a Profile.DoesNotExist error.

Apply this diff to track the event synchronously before deletion:

-    # Track deletion event before deleting
-    if settings.POSTHOG_API_KEY:
-        async_task(
-            track_event,
-            profile_id=profile.id,
-            event_name="account_deleted",
-            properties={
-                "$set": {
-                    "email": user.email,
-                    "username": user.username,
-                },
-            },
-            source_function="delete_account",
-            group="Track Event",
-        )
+    # Track deletion event before deleting (synchronous to ensure it completes)
+    if settings.POSTHOG_API_KEY:
+        try:
+            import posthog
+            posthog.capture(
+                user.email,
+                event="account_deleted",
+                properties={
+                    "profile_id": profile.id,
+                    "email": user.email,
+                    "current_state": profile.state,
+                    "username": user.username,
+                },
+            )
+        except Exception as e:
+            logger.error(
+                "[DeleteAccount] Failed to track deletion event",
+                user_id=user.id,
+                error=str(e),
+                exc_info=True,
+            )

Note: You'll need to add import posthog at the top of the file or in the function.

🤖 Prompt for AI Agents
In core/views.py around lines 255 to 269, queuing the PostHog tracking via
async_task creates a race because the background task fetches Profile by id but
the subsequent user.delete() will cascade-delete that Profile; instead, import
posthog and call PostHog synchronously before deleting the user (build the same
event properties using user.email and user.username and call
posthog.capture/identify as appropriate), then proceed with user.delete();
remove or avoid scheduling the async_task for this event to prevent
Profile.DoesNotExist errors.


# Django will cascade delete all related data:
# - Profile (CASCADE)
# - Projects (CASCADE via Profile)
# - BlogPostTitleSuggestion (CASCADE via Project)
# - GeneratedBlogPost (CASCADE via Project)
# - ProjectPage (CASCADE via Project)
# - Competitor (CASCADE via Project)
# - ProjectKeyword (CASCADE via Project)
# - AutoSubmissionSetting (CASCADE via Project)
# - Feedback (CASCADE via Profile)
# - ProfileStateTransition (SET_NULL via Profile)
# - EmailAddress objects (CASCADE via allauth)

username = user.username
email = user.email

user.delete()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: user session not cleared before redirect - user remains authenticated after account deletion, which could cause session errors or security issues

Suggested change
user.delete()
user.delete()
logout(request)
Prompt To Fix With AI
This is a comment left during a code review.
Path: core/views.py
Line: 287:287

Comment:
**logic:** user session not cleared before redirect - user remains authenticated after account deletion, which could cause session errors or security issues

```suggestion
    user.delete()
    logout(request)
```

How can I resolve this? If you propose a fix, please make it concise.


logger.info(
"[DeleteAccount] Successfully deleted user account",
username=username,
email=email,
)
Comment on lines +289 to +293
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove PII from success logs.

The email address should not be logged due to privacy compliance concerns (GDPR/CCPA).

As per coding guidelines.

Apply this diff:

 logger.info(
     "[DeleteAccount] Successfully deleted user account",
     username=username,
-    email=email,
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.info(
"[DeleteAccount] Successfully deleted user account",
username=username,
email=email,
)
logger.info(
"[DeleteAccount] Successfully deleted user account",
username=username,
)
🤖 Prompt for AI Agents
In core/views.py around lines 289-293, the success log currently includes the
user's email which is PII; remove the email from the logger call and replace it
with a non-PII identifier (e.g., user_id or internal ID) or omit the field
entirely. Update the logger invocation to no longer pass email=email and, if
available, add user_id=user.id (or username only if allowed by policy) to
satisfy auditing without logging the email.


messages.success(
request,
"Your account and all associated data have been permanently deleted. We're sorry to see you go!",
)

return redirect(reverse("landing"))


@login_required
def resend_confirmation_email(request):
user = request.user
Expand Down
97 changes: 97 additions & 0 deletions frontend/templates/pages/user-settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -370,10 +370,107 @@ <h2 class="text-lg font-semibold text-gray-900">Quick Actions</h2>
</ul>
</div>
</div>

<!-- Danger Zone -->
<div class="mt-6 overflow-hidden rounded-lg border border-red-200 bg-white shadow-sm">
<div class="border-b border-red-200 bg-red-50 px-6 py-4">
<h2 class="text-lg font-semibold text-red-900">Danger Zone</h2>
</div>
<div class="px-6 py-6">
<div class="flex items-start">
<div class="flex-1">
<h3 class="text-sm font-medium text-gray-900">Delete Account</h3>
<p class="mt-1 text-sm text-gray-600">
Permanently delete your account and all associated data. This action cannot be undone.
</p>
</div>
</div>
<div class="mt-4">
<button
type="button"
onclick="document.getElementById('delete-account-dialog').showModal()"
class="inline-flex items-center rounded-md border border-red-300 bg-white px-4 py-2 text-sm font-medium text-red-700 shadow-sm hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
>
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Delete Account
</button>
</div>
</div>
</div>
</div>
</div>

</div>
</div>
</div>

<!-- Delete Account Confirmation Dialog -->
<dialog id="delete-account-dialog" class="rounded-lg shadow-xl backdrop:bg-gray-900/50">
<div class="bg-white px-6 py-6 sm:max-w-md">
<div class="flex items-start">
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100">
<svg class="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
</div>
<div class="mt-4 text-center">
<h3 class="text-lg font-semibold text-gray-900">Delete Account</h3>
<div class="mt-2">
<p class="text-sm text-gray-600">
Are you sure you want to delete your account? This will permanently delete:
</p>
<ul class="mt-3 space-y-1 text-left text-sm text-gray-600">
<li class="flex items-start">
<svg class="mr-2 mt-0.5 h-4 w-4 flex-shrink-0 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
<span>All your projects and blog posts</span>
</li>
<li class="flex items-start">
<svg class="mr-2 mt-0.5 h-4 w-4 flex-shrink-0 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
<span>All keywords and competitor analysis</span>
</li>
<li class="flex items-start">
<svg class="mr-2 mt-0.5 h-4 w-4 flex-shrink-0 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
<span>Your subscription (if active)</span>
</li>
<li class="flex items-start">
<svg class="mr-2 mt-0.5 h-4 w-4 flex-shrink-0 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
<span>All account settings and preferences</span>
</li>
</ul>
<p class="mt-4 text-sm font-semibold text-red-600">
This action cannot be undone.
</p>
</div>
</div>
<div class="mt-6 flex gap-3">
<button
type="button"
onclick="document.getElementById('delete-account-dialog').close()"
class="flex-1 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
>
Cancel
</button>
<form method="post" action="{% url 'delete_account' %}" class="flex-1">
{% csrf_token %}
<button
type="submit"
class="w-full rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
>
Yes, Delete My Account
</button>
</form>
</div>
</div>
</dialog>
{% endblock content %}