Skip to content

Conversation

@rasulkireev
Copy link
Owner

@rasulkireev rasulkireev commented Nov 2, 2025

This pull request contains changes generated by a Cursor Cloud Agent

Open in Cursor Open in Web

Summary by CodeRabbit

  • New Features
    • Introduced account deletion capability in user settings with a dedicated "Danger Zone" section for destructive actions.
    • Delete Account feature includes a detailed confirmation dialog displaying all user data scheduled for permanent removal.
    • Secure account deletion process with clear confirmation and cancellation options available.

@cursor
Copy link

cursor bot commented Nov 2, 2025

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 2, 2025

Walkthrough

This PR introduces a complete account deletion feature. It adds a new POST endpoint that cancels active Stripe subscriptions, logs deletion events via PostHog if configured, and removes user accounts with cascade deletes. A "Danger Zone" UI section in user settings provides a confirmation dialog.

Changes

Cohort / File(s) Summary
Backend Account Deletion
core/urls.py, core/views.py
Adds delete-account/ URL route and protected delete_account() view that processes POST requests only. Cancels active Stripe subscriptions, tracks account deletion via PostHog async task when configured, deletes user via cascade, and redirects to landing page. Includes error handling for invalid HTTP methods and Stripe failures.
Frontend Settings UI
frontend/templates/pages/user-settings.html
Introduces "Danger Zone" section with Delete Account button triggering a confirmation modal. Modal displays data deletion details and includes CSRF-protected form with Cancel and "Yes, Delete My Account" actions.

Sequence Diagram

sequenceDiagram
    participant User
    participant Frontend as User Settings UI
    participant Backend as delete_account View
    participant Stripe
    participant PostHog
    participant DB

    User->>Frontend: Click Delete Account
    Frontend->>User: Show Confirmation Dialog
    User->>Frontend: Confirm Deletion
    Frontend->>Backend: POST /delete-account/

    rect rgb(200, 220, 255)
    note over Backend: Validate & Process
    Backend->>Stripe: Cancel Active Subscription
    alt Subscription Found
        Stripe-->>Backend: Cancellation OK
    else No Subscription
        Stripe-->>Backend: N/A
    end
    end

    rect rgb(220, 200, 255)
    note over Backend: Event Tracking
    Backend->>PostHog: async_task(account_deleted)
    PostHog-->>Backend: Queued
    end

    rect rgb(200, 230, 200)
    note over Backend: Delete & Cleanup
    Backend->>DB: Delete User + Cascade
    DB-->>Backend: Deleted
    end

    Backend-->>Frontend: Redirect to Landing
    Frontend->>User: Navigate Away
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–25 minutes

  • Stripe subscription cancellation logic: Verify correct API usage and error handling for missing or inactive subscriptions
  • PostHog async task integration: Confirm async_task() is properly configured and gracefully handled if PostHog is unconfigured
  • Cascade delete verification: Ensure all related user data (subscriptions, profiles, etc.) is properly deleted
  • Form CSRF protection: Confirm CSRF token is present in the confirmation modal form
  • Error messaging and redirect paths: Review error handling for invalid methods and Stripe failures

Poem

🐰 A "Danger Zone" blooms in settings fair,
With one click, accounts dissolve to air—
Stripe subscriptions wave goodbye with grace,
PostHog logs the final trace,
Cascade deletes leave not a trace! 🗑️✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Implement user account deletion with data purge" directly and comprehensively captures the primary changes across all three modified files. The changes introduce a new URL endpoint, a protected view function that handles account deletion with Stripe subscription cancellation and event tracking, and UI components for users to delete their accounts. The title accurately reflects both the core functionality (user account deletion) and the associated data removal (cascade deletes), making it specific and meaningful for someone scanning commit history.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cursor/implement-user-account-deletion-with-data-purge-1ceb

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rasulkireev rasulkireev marked this pull request as ready for review November 2, 2025 00:22
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Greptile Overview

Greptile Summary

Implements user account deletion with comprehensive data purge functionality. The feature allows users to permanently delete their accounts through a new settings UI with confirmation dialog.

Key Changes:

  • Added delete_account view that cancels Stripe subscriptions, tracks deletion events in PostHog, and leverages Django's CASCADE deletion to remove all user data (projects, blog posts, keywords, competitors, feedback)
  • Added URL route for the delete account endpoint
  • Introduced "Danger Zone" UI section in user settings with a native <dialog> element for confirmation
  • Clear user communication about what data will be deleted (projects, blog posts, subscription, settings)

Issues Found:

  • Missing user logout after account deletion - user session remains active after their account is deleted
  • Using broad exception catching (StripeError) instead of specific Stripe exceptions

Confidence Score: 3/5

  • Safe to merge with one critical fix needed
  • The implementation correctly handles Stripe subscription cancellation and data deletion via CASCADE constraints. However, there's a critical issue where the user session is not properly terminated after account deletion, which could lead to session errors or security concerns. The broad exception handling is a style issue but doesn't affect functionality.
  • core/views.py needs attention - must add logout call after user deletion

Important Files Changed

File Analysis

Filename Score Overview
core/views.py 3/5 Added delete_account view with Stripe subscription cancellation, PostHog tracking, and cascade deletion. Missing logout after deletion and using broad exception catching.
core/urls.py 5/5 Added URL route for delete_account view. Simple and correct addition.
frontend/templates/pages/user-settings.html 5/5 Added danger zone section with delete account UI using native dialog element. Clear warnings and proper CSRF protection.

Sequence Diagram

sequenceDiagram
    participant User
    participant Browser
    participant DeleteAccountView
    participant StripeAPI
    participant Database
    participant PostHog
    
    User->>Browser: Click "Delete Account"
    Browser->>Browser: Show confirmation dialog
    User->>Browser: Confirm deletion
    Browser->>DeleteAccountView: POST /delete-account/
    
    DeleteAccountView->>DeleteAccountView: Check request method
    DeleteAccountView->>DeleteAccountView: Get user and profile
    DeleteAccountView->>DeleteAccountView: Log deletion initiation
    
    alt Subscription exists and is active/trialing
        DeleteAccountView->>StripeAPI: stripe.Subscription.delete(subscription_id)
        alt Stripe cancellation succeeds
            StripeAPI-->>DeleteAccountView: Success
            DeleteAccountView->>DeleteAccountView: Log success
        else Stripe cancellation fails
            StripeAPI-->>DeleteAccountView: StripeError
            DeleteAccountView->>DeleteAccountView: Log error (continue)
        end
    end
    
    alt PostHog configured
        DeleteAccountView->>PostHog: async_task(track_event, "account_deleted")
        Note over PostHog: Async task queued
    end
    
    DeleteAccountView->>Database: user.delete()
    Note over Database: CASCADE deletes:<br/>Profile, Projects, BlogPosts,<br/>Keywords, Competitors, etc.
    Database-->>DeleteAccountView: Deletion complete
    
    DeleteAccountView->>DeleteAccountView: Log successful deletion
    DeleteAccountView->>Browser: Redirect to landing page
    Browser->>User: Show success message
Loading

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +247 to +253
except stripe.error.StripeError as e:
logger.error(
"[DeleteAccount] Failed to cancel Stripe subscription",
user_id=user.id,
error=str(e),
exc_info=True,
)
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.

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
frontend/templates/pages/user-settings.html (1)

389-398: Consider refactoring to use Stimulus JS instead of inline onclick.

The inline onclick handler works but doesn't follow the project's preference for Stimulus JS controllers. Consider creating a Stimulus controller to handle the dialog interaction.

As per coding guidelines.

You could create a simple controller:

// delete_account_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["dialog"]
  
  open(event) {
    event.preventDefault()
    this.dialogTarget.showModal()
  }
  
  close(event) {
    event.preventDefault()
    this.dialogTarget.close()
  }
}

Then update the HTML:

-<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"
->
+<div data-controller="delete-account">
+  <button
+    type="button"
+    data-action="click->delete-account#open"
+    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"
+  >
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1317210 and 34cf995.

📒 Files selected for processing (3)
  • core/urls.py (1 hunks)
  • core/views.py (1 hunks)
  • frontend/templates/pages/user-settings.html (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.html

📄 CodeRabbit inference engine (.cursor/rules/frontend.mdc)

**/*.html: Prefer Stimulus JS for adding interactivity to Django templates instead of raw script elements
Leverage Stimulus data attributes to connect HTML elements with JavaScript functionality
Utilize Stimulus targets to reference specific elements within a controller
Employ Stimulus actions to handle user interactions and events

Files:

  • frontend/templates/pages/user-settings.html
**/*.{html,htm}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

Always generate semantic HTML

Files:

  • frontend/templates/pages/user-settings.html
{**/*.{html,htm,css,scss},**/*_controller.@(js|ts)}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

Always favor the utility-first Tailwind approach; avoid creating reusable style classes and prefer reuse via template components

Files:

  • frontend/templates/pages/user-settings.html
frontend/templates/**/*.html

📄 CodeRabbit inference engine (.cursor/rules/stimulus-events.mdc)

Avoid data-*-outlet links for sibling controller communication when using the event-based approach; keep controllers self-contained

Use semantic HTML elements (e.g., dialog, details/summary) in templates

Files:

  • frontend/templates/pages/user-settings.html
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

**/*.py: Prioritize readability and maintainability; follow PEP 8
Use descriptive variable/function names with underscores

In Python, use try-except blocks that catch specific exception types; do not catch broad Exception.

**/*.py: Follow PEP 8 for Python code style
Use descriptive variable names with underscores (snake_case) in Python
Prefer Django's built-in features over external libraries

**/*.py: Use structlog for logging in Python code (avoid print and the standard logging module)
Include contextual key-value fields in log messages (e.g., error=str(e), exc_info=True, profile_id=profile.id)

**/*.py: Use descriptive, full-word variable names; avoid abbreviations and single-letter variables in Python
Provide context in variable names when format or type matters (e.g., include _iso_format, date)
Extract unchanging values into UPPER_CASE constants
Use intermediary variables to name parsed groups instead of using index access directly
Naming conventions: use is_/has_/can_ for booleans; include 'date' for dates; snake_case for variables/functions; PascalCase for classes
Define variables close to where they are used to keep lifespan short
Name things after what they do, not how they're used; ensure names make sense without extra context
Avoid generic names like data, info, manager; use specific, intention-revealing names
Function names should include necessary context without being verbose
If naming is hard, split functions into smaller focused parts
Maintain consistency: reuse the same verbs and nouns for the same concepts; name variables after the functions that create them
Use more descriptive names for longer-lived variables
Avoid else statements by using guard clauses for early returns
Replace simple conditionals with direct assignment when both branches call the same function with different values
Use dictionaries as dispatch tables instead of multiple equal-probability elif chains
Validate input before processing to prevent errors propagating in...

Files:

  • core/views.py
  • core/urls.py
core/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

Leverage Django's ORM; avoid raw SQL when possible

Files:

  • core/views.py
  • core/urls.py
core/views.py

📄 CodeRabbit inference engine (CLAUDE.md)

Keep Django views skinny: handle request/response only

Files:

  • core/views.py
🧬 Code graph analysis (2)
core/views.py (1)
core/tasks.py (1)
  • track_event (338-368)
core/urls.py (1)
core/views.py (1)
  • delete_account (221-300)
🔇 Additional comments (4)
core/urls.py (1)

40-40: LGTM!

The URL pattern is correctly configured and appropriately placed in the utils section.

frontend/templates/pages/user-settings.html (1)

409-475: Excellent use of semantic HTML with the dialog element.

The confirmation dialog provides clear information about what will be deleted and properly uses CSRF protection. The same Stimulus JS refactoring suggestion from the previous comment applies to the close button on line 459.

As per coding guidelines.

core/views.py (2)

223-225: Good security practice with POST-only enforcement.

Correctly restricts the dangerous delete operation to POST requests only, providing CSRF protection.


271-287: Excellent documentation of cascade delete behavior.

The comprehensive comment listing all related data that will be cascade-deleted is very helpful for future maintainers. The approach of saving username and email before deletion is correct.

Comment on lines +230 to +235
logger.info(
"[DeleteAccount] User initiated account deletion",
user_id=user.id,
profile_id=profile.id,
email=user.email,
)
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.

Comment on lines +237 to +253
# 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,
)
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,
+            )

Comment on lines +255 to +269
# 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",
)
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.

Comment on lines +289 to +293
logger.info(
"[DeleteAccount] Successfully deleted user account",
username=username,
email=email,
)
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants