From 8ba094a0be54c5f86d01615ccaf2bd22144bd893 Mon Sep 17 00:00:00 2001
From: Emi Matchu <emi@matchu.dev>
Date: Sun, 16 Feb 2025 09:32:52 -0800
Subject: [PATCH] Add Support form for users, with shadowban option

---
 .../stylesheets/closet_hangers/_index.sass    |  5 ++-
 app/controllers/closet_hangers_controller.rb  |  8 +++-
 app/controllers/users_controller.rb           | 27 +++++++++----
 app/views/alt_styles/edit.html.haml           |  2 +-
 app/views/closet_hangers/index.html.haml      |  8 ++++
 app/views/items/edit.html.haml                |  2 +-
 app/views/users/edit.html.haml                | 40 +++++++++++++++++++
 config/locales/en.yml                         |  1 +
 config/routes.rb                              |  2 +-
 9 files changed, 81 insertions(+), 14 deletions(-)
 create mode 100644 app/views/users/edit.html.haml

diff --git a/app/assets/stylesheets/closet_hangers/_index.sass b/app/assets/stylesheets/closet_hangers/_index.sass
index 4356da58..32109cf5 100644
--- a/app/assets/stylesheets/closet_hangers/_index.sass
+++ b/app/assets/stylesheets/closet_hangers/_index.sass
@@ -33,9 +33,12 @@ body.closet_hangers-index
     margin-left: 2em
     min-height: $icon-height
 
+    display: flex
+    gap: .5em
+    align-items: center
+
     a
       color: inherit
-      margin-right: .5em
       text-decoration: none
       &:hover
         text-decoration: underline
diff --git a/app/controllers/closet_hangers_controller.rb b/app/controllers/closet_hangers_controller.rb
index 31bf11c7..2e2455b7 100644
--- a/app/controllers/closet_hangers_controller.rb
+++ b/app/controllers/closet_hangers_controller.rb
@@ -218,8 +218,12 @@ class ClosetHangersController < ApplicationController
   def enforce_shadowban
     # If this user is shadowbanned, and this *doesn't* seem to be a request
     # from that user, render the 404 page.
-    if @user.shadowbanned? && !@user.likely_is?(current_user, request.remote_ip)
-      render file: "public/404.html", layout: false, status: :not_found
+    if @user.shadowbanned?
+      can_see = support_staff? ||
+        @user.likely_is?(current_user, request.remote_ip)
+      if !can_see
+        render file: "public/404.html", layout: false, status: :not_found
+      end
     end
   end
 
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 4854976c..c5e636ee 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,5 +1,6 @@
 class UsersController < ApplicationController
-  before_action :find_and_authorize_user!, :only => [:update]
+  before_action :find_and_authorize_user!, only: [:edit, :update]
+  before_action :support_staff_only, only: [:edit]
 
   def index # search, really
     name = params[:name]
@@ -16,6 +17,9 @@ class UsersController < ApplicationController
     @users = User.top_contributors.paginate :page => params[:page], :per_page => 20
   end
 
+  def edit
+  end
+
   def update
     @user.attributes = user_params
     success = @user.save
@@ -42,17 +46,24 @@ class UsersController < ApplicationController
 
   protected
 
+  ALLOWED_ATTRS = [
+    :owned_closet_hangers_visibility,
+    :wanted_closet_hangers_visibility,
+    :contact_neopets_connection_id,
+  ]
   def user_params
-    params.require(:user).permit(:owned_closet_hangers_visibility,
-      :wanted_closet_hangers_visibility, :contact_neopets_connection_id)
+    if support_staff?
+      params.require(:user).permit(
+        *ALLOWED_ATTRS, :name, :shadowbanned, :support_staff
+      )
+    else
+      params.require(:user).permit(*ALLOWED_ATTRS)
+    end
   end
 
   def find_and_authorize_user!
-    if current_user.id == params[:id].to_i
-      @user = current_user
-    else
-      raise AccessDenied
-    end
+    @user = User.find(params[:id])
+    raise AccessDenied unless current_user == @user || support_staff?
   end
 end
 
diff --git a/app/views/alt_styles/edit.html.haml b/app/views/alt_styles/edit.html.haml
index 6daaa8ba..639c36bf 100644
--- a/app/views/alt_styles/edit.html.haml
+++ b/app/views/alt_styles/edit.html.haml
@@ -13,7 +13,7 @@
 
 = image_tag @alt_style.preview_image_url, class: "alt-style-preview"
 
-= support_form_with model: @alt_style, class: "support-form" do |f|
+= support_form_with model: @alt_style do |f|
 	= f.errors
 
 	= f.fields do
diff --git a/app/views/closet_hangers/index.html.haml b/app/views/closet_hangers/index.html.haml
index 2f277ad7..eba2f37f 100644
--- a/app/views/closet_hangers/index.html.haml
+++ b/app/views/closet_hangers/index.html.haml
@@ -31,6 +31,14 @@
         = f.label :contact_neopets_connection_id
         = f.collection_select :contact_neopets_connection_id, @user.neopets_connections, :id, :neopets_username, {include_blank: true}, 'data-new-text' => t('.neopets_username.new'), 'data-new-prompt' => t('.neopets_username.prompt')
         = f.submit t('.neopets_username.submit')
+    - if support_staff?
+      = link_to "✏️ #{t('.support')}", edit_user_path(@user)
+
+- if support_staff? && @user.shadowbanned?
+  %p.warning
+    %strong 🕶️ Shadowbanned:
+    For most users, this page is hidden, but you can still see them because
+    you're Support staff.
 
 - unless public_perspective?
   %noscript
diff --git a/app/views/items/edit.html.haml b/app/views/items/edit.html.haml
index 06d59b20..8eb38d07 100644
--- a/app/views/items/edit.html.haml
+++ b/app/views/items/edit.html.haml
@@ -8,7 +8,7 @@
 	you change something, but it doesn't match what we're seeing on Neopets.com,
 	it will probably be reverted automatically when someone models it.
 
-= support_form_with model: @item, class: "support-form" do |f|
+= support_form_with model: @item do |f|
 	= f.errors
 
 	= f.fields do
diff --git a/app/views/users/edit.html.haml b/app/views/users/edit.html.haml
new file mode 100644
index 00000000..f42ec2ea
--- /dev/null
+++ b/app/views/users/edit.html.haml
@@ -0,0 +1,40 @@
+- title @user.name
+- use_responsive_design
+
+%ol.breadcrumbs
+	%li Users
+	%li= link_to @user.name, user_closet_hangers_path(@user)
+
+= support_form_with model: @user do |f|
+	= f.errors
+
+	= f.fields do
+		= f.field do
+			= f.label :name
+			= f.text_field :name
+
+		= f.radio_fieldset "Item list visibility" do
+			= f.radio_field do
+				= f.radio_button :shadowbanned, false
+				%strong 👁️ Visible:
+				Everyone can see page and trades
+			= f.radio_field do
+				= f.radio_button :shadowbanned, true
+				%strong 🕶️ Shadowbanned:
+				Page and trades hidden from other users/IPs
+
+		= f.radio_fieldset "Account role" do
+			= f.radio_field do
+				= f.radio_button :support_staff, false
+				%strong 👤 User:
+				Can manage their own data
+			= f.radio_field do
+				= f.radio_button :support_staff, true
+				%strong 💖 Support:
+				Can manage other users' data and customization data
+
+	= f.actions do
+		= f.submit "Save changes"
+
+- content_for :stylesheets do
+	= stylesheet_link_tag "application/breadcrumbs", "application/support-form"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 99b53398..de1e1483 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -95,6 +95,7 @@ en:
       item_search_submit: Search
       send_neomail: Neomail %{neopets_username}
       lookup: "%{neopets_username}'s lookup"
+      support: Support
       neopets_username:
         new: "Add username…"
         prompt: "What Neopets username should we add?"
diff --git a/config/routes.rb b/config/routes.rb
index 8e464052..89aebd8c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -55,7 +55,7 @@ OpenneoImpressItems::Application.routes.draw do
   get 'users/top_contributors' => redirect('/users/top-contributors')
 
   # User resources, like their item lists!
-  resources :users, :path => 'user', :only => [:index, :update] do
+  resources :users, :path => 'user', :only => [:index, :edit, :update] do
     resources :contributions, :only => [:index]
     resources :closet_hangers, :only => [:index, :update, :destroy], :path => 'closet' do
       collection do