diff --git a/ADMIN_TASKS.md b/ADMIN_TASKS.md new file mode 100644 index 000000000..ee9621f50 --- /dev/null +++ b/ADMIN_TASKS.md @@ -0,0 +1,250 @@ +# Admin Rake Tasks + +This document describes the administrative rake tasks available for managing users in the Campfire chat system. + +## Available Tasks + +### User Management + +#### `rake admin:delete_user[identifier,censor]` +Completely removes a user from the system with options for message handling. + +**Parameters:** +- `identifier`: User's email, name, or ID +- `censor`: Either `censor` or `delete` (default: `delete`) + +**Options:** +- `censor`: Replaces all user messages with "[Message content removed by administrator]" +- `delete`: Permanently deletes all user messages + +**Examples:** +```bash +# Delete user and all their messages +rake 'admin:delete_user[user@example.com,delete]' + +# Delete user but censor their messages +rake 'admin:delete_user[john.doe,censor]' + +# Delete user by ID +rake 'admin:delete_user[123,delete]' +``` + +#### `rake admin:disable_user[identifier]` +Deactivates a user without deleting their data. + +**Parameters:** +- `identifier`: User's email, name, or ID + +**What it does:** +- Sets `active` to `false` +- Removes user from all rooms (except direct rooms) +- Clears push subscriptions and searches +- Clears all sessions +- Modifies email address to prevent re-registration + +**Examples:** +```bash +rake 'admin:disable_user[user@example.com]' +rake 'admin:disable_user[john.doe]' +rake 'admin:disable_user[123]' +``` + +#### `rake admin:reset_password[identifier,password]` +Resets a user's password and clears all sessions. + +**Parameters:** +- `identifier`: User's email, name, or ID +- `password`: New password + +**Examples:** +```bash +rake 'admin:reset_password[user@example.com,newpassword123]' +rake 'admin:reset_password[john.doe,securepass]' +``` + +#### `rake admin:lock_user[identifier]` +Locks out a user by deactivating them and clearing all sessions. + +**Parameters:** +- `identifier`: User's email, name, or ID + +**What it does:** +- Deactivates the user +- Clears all active sessions +- Disconnects from ActionCable + +**Examples:** +```bash +rake 'admin:lock_user[user@example.com]' +rake 'admin:lock_user[john.doe]' +``` + +#### `rake admin:unlock_user[identifier]` +Unlocks/reactivates a previously locked user. + +**Parameters:** +- `identifier`: User's email, name, or ID + +**What it does:** +- Reactivates the user (sets `active` to `true`) +- Restores original email address if it was modified during deactivation +- User can log in again + +**Examples:** +```bash +rake 'admin:unlock_user[user@example.com]' +rake 'admin:unlock_user[john.doe]' +``` + +### Information Tasks + +#### `rake admin:list_users` +Lists all users with basic information. + +**Output includes:** +- User ID, name, email, role, active status, message count + +#### `rake admin:show_user[identifier]` +Shows detailed information about a specific user. + +**Parameters:** +- `identifier`: User's email, name, or ID + +**Information displayed:** +- Basic user details (ID, name, email, role, etc.) +- Statistics (messages, rooms, boosts, sessions, etc.) +- Bot-specific information (if applicable) + +#### `rake admin:censor_messages[identifier]` +Censors all messages from a user without deactivating them. + +**Parameters:** +- `identifier`: User's email, name, or ID + +**What it does:** +- Replaces all message content with "[Message content removed by administrator]" +- Preserves message timestamps and other metadata + +## User Identification + +Tasks accept users by: +- **Email address**: `user@example.com` +- **Name**: `John Doe` (partial matches supported) +- **ID**: `123` (numeric only) + +When multiple users match a name search, the task will list all matches and ask for clarification. + +## Safety Features + +- All destructive operations are wrapped in database transactions +- User confirmation is required for destructive operations +- Detailed output shows what will be affected before proceeding +- Sessions are cleared to force re-authentication after password resets + +## Examples + +### Complete User Removal +```bash +# Remove user and delete all messages +rake 'admin:delete_user[spam@example.com,delete]' + +# Remove user but keep censored message history +rake 'admin:delete_user[spam@example.com,censor]' +``` + +### Temporary Suspension +```bash +# Disable user temporarily +rake 'admin:disable_user[problematic@example.com]' + +# Lock out user immediately +rake 'admin:lock_user[problematic@example.com]' + +# Unlock user later +rake 'admin:unlock_user[problematic@example.com]' +``` + +### Password Issues +```bash +# Reset password for locked out user +rake 'admin:reset_password[user@example.com,newpassword123]' +``` + +### Investigation +```bash +# List all users +rake admin:list_users + +# Get detailed user info +rake 'admin:show_user[user@example.com]' + +# Censor messages without deactivating user +rake 'admin:censor_messages[user@example.com]' +``` + +## User Management Concepts + +### Disable vs Lock User + +Understanding when to use `disable_user` vs `lock_user` is important for proper user management: + +#### **Disable User** (`rake admin:disable_user`) +**Comprehensive cleanup** - Removes user from all rooms (except direct rooms) +- **Clears all data** - Removes push subscriptions, searches, and sessions +- **Disconnects from chat** - Closes ActionCable connections +- **Prevents re-registration** - Modifies email address to prevent account reuse +- **Sets inactive status** - Marks user as `active: false` + +**Use case:** Permanent or long-term user removal from the system + +#### **Lock User** (`rake admin:lock_user`) +**Immediate access removal** - Disconnects from ActionCable and clears sessions +- **Sets inactive status** - Marks user as `active: false` +- **Prevents re-registration** - Modifies email address to prevent account reuse +- **Preserves room memberships** - User stays in rooms but can't access them +- **Preserves data** - Keeps push subscriptions, searches, and other data + +**Use case:** Temporary suspension or immediate access restriction + +#### **Key Differences Summary:** + +| Aspect | Disable User | Lock User | +|--------|-------------|-----------| +| **Room Memberships** | ❌ Removed (except direct) | ✅ Preserved | +| **Push Subscriptions** | ❌ Deleted | ✅ Preserved | +| **Search History** | ❌ Deleted | ✅ Preserved | +| **Sessions** | ❌ Deleted | ❌ Deleted | +| **ActionCable** | ❌ Disconnected | ❌ Disconnected | +| **Email Address** | ❌ Modified | ❌ Modified | +| **Active Status** | ❌ False | ❌ False | +| **Reversibility** | ✅ Yes (unlock) | ✅ Yes (unlock) | + +#### **When to Use Each:** + +**Use Disable User when:** +- User is leaving the organization permanently +- You want to completely remove their presence from the system +- GDPR compliance requires data deletion +- User has violated policies and needs complete removal + +**Use Lock User when:** +- Temporary suspension (investigation, vacation, etc.) +- Immediate access restriction needed +- You want to preserve their data for potential restoration +- Quick response to suspicious activity + +**Both Support Unlock:** +Both disabled and locked users can be restored using `rake admin:unlock_user`, which: +- Reactivates the user (`active: true`) +- Restores original email address +- Allows user to log in again + +The main difference is that **disabled users** lose their room memberships and data, while **locked users** retain their memberships and data but just can't access the system. + +## Notes + +- All tasks require the Rails environment to be loaded +- Tasks will exit with error codes if user is not found +- Direct room memberships are preserved during user deletion/deactivation +- Bot users can be managed with the same tasks +- All operations are logged and provide detailed feedback diff --git a/app/models/user.rb b/app/models/user.rb index de5ba4c71..8438d2fdb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -52,6 +52,57 @@ def reset_remote_connections close_remote_connections reconnect: true end + def lock_out! + transaction do + close_remote_connections + sessions.delete_all + update!(active: false, email_address: deactived_email_address) + end + end + + def unlock! + transaction do + update!(active: true) + # Restore original email if it was deactivated + if email_address&.include?("-deactivated-") + original_email = email_address.gsub(/-deactivated-[^@]+@/, "@") + update!(email_address: original_email) if original_email != email_address + end + end + end + + def censor_messages! + transaction do + messages.find_each do |message| + if message.body.present? + message.body.update!(body: "[Message content removed by administrator]") + end + end + end + end + + def admin_delete!(censor_messages: false) + transaction do + if censor_messages + censor_messages! + else + messages.destroy_all + end + + # Remove from all rooms except direct rooms + memberships.without_direct_rooms.delete_all + + # Clean up other associations + push_subscriptions.delete_all + searches.delete_all + sessions.delete_all + boosts.destroy_all + + # Deactivate the user + update!(active: false, email_address: deactived_email_address) + end + end + private def grant_membership_to_open_rooms Membership.insert_all(Rooms::Open.pluck(:id).collect { |room_id| { room_id: room_id, user_id: id } }) diff --git a/lib/tasks/admin.rake b/lib/tasks/admin.rake new file mode 100644 index 000000000..e4fca8937 --- /dev/null +++ b/lib/tasks/admin.rake @@ -0,0 +1,289 @@ +namespace :admin do + desc "Delete a user completely or censor their messages" + task :delete_user, [ :identifier, :censor ] => :environment do |t, args| + identifier = args[:identifier] + censor_messages = args[:censor] == "true" || args[:censor] == "censor" + + if identifier.blank? + puts "Usage: rake admin:delete_user[,]" + puts " censor: Replace all user messages with '[Message content removed by administrator]'" + puts " delete: Permanently delete all user messages" + exit 1 + end + + user = find_user(identifier) + unless user + exit 1 + end + + puts "Found user: #{user.name} (#{user.email_address})" + puts "Role: #{user.role}" + puts "Active: #{user.active?}" + puts "Messages: #{user.messages.count}" + + if censor_messages + puts "\nCensoring #{user.messages.count} messages..." + user.admin_delete!(censor_messages: true) + puts "✓ User deactivated and messages censored" + else + puts "\nDeleting #{user.messages.count} messages..." + user.admin_delete!(censor_messages: false) + puts "✓ User deactivated and messages deleted" + end + + puts "User has been removed from all rooms and deactivated." + end + + desc "Disable/deactivate a user" + task :disable_user, [ :identifier ] => :environment do |t, args| + identifier = args[:identifier] + + if identifier.blank? + puts "Usage: rake admin:disable_user[]" + exit 1 + end + + user = find_user(identifier) + unless user + exit 1 + end + + puts "Found user: #{user.name} (#{user.email_address})" + puts "Current status: #{user.active? ? 'Active' : 'Inactive'}" + + if user.active? + puts "User is already deactivated." + return + end + + puts "Deactivating user..." + user.deactivate + puts "✓ User deactivated successfully" + puts "User has been removed from all rooms and sessions cleared." + end + + desc "Reset a user's password" + task :reset_password, [ :identifier, :password ] => :environment do |t, args| + identifier = args[:identifier] + password = args[:password] + + if identifier.blank? || password.blank? + puts "Usage: rake admin:reset_password[,]" + exit 1 + end + + user = find_user(identifier) + unless user + exit 1 + end + + puts "Found user: #{user.name} (#{user.email_address})" + puts "Resetting password..." + + user.update!(password: password) + puts "✓ Password reset successfully" + + # Clear all sessions to force re-login + user.sessions.delete_all + puts "✓ All user sessions cleared" + end + + desc "Lock out a user (disable + clear all sessions)" + task :lock_user, [ :identifier ] => :environment do |t, args| + identifier = args[:identifier] + + if identifier.blank? + puts "Usage: rake admin:lock_user[]" + exit 1 + end + + user = find_user(identifier) + unless user + exit 1 + end + + puts "Found user: #{user.name} (#{user.email_address})" + puts "Current status: #{user.active? ? 'Active' : 'Inactive'}" + + if user.deactivated? + puts "User is already deactivated." + exit 0 + end + + puts "Locking out user..." + user.lock_out! + puts "✓ User locked out successfully" + puts "User has been deactivated and all sessions cleared." + end + + desc "Unlock/reactivate a user" + task :unlock_user, [ :identifier ] => :environment do |t, args| + identifier = args[:identifier] + + if identifier.blank? + puts "Usage: rake admin:unlock_user[]" + exit 1 + end + + user = find_user(identifier) + unless user + exit 1 + end + + puts "Found user: #{user.name} (#{user.email_address})" + puts "Current status: #{user.active? ? 'Active' : 'Inactive'}" + + if user.active? + puts "User is already active." + exit 0 + end + + puts "Unlocking user..." + user.unlock! + puts "✓ User unlocked successfully" + puts "User has been reactivated and can log in again." + end + + desc "List all users with basic information" + task list_users: :environment do + puts "All Users:" + puts "=" * 80 + printf "%-4s %-20s %-30s %-12s %-8s %-10s\n", "ID", "Name", "Email", "Role", "Active", "Messages" + puts "-" * 80 + + User.order(:id).each do |user| + printf "%-4d %-20s %-30s %-12s %-8s %-10d\n", + user.id, + user.name.truncate(20), + (user.email_address || "").truncate(30), + user.role, + user.active? ? "Yes" : "No", + user.messages.count + end + + puts "\nTotal users: #{User.count}" + puts "Active users: #{User.active.count}" + puts "Inactive users: #{User.where(active: false).count}" + end + + desc "Show detailed information about a user" + task :show_user, [ :identifier ] => :environment do |t, args| + identifier = args[:identifier] + + if identifier.blank? + puts "Usage: rake admin:show_user[]" + exit 1 + end + + user = find_user(identifier) + unless user + exit 1 + end + + puts "User Details:" + puts "=" * 50 + puts "ID: #{user.id}" + puts "Name: #{user.name}" + puts "Email: #{user.email_address || 'Not set'}" + puts "Role: #{user.role}" + puts "Active: #{user.active? ? 'Yes' : 'No'}" + puts "Bio: #{user.bio || 'Not set'}" + puts "Created: #{user.created_at}" + puts "Updated: #{user.updated_at}" + puts "" + puts "Statistics:" + puts " Messages: #{user.messages.count}" + puts " Rooms: #{user.rooms.count}" + puts " Boosts: #{user.boosts.count}" + puts " Sessions: #{user.sessions.count}" + puts " Push Subscriptions: #{user.push_subscriptions.count}" + puts " Searches: #{user.searches.count}" + + if user.bot? + puts " Bot Token: #{user.bot_token}" + puts " Webhook URL: #{user.webhook_url || 'Not set'}" + end + end + + desc "Censor all messages from a user" + task :censor_messages, [ :identifier ] => :environment do |t, args| + identifier = args[:identifier] + + if identifier.blank? + puts "Usage: rake admin:censor_messages[]" + exit 1 + end + + user = find_user(identifier) + unless user + exit 1 + end + + message_count = user.messages.count + puts "Found user: #{user.name} (#{user.email_address})" + puts "Messages to censor: #{message_count}" + + if message_count == 0 + puts "User has no messages to censor." + return + end + + puts "Censoring #{message_count} messages..." + user.censor_messages! + puts "✓ All messages have been censored" + end + + private + + def find_user(identifier) + # Try to find by ID first (if it's numeric) + if identifier.match?(/^\d+$/) + user = User.find_by(id: identifier) + if user + return user + else + puts "No user found with ID: #{identifier}" + return nil + end + end + + # Try to find by email address (including deactivated users) + if identifier.include?("@") + # First try exact match + user = User.find_by(email_address: identifier) + if user + return user + end + + # If not found, try to find by original email (for deactivated users) + # Look for users with deactivated email pattern + deactivated_users = User.where("email_address LIKE ?", "%-deactivated-%") + deactivated_users.each do |u| + original_email = u.email_address.gsub(/-deactivated-[^@]+@/, "@") + if original_email == identifier + return u + end + end + + puts "No user found with email: #{identifier}" + return nil + end + + # Try to find by name (case insensitive, partial match) + users = User.where("LOWER(name) LIKE LOWER(?)", "%#{identifier}%") + + if users.empty? + puts "No user found with name containing: #{identifier}" + nil + elsif users.count == 1 + users.first + else + puts "Multiple users found with name containing '#{identifier}':" + users.each do |user| + puts " #{user.id}: #{user.name} (#{user.email_address})" + end + puts "Please be more specific or use the user ID." + nil + end + end +end