Skip to content

Conversation

vahidbazzaz
Copy link
Contributor

Problem

When a user is deleted, we append .deleted.<user_id> to their email address to work around the uniqueness constraint in the cms_users table. However, if someone attempts to delete an already-deleted user (due to race condition, UI bug, or direct API call), the suffix gets appended again:

user@domain.com → user@domain.com.deleted.123 → user@domain.com.deleted.123.deleted.123

When reactivating, the code only removes ONE suffix, leaving user@domain.com.deleted.123, which is invalid. Auth0 rejects this email, making the user impossible to reactivate.

Real-world example from bug report:

email became .deleted.1050.deleted.1050 and reactivation failed with Auth0 error

Root Cause

Files: include/cms_users_handle_ajax_operations.php, library/ajax/deleteprofile.php

The deletion code always appends the suffix without checking if it already exists:

db_query('UPDATE '.$table.' SET email = CONCAT(email,".deleted.",id) WHERE id = :id', ['id' => $id]);

While listDelete() prevents updating the deleted timestamp for already-deleted users, the email CONCAT still executes unconditionally.

Solution

1. Prevention (Primary Fix)

Added regex check to prevent appending suffix if it already exists:

db_query('UPDATE '.$table.' SET email = CONCAT(email,".deleted.",id) 
         WHERE id = :id AND email NOT REGEXP "@.*\.deleted\.[0-9]+$"', ['id' => $id]);

The pattern @.*\.deleted\.[0-9]+$:

  • Matches .deleted.<digits> only after the @ symbol at end of email
  • Avoids false positives like john.deleted.smith@example.com
  • Applied to both admin deletion and self-deletion flows

2. Cleanup (For Existing Issues)

Created cron job /cron/fix-double-deleted-emails.php:

  • Scans for emails with pattern %.deleted.%.deleted.%
  • Removes all .deleted.<id> suffixes and adds back a single one
  • Logs changes to history table
  • Can be run manually or scheduled

3. Testing

Added comprehensive Cypress tests (2_9_DoubleDeletedEmailFix.js):

  • Test 1: Normal delete/reactivate flow works
  • Test 2: Multiple delete/reactivate cycles work without suffix accumulation

Changes

4 commits:

  1. Prevention fix for double-deletion
  2. Cleanup cron job for existing corrupted data
  3. Cypress tests
  4. Lint fix (removed unused imports)

Files modified:

  • include/cms_users_handle_ajax_operations.php - Prevention fix
  • library/ajax/deleteprofile.php - Prevention fix
  • cron/fix-double-deleted-emails.php - New cleanup cron job
  • gcloud-entry.php - Routing for cron job
  • cypress/e2e/2_auth_tests/2_9_DoubleDeletedEmailFix.js - New tests

Testing Done

✅ All Cypress tests pass (including new tests)
✅ PHP CS Fixer passes
✅ Logic verified: multiple delete/reactivate cycles work correctly
✅ Regex pattern tested with various email formats

Reviewer Checklist

Please verify:

  • Prevention logic: Check the regex pattern @.*\.deleted\.[0-9]+$ correctly identifies already-deleted emails
  • No false positives: Confirm emails like john.deleted.smith@example.com are not blocked from deletion
  • Cron job safety: Review that the cron job only fixes genuinely corrupted emails
  • Test coverage: Run Cypress tests to confirm they pass
  • Edge cases: Consider scenarios like:
    • User with email containing .deleted. in username (handled ✅)
    • User deleted multiple times in rapid succession (prevented ✅)
    • Existing users with double suffixes (cron job fixes ✅)

Deployment Notes

After merge:

  1. No immediate action needed - prevention is automatic
  2. Optionally run cron job to fix existing corrupted emails: /cron/fix-double-deleted-emails.php

Add regex check to prevent appending .deleted.<id> suffix if it already
exists. This prevents emails like user@domain.com.deleted.123.deleted.123
when a user is accidentally deleted twice.

Pattern: email NOT REGEXP '@.*\.deleted\.[0-9]+$'
- Only matches .deleted.<digits> after @ symbol at end of email
- Avoids false positives like john.deleted.smith@example.com
- Applied to both admin deletion and self-deletion flows
New cron job to clean up emails that already have multiple .deleted
suffixes from the bug. Can be run manually or scheduled.

The job:
- Finds emails matching pattern: %.deleted.%.deleted.%
- Removes all .deleted.<id> suffixes
- Adds back a single .deleted.<id> suffix
- Logs changes to history table

Also adds routing in gcloud-entry.php for the cron endpoint.
Two tests verify the fix:
1. Normal delete/reactivate flow works correctly
2. Multiple delete/reactivate cycles work without accumulating suffixes

Test 2_9_2 validates the bug fix by performing two complete
delete/reactivate cycles, which would fail without the prevention
fix due to Auth0 rejecting the double-suffixed email.
@vahidbazzaz
Copy link
Contributor Author

@vahidbazzaz vahidbazzaz requested a review from HaGuesto October 11, 2025 13:11
Two improvements to the cleanup cron job:
1. Use REGEXP instead of LIKE to avoid false positives
   Pattern: '@.*\.deleted\.[0-9]+.*\.deleted\.[0-9]+'
   Only matches emails with multiple .deleted.<id> suffixes after @

2. Add 'deleted IS NOT NULL' condition
   Only processes actually deleted users, not active ones
Copy link
Contributor

@pylipp pylipp left a comment

Choose a reason for hiding this comment

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

@vahidbazzaz I'm just quickly checking in on this and noticed that you added a cron job. This I've already implemented on v2 side, I'm sorry that it wasn't very visible there or on the Trello card...

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.

2 participants