From 6ca6e310e76473878b843062758d8d4e9370ffe0 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 25 Sep 2026 13:18:11 +0300 Subject: [PATCH 1/6] Show commnity name & link for audit logs --- app/views/admin/_audit_log.html.erb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/admin/_audit_log.html.erb b/app/views/admin/_audit_log.html.erb index 0e80e78ee..c2312da9c 100644 --- a/app/views/admin/_audit_log.html.erb +++ b/app/views/admin/_audit_log.html.erb @@ -26,6 +26,9 @@ <%= t('g.type').capitalize %>: <%= log.log_type.humanize %>
+ <%= t('g.community').capitalize %> + <%= link_to log.community.name, log.community.host %>
+ <%= t('g.user').capitalize %>: <%= user_link(log.user) %>
From 74fda6082cb19263b4d822be33d55f40c0608a1d Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 25 Sep 2026 13:43:37 +0300 Subject: [PATCH 2/6] Add community filter & display to audit logs --- app/assets/javascripts/audit_logs.js | 8 +++++--- app/controllers/admin_controller.rb | 13 +++++++++++++ app/views/admin/audit_logs.html.erb | 29 ++++++++++++++++++++++------ 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/audit_logs.js b/app/assets/javascripts/audit_logs.js index c64246c28..6f6b41fc0 100644 --- a/app/assets/javascripts/audit_logs.js +++ b/app/assets/javascripts/audit_logs.js @@ -1,5 +1,7 @@ document.addEventListener('DOMContentLoaded', () => { - document.querySelectorAll('.js-log-type-select, .js-event-type-select').forEach((el) => { - $(el).select2(); - }); + document + .querySelectorAll(['.js-log-type-select', '.js-event-type-select', '.js-community-select'].join(', ')) + .forEach((el) => { + $(el).select2(); + }); }); diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 80880fcd7..27a513fd7 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -4,6 +4,7 @@ class AdminController < ApplicationController before_action :verify_global_admin, only: [:admin_email, :send_admin_email, :new_site, :create_site, :setup, :setup_save, :failban, :all_email, :send_all_email] before_action :verify_developer, only: [:change_users, :impersonate] + before_action :set_communities, only: [:audit_logs] before_action :set_user, only: [:change_users, :failban, :impersonate] skip_before_action :check_if_warning_or_suspension_pending, only: [:change_back, :verify_elevation] @@ -122,6 +123,10 @@ def audit_logs end end + if params[:community].present? + @logs = @logs.where(community_id: params[:community]) + end + if params[:from].present? @logs = @logs.where('date(created_at) >= ?', params[:from]) end @@ -274,6 +279,14 @@ def do_email_query private + def set_communities + @communities = if current_user&.global_admin? + Community.unscoped.order(name: :asc) + else + Community.none + end + end + def set_user @user = User.find(params[:id]) end diff --git a/app/views/admin/audit_logs.html.erb b/app/views/admin/audit_logs.html.erb index 1b18ccd68..623965ac5 100644 --- a/app/views/admin/audit_logs.html.erb +++ b/app/views/admin/audit_logs.html.erb @@ -10,16 +10,33 @@ <%= form_tag audit_logs_path, method: :get, class: 'form-inline audit-log-filters' do %>
- <%= label_tag :log_type, 'Log category', class: 'form-element' %> - <%= select_tag :log_type, options_for_select(@log_types.map { |lt| [lt, lt] }, selected: params[:log_type]), - include_blank: true, class: 'form-element js-log-type-select' %> + <%= label_tag :log_type, 'Type', class: 'form-element' %> + <%= select_tag :log_type, + options_for_select(@log_types.map { |lt| [lt, lt] }, + selected: params[:log_type]), + include_blank: true, + class: 'form-element js-log-type-select' %>
<%= label_tag :event_type, 'Event', class: 'form-element' %> - <%= select_tag :event_type, options_for_select(@event_types.map { |et| [et, et] }, - selected: params[:event_type]), - include_blank: true, class: 'form-element js-event-type-select' %> + <%= select_tag :event_type, + options_for_select(@event_types.map { |et| [et, et] }, + selected: params[:event_type]), + include_blank: true, + class: 'form-element js-event-type-select' %>
+ + <% if current_user&.global_admin? %> +
+ <%= label_tag :community, 'Community', class: 'form-element' %> + <%= select_tag :community, + options_for_select(@communities.map { |c| [c.name, c.id] }, + selected: params[:community]), + include_blank: true, + class: 'form-element js-community-select' %> +
+ <% end %> +
<%= label_tag :from, 'From date', class: 'form-element' %> <%= date_field_tag :from, params[:from], class: 'form-element' %> From a3a6da49eb374fd22fd6f5ac0fdfed10295e33e3 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 25 Sep 2026 14:03:57 +0300 Subject: [PATCH 3/6] Add tests for the new community filter --- test/controllers/admin_controller_test.rb | 40 +++++++++++++++++++++++ test/fixtures/audit_logs.yml | 8 +++++ 2 files changed, 48 insertions(+) diff --git a/test/controllers/admin_controller_test.rb b/test/controllers/admin_controller_test.rb index 7ea0f47f2..7f117d7d8 100644 --- a/test/controllers/admin_controller_test.rb +++ b/test/controllers/admin_controller_test.rb @@ -132,6 +132,46 @@ class AdminControllerTest < ActionController::TestCase assert_not_nil assigns(:logs) end + test ':audit_logs should correctly check community access' do + first_comm = communities(:sample) + second_comm = communities(:second) + + expected = [ + [:admin, false], + [:global_admin, true] + ] + + expected.each do |test_case| + user = users(test_case.first) + is_unscoped = test_case.second + + sign_in(user) + + get :audit_logs, params: { community: first_comm.id } + assert_response(:success) + @logs = assigns(:logs) + assert_not_nil(@logs) + + assert @logs.any? + assert @logs.all? { |l| l.community.id == first_comm.id } + + get :audit_logs, params: { community: second_comm.id } + assert_response(:success) + @logs = assigns(:logs) + assert_not_nil(@logs) + + if is_unscoped + assert @logs.any? + assert @logs.all? { |l| l.community.id == second_comm.id } + else + assert @logs.none? + end + + sign_out(user) + end + + end + test 'should do email query' do sign_in users(:admin) post :do_email_query, params: { email: users(:standard_user).email } diff --git a/test/fixtures/audit_logs.yml b/test/fixtures/audit_logs.yml index 62b3aae7c..2f94ee3b4 100644 --- a/test/fixtures/audit_logs.yml +++ b/test/fixtures/audit_logs.yml @@ -36,3 +36,11 @@ vote_rate_limit_log: comment: > limit: 10 vote: 1 + +second_community_log: + community: second + log_type: admin_audit + event_type: impersonation_end + related_type: User + related: moderator + user: admin From c49f16531c3fb471eb9abda76d655265ab43f2a6 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 25 Sep 2026 14:07:34 +0300 Subject: [PATCH 4/6] Rubocop fixes for the new tests --- test/controllers/admin_controller_test.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/controllers/admin_controller_test.rb b/test/controllers/admin_controller_test.rb index 7f117d7d8..1de7ce0cd 100644 --- a/test/controllers/admin_controller_test.rb +++ b/test/controllers/admin_controller_test.rb @@ -153,7 +153,7 @@ class AdminControllerTest < ActionController::TestCase assert_not_nil(@logs) assert @logs.any? - assert @logs.all? { |l| l.community.id == first_comm.id } + assert(@logs.all? { |l| l.community.id == first_comm.id }) get :audit_logs, params: { community: second_comm.id } assert_response(:success) @@ -162,14 +162,13 @@ class AdminControllerTest < ActionController::TestCase if is_unscoped assert @logs.any? - assert @logs.all? { |l| l.community.id == second_comm.id } + assert(@logs.all? { |l| l.community.id == second_comm.id }) else assert @logs.none? end sign_out(user) end - end test 'should do email query' do From 0d41a1972b9ccf2d9982d480f6286d52205bad34 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 25 Sep 2026 21:04:20 +0300 Subject: [PATCH 5/6] Restore mod button granting local admin abilities to users --- app/views/users/mod_privileges.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/users/mod_privileges.html.erb b/app/views/users/mod_privileges.html.erb index b8a2d80bc..448920cc8 100644 --- a/app/views/users/mod_privileges.html.erb +++ b/app/views/users/mod_privileges.html.erb @@ -136,7 +136,7 @@

Administrators can edit site settings and user roles.

- <% if @user.global_moderator? %> + <% if @user.admin? %> From 709dbe030c6e92c14cdd5302b576489a2be65782 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 25 Sep 2026 23:25:06 +0300 Subject: [PATCH 6/6] Fix community host with port being interpreted as a scheme --- app/helpers/uri_helper.rb | 22 ++++++++++++++++------ app/views/admin/_audit_log.html.erb | 2 +- test/helpers/uri_helper_test.rb | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 test/helpers/uri_helper_test.rb diff --git a/app/helpers/uri_helper.rb b/app/helpers/uri_helper.rb index 9792da174..a98e6e712 100644 --- a/app/helpers/uri_helper.rb +++ b/app/helpers/uri_helper.rb @@ -1,23 +1,33 @@ module UriHelper [:http, :https].each do |method| # Does a given URI have the relevant scheme? - # @param {String} uri URI to check - # @return {Boolean} check result + # @param uri [String] URI to check + # @return [Boolean] check result define_method "#{method}_uri?" do |uri| URI(uri).scheme.casecmp(method.to_s).zero? end end + # Forces a given URI to be a safe one + # @para uri [String] URI to fixup + # @param scheme [Symbol] scheme to use if the URI is unsafe + # @return [String] safe URI + def force_safe_uri(uri, scheme = :http) + return uri if safe_uri?(uri) + + safe_uri?(uri) ? uri : "#{scheme}://#{uri}" + end + # Is a given URI a relative one? - # @param {String} uri URI to check - # @return {Boolean} check result + # @param uri [String] URI to check + # @return [Boolean] check result def relative_uri?(uri) URI(uri).relative? end # Is a given URI a safe one (absolute http://, https://, or relative)? - # @param {String} uri URI to check - # @return {Boolean} check result + # @param uri [String] URI to check + # @return [Boolean] check result def safe_uri?(uri) relative_uri?(uri) || http_uri?(uri) || https_uri?(uri) end diff --git a/app/views/admin/_audit_log.html.erb b/app/views/admin/_audit_log.html.erb index c2312da9c..aab94ae3d 100644 --- a/app/views/admin/_audit_log.html.erb +++ b/app/views/admin/_audit_log.html.erb @@ -27,7 +27,7 @@ <%= log.log_type.humanize %>
<%= t('g.community').capitalize %> - <%= link_to log.community.name, log.community.host %>
+ <%= link_to log.community.name, force_safe_uri(log.community.host) %>
<%= t('g.user').capitalize %>: <%= user_link(log.user) %>
diff --git a/test/helpers/uri_helper_test.rb b/test/helpers/uri_helper_test.rb new file mode 100644 index 000000000..10551e115 --- /dev/null +++ b/test/helpers/uri_helper_test.rb @@ -0,0 +1,19 @@ +require 'test_helper' + +class UriHelperTest < ActionView::TestCase + include Devise::Test::ControllerHelpers + + test ':force_safe_uri should correctly handle URIs' do + expected = [ + ['/relative', '/relative'], + ['http://example.com', 'http://example.com'], + ['https://example.com', 'https://example.com'], + ['localhost:3000', 'http://localhost:3000'] + ] + + expected.each do |input, expected| + actual = force_safe_uri(input) + assert_equal expected, actual + end + end +end