From 27e23be59d49ef7846235bb9b50ad46f6ccae789 Mon Sep 17 00:00:00 2001 From: JJ Asghar Date: Mon, 15 Sep 2025 10:20:43 -0500 Subject: [PATCH] Added rake user admin tasks. If you wanted to start using campfire for public projects or events you may need to do some user administration. These are some tasks to help make the changes quickly without going to the rails console. Signed-off-by: JJ Asghar --- ADMIN_TASKS.md | 250 +++++++++++++++++++++++++++++++++++++ app/models/user.rb | 51 ++++++++ lib/tasks/admin.rake | 289 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 590 insertions(+) create mode 100644 ADMIN_TASKS.md create mode 100644 lib/tasks/admin.rake 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