Files
timebank-cc-public/references/PROFILE_DELETION_GRACE_PERIOD.md
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

639 lines
21 KiB
Markdown

# Profile Deletion Grace Period
This document describes the grace period system for profile deletions, which allows profiles to be restored within a configurable timeframe after deletion.
## Overview
When a profile is deleted (either manually by the user or automatically due to inactivity), the system implements a two-phase deletion process:
1. **Soft Deletion**: Profile is marked as deleted but data remains intact
2. **Permanent Deletion**: After the grace period expires, profile data is permanently anonymized and balances are handled
This gives users a safety window to restore their profiles if deleted accidentally or if they change their mind.
## Configuration
The grace period is configured per platform in the timebank configuration files:
**Location:** `config/timebank-*.php`
```php
'delete_profile' => [
'grace_period_days' => 30, // Number of days before permanent deletion
// ... other deletion settings
],
```
**Default Value:** 30 days
**Recommendation:** Keep at least 14 days to give users adequate time to restore their profiles.
## How It Works
### Manual Deletion (User-Initiated)
1. User initiates deletion from profile settings
2. Profile is soft-deleted:
- `deleted_at` timestamp is set to current time
- Balance handling preferences are stored in **Redis cache** (key: `balance_handling_{ModelClass}_{ID}`)
- Cache TTL: grace period + 7 days buffer
- `comment` field remains **empty** for manual deletions
- User is logged out and redirected to goodbye page
3. Confirmation email is sent with grace period information
4. Profile can be restored during grace period
5. After grace period expires, scheduled command permanently deletes the profile
### Auto-Deletion (Inactivity-Based)
1. System detects inactive profile (based on `inactive_at` timestamp)
2. Warning emails are sent before deletion
3. Profile is soft-deleted if user doesn't respond
4. Profile is soft-deleted:
- `deleted_at` timestamp is set to current time
- Balance handling preferences are stored in **Redis cache**
- `comment` field is set to: **"Profile automatically deleted after X days of inactivity."** (translated)
5. Email notification sent about automatic deletion
6. Grace period begins, allowing restoration
7. After grace period expires, permanent deletion occurs
### Permanent Deletion Process
After the grace period expires:
1. **Balance handling preferences are retrieved from Redis cache**:
- Cache key: `balance_handling_{ModelClass}_{ID}`
- If cache found: Execute user's choice (donate or destroy currency)
- **FALLBACK**: If cache lost, **destroy currency** (transfer to debit account)
- Fallback triggers a warning log for monitoring
2. Balance handling is executed:
- If donated: transfers to selected organization account
- If deleted (or fallback): balances transferred to debit account (currency removed from circulation)
3. Profile data is anonymized:
- Email: `removed-{id}@remove.ed`
- Name: `Removed {profiletype} {id}`
- Password: randomized
- Personal data: cleared
- Comment: cleared
4. Profile photo deleted
5. Related data cleaned up (stars, bookmarks, etc.)
6. **Cache is cleared** after successful completion
## Artisan Commands
### Restore Deleted Profile
Restore a profile that was deleted within the grace period.
#### Command Signature
```bash
php artisan profiles:restore {username?} {--list} {--type=}
```
#### Parameters
- `{username?}` - Optional. Username of the profile to restore
- `--list` - Display all profiles eligible for restoration
- `--type=` - Filter by profile type (user, organization, bank, admin)
#### Usage Examples
**List all restorable profiles:**
```bash
php artisan profiles:restore --list
```
**List restorable profiles by type:**
```bash
php artisan profiles:restore --list --type=user
php artisan profiles:restore --list --type=organization
```
**Restore specific profile:**
```bash
php artisan profiles:restore john_doe
```
**Interactive mode (prompts for username):**
```bash
php artisan profiles:restore
```
#### Output Example
```
Available profiles for restoration:
Users:
┌──────────┬───────────────┬──────────────┬─────────────┐
│ Username │ Email │ Deleted │ Time Left │
├──────────┼───────────────┼──────────────┼─────────────┤
│ john_doe │ john@test.com │ 2 days ago │ 28 days │
│ jane_smith│ jane@test.com │ 5 hours ago │ 29 days │
└──────────┴───────────────┴──────────────┴─────────────┘
Organizations:
┌──────────┬───────────────┬──────────────┬─────────────┐
│ Username │ Email │ Deleted │ Time Left │
├──────────┼───────────────┼──────────────┼─────────────┤
│ org_test │ org@test.com │ 1 day ago │ 29 days │
└──────────┴───────────────┴──────────────┴─────────────┘
Total: 3 profile(s) can be restored
```
#### Restoration Process
The command will:
1. Verify the profile exists and is within grace period
2. Check that the profile hasn't been anonymized
3. Clear the `deleted_at` timestamp
4. Clear stored balance handling preferences from `comment` field
5. Log the restoration action
6. Display success message with profile details
#### Error Handling
The command handles various error scenarios:
- **Profile not found**: "Profile 'username' not found"
- **Already active**: "Profile 'username' is not deleted"
- **Grace period expired**: "Profile 'username' cannot be restored (grace period expired)"
- **Already anonymized**: "Profile 'username' has already been permanently deleted"
### Permanent Deletion Command (Scheduled)
This command runs automatically via Laravel scheduler to permanently delete expired profiles.
#### Command Signature
```bash
php artisan profiles:permanently-delete-expired
```
#### Schedule Configuration
**Location:** `app/Console/Kernel.php`
```php
$schedule->command('profiles:permanently-delete-expired')
->daily()
->at('02:30')
->withoutOverlapping()
->appendOutputTo(storage_path('logs/permanent-deletions.log'));
```
**Default Schedule:** Daily at 02:30 AM
#### What It Does
1. Finds all profiles where:
- `deleted_at` is set
- `deleted_at` + grace_period_days < now
- Email is not already anonymized (not deleted_user_*)
2. For each expired profile:
- Retrieves balance handling preferences from `comment` field
- Calls `DeleteUser::permanentlyDelete()` method
- Handles balances according to stored preferences
- Anonymizes profile data
3. Logs all actions to `storage/logs/permanent-deletions.log`
#### Manual Execution
You can manually trigger permanent deletion of expired profiles:
```bash
php artisan profiles:permanently-delete-expired
```
**Warning:** This command is destructive and cannot be undone. Only run manually for testing or if you need to immediately process expired profiles.
## Email Notifications
### User-Deleted Email
Sent immediately when a profile is deleted.
**Template:** `resources/views/emails/administration/user-deleted.blade.php`
**Contains:**
- Account details (username, email, deletion time)
- Balance handling information (if applicable)
- Grace period notification
- Data handling information
**Key Message:**
> "This action will become permanent after {days} days."
### Auto-Deleted Email
Sent when a profile is automatically deleted due to inactivity.
**Template:** Same as user-deleted, with additional fields:
- `autoDeleted: true`
- Deletion reason
- Days of inactivity
## Technical Implementation
### Data Storage During Grace Period
**Profile Models** (User, Organization, Bank, Admin):
- `deleted_at` (datetime): Timestamp when profile was soft-deleted
- `comment` (text):
- **Auto-deletion**: Human-readable message (e.g., "Profile automatically deleted after 120 days of inactivity.")
- **Manual deletion**: Empty (no comment)
**Balance Handling Cache Storage (Redis):**
**Why Cache?**
- Balance handling is stored in cache (not database) to keep the `comment` field human-readable for administrators
- User's balance handling choice (donate vs delete) needs to persist through the grace period
- Balances are handled during permanent deletion (after grace period expires), not during soft delete
- Cache automatically expires after grace period + buffer, self-cleaning the data
- Transactions table provides permanent audit trail of actual balance transfers
**Cache Details:**
- **Key**: `balance_handling_{ModelClass}_{ProfileID}`
- **TTL**: Grace period days + 7 days buffer
- **Structure**:
```php
[
'option' => 'donate|delete',
'donation_account_id' => 123, // null if option is 'delete'
'stored_at' => '2025-12-19 10:30:00'
]
```
**Cache Fallback:**
- If cache is lost during permanent deletion, the system defaults to **destroying currency** (transfer to debit account)
- A warning is logged: `Balance handling cache lost for profile deletion`
- This ensures no orphaned balances remain in the system
- Transaction table will show the fallback action for audit purposes
### Translation Keys
**Auto-deletion comment** (`resources/lang/*.json`):
- English: `"Profile automatically deleted after :days days of inactivity."`
- Dutch: `"Profiel automatisch verwijderd na :days dagen inactiviteit."`
- French: `"Profil automatiquement supprimé après :days jours d'inactivité."`
- Spanish: `"Perfil eliminado automáticamente después de :days días de inactividad."`
- German: `"Profil automatisch nach :days Tagen Inaktivität gelöscht."`
### Key Classes and Files
#### Actions
**`app/Actions/Jetstream/DeleteUser.php`**
- `delete()`: Soft deletes profile, stores balance preferences in Redis cache
- `permanentlyDelete()`: Retrieves balance preferences from cache, handles balances, anonymizes profile, clears cache
**`app/Actions/Jetstream/RestoreProfile.php`**
- `restore()`: Restores a soft-deleted profile within grace period
#### Commands
**`app/Console/Commands/RestoreDeletedProfile.php`**
- Interactive CLI for restoring profiles
- Lists eligible profiles
- Handles restoration process
**`app/Console/Commands/PermanentlyDeleteExpiredProfiles.php`**
- Scheduled task for permanent deletion
- Processes expired profiles
- Logs all actions
#### Livewire Components
**`app/Http/Livewire/Profile/DeleteUserForm.php`**
- `deleteUser()`: Handles user-initiated deletion
- Stores balance handling options
- Manages logout and session
- Redirects to goodbye page
### Views
**`resources/views/goodbye-deleted-user.blade.php`**
- Displayed after manual deletion
- Shows deletion confirmation and grace period info
### Routes
**Route:** `/goodbye-deleted-user` (Named: `goodbye-deleted-user`)
- Displays goodbye page after deletion
- Requires `session('result')` data
## Testing
### Test Manual Deletion
1. Set grace period to 1 day in config for testing:
```php
'grace_period_days' => 1,
```
2. Delete a test profile from profile settings
3. Verify:
- Goodbye page displays
- Email received with grace period info
- Profile listed in `php artisan profiles:restore --list`
4. Restore the profile:
```bash
php artisan profiles:restore test_user
```
5. Verify profile is active and can log in
### Test Automatic Deletion
1. Run the permanent deletion command after grace period:
```bash
php artisan profiles:permanently-delete-expired
```
2. Verify:
- Profile is permanently deleted
- Data is anonymized
- Balances handled according to preferences
- Logged in `storage/logs/permanent-deletions.log`
### Test Edge Cases
**Grace period expired:**
```bash
php artisan profiles:restore expired_user
# Should show: "Profile cannot be restored (grace period expired)"
```
**Already anonymized:**
```bash
php artisan profiles:restore permanently_deleted_user
# Should show: "Profile has already been permanently deleted"
```
**Non-existent profile:**
```bash
php artisan profiles:restore nonexistent
# Should show: "Profile 'nonexistent' not found"
```
## Best Practices
### For Administrators
1. **Monitor grace period activity:**
- Check `storage/logs/permanent-deletions.log` regularly
- Review restoration requests
- Adjust grace period based on user feedback
2. **Backup before major changes:**
- Database backup before reducing grace period
- Test restoration process in staging first
3. **User communication:**
- Clearly communicate grace period in deletion UI
- Include grace period in Terms of Service
- Send reminder emails before permanent deletion (optional enhancement)
### For Developers
1. **Never bypass grace period:**
- Always use `DeleteUser@delete()` for deletions
- Never directly set `deleted_at` without storing balance preferences
- Use `DeleteUser@permanentlyDelete()` only after grace period
2. **Preserve balance preferences:**
- Always store in `comment` field as JSON
- Include `stored_at` timestamp
- Validate before permanent deletion
3. **Testing:**
- Use short grace period (1 day) in development
- Test both restoration and permanent deletion
- Verify email notifications
4. **Cache management:**
- Clear account balance cache when restoring
- Invalidate session data appropriately
- Refresh Elasticsearch indices if needed
## Troubleshooting
### Profile shows as deleted but can't be restored
**Possible causes:**
1. Grace period already expired
2. Profile already permanently deleted (email starts with `deleted_user_`)
3. Database inconsistency
**Solution:**
```bash
# Check profile status in database
php artisan tinker
>>> $user = App\Models\User::withTrashed()->where('name', 'username')->first();
>>> $user->deleted_at
>>> $user->email
>>> Carbon\Carbon::parse($user->deleted_at)->addDays(config('timebank_cc.delete_profile.grace_period_days'))
```
### Balance handling preferences lost
**Possible causes:**
1. `comment` field was cleared manually
2. Profile updated after deletion
**Solution:**
- Check database for `comment` field content
- If lost, you'll need to manually decide balance handling during restoration
### Permanent deletion not running
**Possible causes:**
1. Laravel scheduler not configured
2. Command has errors
3. No expired profiles
**Solution:**
```bash
# Check scheduler is running
php artisan schedule:list
# Run manually to see errors
php artisan profiles:permanently-delete-expired
# Check logs
tail -f storage/logs/laravel.log
tail -f storage/logs/permanent-deletions.log
```
### Restored profile can't log in
**Possible causes:**
1. Cache not cleared
2. Session data corrupted
3. Password was changed during soft deletion
**Solution:**
```bash
# Clear all caches
php artisan cache:clear
php artisan view:clear
php artisan config:clear
# Reset password if needed
php artisan tinker
>>> $user = App\Models\User::where('name', 'username')->first();
>>> $user->password = Hash::make('newpassword');
>>> $user->save();
```
## Future Enhancements
Potential improvements to the grace period system:
1. **Reminder emails:**
- Send email 7 days before permanent deletion
- Send final warning 24 hours before deletion
2. **Self-service restoration:**
- Allow users to restore their own profiles via email link
- Magic link authentication for deleted accounts
3. **Extended grace for specific cases:**
- Longer grace period for profiles with high balance
- Longer grace for organization/bank profiles
4. **Audit trail:**
- Track who restored profiles and when
- Log reason for restoration
- Activity log entries for all grace period actions
5. **Dashboard for admins:**
- View all profiles in grace period
- Bulk restore operations
- Analytics on deletion patterns
### WireChat Message Handling
All WireChat messages are eventually deleted to prevent orphaned data. When a profile is permanently deleted, their sent messages are released for cleanup.
#### Configuration
**Location:** `config/timebank-*.php`
```php
'wirechat' => [
'disappearing_messages' => [
'allow_users_to_keep' => true, // Allow marking messages as "kept"
'duration' => 30, // Days before regular messages deleted
'kept_messages_duration' => 90, // Days before kept messages deleted
'cleanup_schedule' => 'everyFiveMinutes',
],
'profile_deletion' => [
'release_kept_messages' => true, // MUST be true to prevent orphans
],
],
```
**Note:** Disappearing messages are ALWAYS enabled. There is no 'enabled' flag - messages are always cleaned up to prevent orphaned data from deleted profiles.
#### Behavior
**All messages are eventually deleted:**
- Regular messages: Deleted after `duration` days (default: 30 days)
- Kept messages: Deleted after `kept_messages_duration` days (default: 90 days)
- Messages from deleted profiles: Released and then deleted after `duration` days
**When profile is permanently deleted:**
1. Profile enters grace period (default: 30 days)
2. Grace period expires → permanent deletion runs
3. All kept messages sent by that profile: `kept_at = null` (released)
4. Messages become regular messages, deleted after `duration` days
5. This prevents orphaned messages with invalid `sendable_id`
#### Timeline Example
**With default settings:**
- `duration = 30` days
- `kept_messages_duration = 90` days
- `release_kept_messages = true`
```
Scenario 1: Normal message lifecycle
Day 0: User sends message
Day 30: Message automatically deleted by DeleteExpiredWireChatMessagesJob
Scenario 2: Kept message lifecycle
Day 0: User sends message, recipient marks as "kept"
Day 90: Kept message automatically deleted by cleanup job
Scenario 3: Sender deletes profile
Day 0: User sends message, recipient marks as "kept"
Day 365: User deletes profile → grace period starts
Day 395: Grace period expires → permanent deletion
→ kept_at set to null (message "released")
→ Message now subject to normal 30-day duration
Day 425: Message deleted by cleanup job (30 days after release)
```
#### Why This Prevents Orphaned Messages
**Without `release_kept_messages`:**
- Deleted profile has `sendable_id = 123`, `sendable_type = 'App\Models\User'`
- Profile anonymized: name becomes "Removed user abc123ef"
- Kept messages still reference old `sendable_id` pointing to anonymized profile
- Messages orphaned forever with no way to trace original sender
- Database accumulates orphaned data indefinitely
**With `release_kept_messages = true`:**
- Kept messages released when profile permanently deleted
- Normal cleanup job deletes them after `duration` days
- No orphaned data remains in database
- Clean data retention without privacy concerns
- Conversation history maintained for recipients until cleanup
#### Configuration Notes
- `release_kept_messages` should ALWAYS be `true` (default and recommended)
- Setting it to `false` will cause orphaned messages to accumulate
- All durations specified in DAYS (converted to seconds internally)
- Cleanup job runs based on `cleanup_schedule` (default: every 5 minutes)
- User control is hardcoded to `false` (users cannot change message duration)
- Disappearing messages are hardcoded to enabled (always active)
## Related Documentation
- **Profile Auto-Delete:** `references/PROFILE_AUTO_DELETE.md` - Automatic deletion based on inactivity
- **Profile Inactive Config:** `references/PROFILE_INACTIVE_CONFIG.md` - Configuration for inactive profile detection
- **Email Testing:** `references/EMAIL-TESTING-GUIDE.md` - Testing email notifications
## Changelog
### Version 1.1 (2025-12-19)
- **BREAKING CHANGE**: Balance handling data moved from `comment` field to Redis cache
- Added cache fallback: automatically destroys currency if cache is lost
- Comment field now human-readable:
- Auto-deletion: Shows translated message "Profile automatically deleted after X days of inactivity"
- Manual deletion: Empty (no comment)
- Added translation keys for auto-deletion comments in 5 languages (en, nl, fr, de, es)
- Enhanced logging: Warning logged when cache fallback is triggered
- Cache automatically expires after grace period + 7 days buffer
- Transaction table provides complete audit trail of balance handling
### Version 1.0 (2025-12-19)
- Initial implementation of grace period system
- Two-phase deletion process (soft delete + permanent delete)
- Restore command with interactive and list modes
- Scheduled permanent deletion command
- Email notifications with grace period information
- Balance handling deferred until permanent deletion