mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-08 18:59:11 +00:00
Add ability to block words in usernames (#35407)
This commit is contained in:
parent
8cf7a77808
commit
20bbd20ef1
77
app/controllers/admin/username_blocks_controller.rb
Normal file
77
app/controllers/admin/username_blocks_controller.rb
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::UsernameBlocksController < Admin::BaseController
|
||||||
|
before_action :set_username_block, only: [:edit, :update]
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :username_block, :index?
|
||||||
|
@username_blocks = UsernameBlock.order(username: :asc).page(params[:page])
|
||||||
|
@form = Form::UsernameBlockBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch
|
||||||
|
authorize :username_block, :index?
|
||||||
|
|
||||||
|
@form = Form::UsernameBlockBatch.new(form_username_block_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
|
@form.save
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
flash[:alert] = I18n.t('admin.username_blocks.no_username_block_selected')
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
flash[:alert] = I18n.t('admin.username_blocks.not_permitted')
|
||||||
|
ensure
|
||||||
|
redirect_to admin_username_blocks_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
authorize :username_block, :create?
|
||||||
|
@username_block = UsernameBlock.new(exact: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
authorize @username_block, :update?
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize :username_block, :create?
|
||||||
|
|
||||||
|
@username_block = UsernameBlock.new(resource_params)
|
||||||
|
|
||||||
|
if @username_block.save
|
||||||
|
log_action :create, @username_block
|
||||||
|
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.created_msg')
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize @username_block, :update?
|
||||||
|
|
||||||
|
if @username_block.update(resource_params)
|
||||||
|
log_action :update, @username_block
|
||||||
|
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.updated_msg')
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_username_block
|
||||||
|
@username_block = UsernameBlock.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_username_block_batch_params
|
||||||
|
params
|
||||||
|
.expect(form_username_block_batch: [username_block_ids: []])
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params
|
||||||
|
.expect(username_block: [:username, :comparison, :allow_with_approval])
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
'delete' if params[:delete]
|
||||||
|
end
|
||||||
|
end
|
|
@ -13,6 +13,8 @@ module Admin::ActionLogsHelper
|
||||||
end
|
end
|
||||||
when 'UserRole'
|
when 'UserRole'
|
||||||
link_to log.human_identifier, admin_roles_path(log.target_id)
|
link_to log.human_identifier, admin_roles_path(log.target_id)
|
||||||
|
when 'UsernameBlock'
|
||||||
|
link_to log.human_identifier, edit_admin_username_block_path(log.target_id)
|
||||||
when 'Report'
|
when 'Report'
|
||||||
link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
|
link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
|
||||||
when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain'
|
when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain'
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m791-55-91-91q-48 32-103.5 49T480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-61 17-116.5T146-700l-91-91 57-57 736 736-57 57ZM477-161q45 0 86-11.5t77-33.5l-91-91q-38 17-74.5 47.5T412-168q16 3 32 5t33 2Zm-124-25q35-72 79.5-107t67.5-47q-29-9-58.5-14.5T380-360q-45 0-89 11t-85 31q26 43 63.5 77.5T353-186Zm461-74L690-384q31-10 50.5-36t19.5-60q0-42-29-71t-71-29q-34 0-60 19.5T564-510l-44-44q2-61-41-104.5T374-700L260-814q48-32 103.5-49T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 61-17 116.5T814-260ZM380-420q11 0 20.5-1.5T420-426L246-600q-3 10-4.5 19.5T240-560q0 58 41 99t99 41Z"/></svg>
|
After Width: | Height: | Size: 698 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m791-55-91-91q-48 32-103.5 49T480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-61 17-116.5T146-700l-91-91 57-57 736 736-57 57ZM412-168q26-51 62-81.5t75-47.5L204-642q-21 36-32.5 76.5T160-480q0 45 11.5 86t34.5 76q41-20 85-31t89-11q32 0 61.5 5.5T500-340q-23 12-43.5 28T418-278q-12-2-20.5-2H380q-32 0-63.5 7T256-252q32 32 71.5 53.5T412-168Zm402-92-58-58q21-35 32.5-76t11.5-86q0-134-93-227t-227-93q-45 0-85.5 11.5T318-756l-58-58q48-32 103.5-49T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 61-17 116.5T814-260ZM520-554 374-700q62-2 105 41.5T520-554ZM380-420q-58 0-99-41t-41-99q0-33 14.5-60.5T292-668l196 196q-20 23-47.5 37.5T380-420Zm310 36L564-510q10-31 36-50.5t60-19.5q42 0 71 29t29 71q0 34-19.5 60T690-384ZM537-537ZM423-423Z"/></svg>
|
After Width: | Height: | Size: 843 B |
|
@ -77,6 +77,9 @@ class Admin::ActionLogFilter
|
||||||
update_user_role: { target_type: 'UserRole', action: 'update' }.freeze,
|
update_user_role: { target_type: 'UserRole', action: 'update' }.freeze,
|
||||||
update_ip_block: { target_type: 'IpBlock', action: 'update' }.freeze,
|
update_ip_block: { target_type: 'IpBlock', action: 'update' }.freeze,
|
||||||
unblock_email_account: { target_type: 'Account', action: 'unblock_email' }.freeze,
|
unblock_email_account: { target_type: 'Account', action: 'unblock_email' }.freeze,
|
||||||
|
create_username_block: { target_type: 'UsernameBlock', action: 'create' }.freeze,
|
||||||
|
update_username_block: { target_type: 'UsernameBlock', action: 'update' }.freeze,
|
||||||
|
destroy_username_block: { target_type: 'UsernameBlock', action: 'destroy' }.freeze,
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
attr_reader :params
|
attr_reader :params
|
||||||
|
|
31
app/models/form/username_block_batch.rb
Normal file
31
app/models/form/username_block_batch.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Form::UsernameBlockBatch < Form::BaseBatch
|
||||||
|
attr_accessor :username_block_ids
|
||||||
|
|
||||||
|
def save
|
||||||
|
case action
|
||||||
|
when 'delete'
|
||||||
|
delete!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def username_blocks
|
||||||
|
@username_blocks ||= UsernameBlock.where(id: username_block_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete!
|
||||||
|
verify_authorization(:destroy?)
|
||||||
|
|
||||||
|
username_blocks.each do |username_block|
|
||||||
|
username_block.destroy
|
||||||
|
log_action :destroy, username_block
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_authorization(permission)
|
||||||
|
username_blocks.each { |username_block| authorize(username_block, permission) }
|
||||||
|
end
|
||||||
|
end
|
|
@ -443,7 +443,7 @@ class User < ApplicationRecord
|
||||||
|
|
||||||
def set_approved
|
def set_approved
|
||||||
self.approved = begin
|
self.approved = begin
|
||||||
if sign_up_from_ip_requires_approval? || sign_up_email_requires_approval?
|
if sign_up_from_ip_requires_approval? || sign_up_email_requires_approval? || sign_up_username_requires_approval?
|
||||||
false
|
false
|
||||||
else
|
else
|
||||||
open_registrations? || valid_invitation? || external?
|
open_registrations? || valid_invitation? || external?
|
||||||
|
@ -499,6 +499,10 @@ class User < ApplicationRecord
|
||||||
EmailDomainBlock.requires_approval?(records + [domain], attempt_ip: sign_up_ip)
|
EmailDomainBlock.requires_approval?(records + [domain], attempt_ip: sign_up_ip)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sign_up_username_requires_approval?
|
||||||
|
account.username? && UsernameBlock.matches?(account.username, allow_with_approval: true)
|
||||||
|
end
|
||||||
|
|
||||||
def open_registrations?
|
def open_registrations?
|
||||||
Setting.registrations_mode == 'open'
|
Setting.registrations_mode == 'open'
|
||||||
end
|
end
|
||||||
|
|
62
app/models/username_block.rb
Normal file
62
app/models/username_block.rb
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: username_blocks
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# allow_with_approval :boolean default(FALSE), not null
|
||||||
|
# exact :boolean default(FALSE), not null
|
||||||
|
# normalized_username :string not null
|
||||||
|
# username :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class UsernameBlock < ApplicationRecord
|
||||||
|
HOMOGLYPHS = {
|
||||||
|
'1' => 'i',
|
||||||
|
'2' => 'z',
|
||||||
|
'3' => 'e',
|
||||||
|
'4' => 'a',
|
||||||
|
'5' => 's',
|
||||||
|
'7' => 't',
|
||||||
|
'8' => 'b',
|
||||||
|
'9' => 'g',
|
||||||
|
'0' => 'o',
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
validates :username, presence: true, uniqueness: true
|
||||||
|
|
||||||
|
scope :matches_exactly, ->(str) { where(exact: true).where(normalized_username: str) }
|
||||||
|
scope :matches_partially, ->(str) { where(exact: false).where(Arel::Nodes.build_quoted(str).matches(Arel::Nodes.build_quoted('%').concat(arel_table[:normalized_username]).concat(Arel::Nodes.build_quoted('%')))) }
|
||||||
|
|
||||||
|
before_save :set_normalized_username
|
||||||
|
|
||||||
|
def comparison
|
||||||
|
exact? ? 'equals' : 'contains'
|
||||||
|
end
|
||||||
|
|
||||||
|
def comparison=(val)
|
||||||
|
self.exact = val == 'equals'
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.matches?(str, allow_with_approval: false)
|
||||||
|
normalized_str = str.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS)
|
||||||
|
where(allow_with_approval: allow_with_approval).matches_exactly(normalized_str).or(matches_partially(normalized_str)).any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
username
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_normalized_username
|
||||||
|
self.normalized_username = normalize(username)
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize(str)
|
||||||
|
str.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS)
|
||||||
|
end
|
||||||
|
end
|
19
app/policies/username_block_policy.rb
Normal file
19
app/policies/username_block_policy.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UsernameBlockPolicy < ApplicationPolicy
|
||||||
|
def index?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
end
|
|
@ -28,14 +28,6 @@ class UnreservedUsernameValidator < ActiveModel::Validator
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_username_reserved?
|
def settings_username_reserved?
|
||||||
settings_has_reserved_usernames? && settings_reserves_username?
|
UsernameBlock.matches?(@username, allow_with_approval: false)
|
||||||
end
|
|
||||||
|
|
||||||
def settings_has_reserved_usernames?
|
|
||||||
Setting.reserved_usernames.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def settings_reserves_username?
|
|
||||||
Setting.reserved_usernames.include?(@username.downcase)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
16
app/views/admin/username_blocks/_form.html.haml
Normal file
16
app/views/admin/username_blocks/_form.html.haml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
.fields-group
|
||||||
|
= form.input :username,
|
||||||
|
wrapper: :with_block_label,
|
||||||
|
input_html: { autocomplete: 'new-password', pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT }
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= form.input :comparison,
|
||||||
|
as: :select,
|
||||||
|
wrapper: :with_block_label,
|
||||||
|
collection: %w(equals contains),
|
||||||
|
include_blank: false,
|
||||||
|
label_method: ->(type) { I18n.t(type, scope: 'admin.username_blocks.comparison') }
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= form.input :allow_with_approval,
|
||||||
|
wrapper: :with_label
|
12
app/views/admin/username_blocks/_username_block.html.haml
Normal file
12
app/views/admin/username_blocks/_username_block.html.haml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.batch-table__row
|
||||||
|
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
|
||||||
|
= f.check_box :username_block_ids, { multiple: true, include_hidden: false }, username_block.id
|
||||||
|
.sr-only= username_block.username
|
||||||
|
.batch-table__row__content.pending-account
|
||||||
|
.pending-account__header
|
||||||
|
= t(username_block.exact? ? 'admin.username_blocks.matches_exactly_html' : 'admin.username_blocks.contains_html', string: content_tag(:samp, link_to(username_block.username, edit_admin_username_block_path(username_block))))
|
||||||
|
%br/
|
||||||
|
- if username_block.allow_with_approval?
|
||||||
|
= t('admin.email_domain_blocks.allow_registrations_with_approval')
|
||||||
|
- else
|
||||||
|
= t('admin.username_blocks.block_registrations')
|
10
app/views/admin/username_blocks/edit.html.haml
Normal file
10
app/views/admin/username_blocks/edit.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.username_blocks.edit.title')
|
||||||
|
|
||||||
|
= simple_form_for @username_block, url: admin_username_block_path(@username_block) do |form|
|
||||||
|
= render 'shared/error_messages', object: @username_block
|
||||||
|
|
||||||
|
= render form
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= form.button :button, t('generic.save_changes'), type: :submit
|
26
app/views/admin/username_blocks/index.html.haml
Normal file
26
app/views/admin/username_blocks/index.html.haml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.username_blocks.title')
|
||||||
|
|
||||||
|
- content_for :heading_actions do
|
||||||
|
= link_to t('admin.username_blocks.add_new'), new_admin_username_block_path, class: 'button'
|
||||||
|
|
||||||
|
= form_with model: @form, url: batch_admin_username_blocks_path do |f|
|
||||||
|
= hidden_field_tag :page, params[:page] || 1
|
||||||
|
|
||||||
|
.batch-table
|
||||||
|
.batch-table__toolbar
|
||||||
|
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||||
|
= check_box_tag :batch_checkbox_all, nil, false
|
||||||
|
.batch-table__toolbar__actions
|
||||||
|
= f.button safe_join([material_symbol('close'), t('admin.username_blocks.delete')]),
|
||||||
|
class: 'table-action-link',
|
||||||
|
data: { confirm: t('admin.reports.are_you_sure') },
|
||||||
|
name: :delete,
|
||||||
|
type: :submit
|
||||||
|
.batch-table__body
|
||||||
|
- if @username_blocks.empty?
|
||||||
|
= nothing_here 'nothing-here--under-tabs'
|
||||||
|
- else
|
||||||
|
= render partial: 'username_block', collection: @username_blocks, locals: { f: f }
|
||||||
|
|
||||||
|
= paginate @username_blocks
|
10
app/views/admin/username_blocks/new.html.haml
Normal file
10
app/views/admin/username_blocks/new.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.username_blocks.new.title')
|
||||||
|
|
||||||
|
= simple_form_for @username_block, url: admin_username_blocks_path do |form|
|
||||||
|
= render 'shared/error_messages', object: @username_block
|
||||||
|
|
||||||
|
= render form
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= form.button :button, t('admin.username_blocks.new.create'), type: :submit
|
|
@ -72,6 +72,8 @@ ignore_unused:
|
||||||
- 'preferences.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use
|
- 'preferences.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use
|
||||||
- 'edit_profile.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use
|
- 'edit_profile.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use
|
||||||
- 'admin.terms_of_service.generate' # temporarily disabled
|
- 'admin.terms_of_service.generate' # temporarily disabled
|
||||||
|
- 'admin.username_blocks.matches_exactly_html'
|
||||||
|
- 'admin.username_blocks.contains_html'
|
||||||
|
|
||||||
ignore_inconsistent_interpolations:
|
ignore_inconsistent_interpolations:
|
||||||
- '*.one'
|
- '*.one'
|
||||||
|
|
|
@ -190,6 +190,7 @@ en:
|
||||||
create_relay: Create Relay
|
create_relay: Create Relay
|
||||||
create_unavailable_domain: Create Unavailable Domain
|
create_unavailable_domain: Create Unavailable Domain
|
||||||
create_user_role: Create Role
|
create_user_role: Create Role
|
||||||
|
create_username_block: Create Username Rule
|
||||||
demote_user: Demote User
|
demote_user: Demote User
|
||||||
destroy_announcement: Delete Announcement
|
destroy_announcement: Delete Announcement
|
||||||
destroy_canonical_email_block: Delete Email Block
|
destroy_canonical_email_block: Delete Email Block
|
||||||
|
@ -203,6 +204,7 @@ en:
|
||||||
destroy_status: Delete Post
|
destroy_status: Delete Post
|
||||||
destroy_unavailable_domain: Delete Unavailable Domain
|
destroy_unavailable_domain: Delete Unavailable Domain
|
||||||
destroy_user_role: Destroy Role
|
destroy_user_role: Destroy Role
|
||||||
|
destroy_username_block: Delete Username Rule
|
||||||
disable_2fa_user: Disable 2FA
|
disable_2fa_user: Disable 2FA
|
||||||
disable_custom_emoji: Disable Custom Emoji
|
disable_custom_emoji: Disable Custom Emoji
|
||||||
disable_relay: Disable Relay
|
disable_relay: Disable Relay
|
||||||
|
@ -237,6 +239,7 @@ en:
|
||||||
update_report: Update Report
|
update_report: Update Report
|
||||||
update_status: Update Post
|
update_status: Update Post
|
||||||
update_user_role: Update Role
|
update_user_role: Update Role
|
||||||
|
update_username_block: Update Username Rule
|
||||||
actions:
|
actions:
|
||||||
approve_appeal_html: "%{name} approved moderation decision appeal from %{target}"
|
approve_appeal_html: "%{name} approved moderation decision appeal from %{target}"
|
||||||
approve_user_html: "%{name} approved sign-up from %{target}"
|
approve_user_html: "%{name} approved sign-up from %{target}"
|
||||||
|
@ -255,6 +258,7 @@ en:
|
||||||
create_relay_html: "%{name} created a relay %{target}"
|
create_relay_html: "%{name} created a relay %{target}"
|
||||||
create_unavailable_domain_html: "%{name} stopped delivery to domain %{target}"
|
create_unavailable_domain_html: "%{name} stopped delivery to domain %{target}"
|
||||||
create_user_role_html: "%{name} created %{target} role"
|
create_user_role_html: "%{name} created %{target} role"
|
||||||
|
create_username_block_html: "%{name} added rule for usernames containing %{target}"
|
||||||
demote_user_html: "%{name} demoted user %{target}"
|
demote_user_html: "%{name} demoted user %{target}"
|
||||||
destroy_announcement_html: "%{name} deleted announcement %{target}"
|
destroy_announcement_html: "%{name} deleted announcement %{target}"
|
||||||
destroy_canonical_email_block_html: "%{name} unblocked email with the hash %{target}"
|
destroy_canonical_email_block_html: "%{name} unblocked email with the hash %{target}"
|
||||||
|
@ -268,6 +272,7 @@ en:
|
||||||
destroy_status_html: "%{name} removed post by %{target}"
|
destroy_status_html: "%{name} removed post by %{target}"
|
||||||
destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}"
|
destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}"
|
||||||
destroy_user_role_html: "%{name} deleted %{target} role"
|
destroy_user_role_html: "%{name} deleted %{target} role"
|
||||||
|
destroy_username_block_html: "%{name} removed rule for usernames containing %{target}"
|
||||||
disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}"
|
disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}"
|
||||||
disable_custom_emoji_html: "%{name} disabled emoji %{target}"
|
disable_custom_emoji_html: "%{name} disabled emoji %{target}"
|
||||||
disable_relay_html: "%{name} disabled the relay %{target}"
|
disable_relay_html: "%{name} disabled the relay %{target}"
|
||||||
|
@ -302,6 +307,7 @@ en:
|
||||||
update_report_html: "%{name} updated report %{target}"
|
update_report_html: "%{name} updated report %{target}"
|
||||||
update_status_html: "%{name} updated post by %{target}"
|
update_status_html: "%{name} updated post by %{target}"
|
||||||
update_user_role_html: "%{name} changed %{target} role"
|
update_user_role_html: "%{name} changed %{target} role"
|
||||||
|
update_username_block_html: "%{name} updated rule for usernames containing %{target}"
|
||||||
deleted_account: deleted account
|
deleted_account: deleted account
|
||||||
empty: No logs found.
|
empty: No logs found.
|
||||||
filter_by_action: Filter by action
|
filter_by_action: Filter by action
|
||||||
|
@ -1085,6 +1091,25 @@ en:
|
||||||
other: Used by %{count} people over the last week
|
other: Used by %{count} people over the last week
|
||||||
title: Recommendations & Trends
|
title: Recommendations & Trends
|
||||||
trending: Trending
|
trending: Trending
|
||||||
|
username_blocks:
|
||||||
|
add_new: Add new
|
||||||
|
block_registrations: Block registrations
|
||||||
|
comparison:
|
||||||
|
contains: Contains
|
||||||
|
equals: Equals
|
||||||
|
contains_html: Contains %{string}
|
||||||
|
created_msg: Successfully created username rule
|
||||||
|
delete: Delete
|
||||||
|
edit:
|
||||||
|
title: Edit username rule
|
||||||
|
matches_exactly_html: Equals %{string}
|
||||||
|
new:
|
||||||
|
create: Create rule
|
||||||
|
title: Create new username rule
|
||||||
|
no_username_block_selected: No username rules were changed as none were selected
|
||||||
|
not_permitted: Not permitted
|
||||||
|
title: Username rules
|
||||||
|
updated_msg: Successfully updated username rule
|
||||||
warning_presets:
|
warning_presets:
|
||||||
add_new: Add new
|
add_new: Add new
|
||||||
delete: Delete
|
delete: Delete
|
||||||
|
|
|
@ -160,6 +160,10 @@ en:
|
||||||
name: Public name of the role, if role is set to be displayed as a badge
|
name: Public name of the role, if role is set to be displayed as a badge
|
||||||
permissions_as_keys: Users with this role will have access to...
|
permissions_as_keys: Users with this role will have access to...
|
||||||
position: Higher role decides conflict resolution in certain situations. Certain actions can only be performed on roles with a lower priority
|
position: Higher role decides conflict resolution in certain situations. Certain actions can only be performed on roles with a lower priority
|
||||||
|
username_block:
|
||||||
|
allow_with_approval: Instead of preventing sign-up outright, matching sign-ups will require your approval
|
||||||
|
comparison: Please be mindful of the Scunthorpe Problem when blocking partial matches
|
||||||
|
username: Will be matched regardless of casing and common homoglyphs like "4" for "a" or "3" for "e"
|
||||||
webhook:
|
webhook:
|
||||||
events: Select events to send
|
events: Select events to send
|
||||||
template: Compose your own JSON payload using variable interpolation. Leave blank for default JSON.
|
template: Compose your own JSON payload using variable interpolation. Leave blank for default JSON.
|
||||||
|
@ -371,6 +375,10 @@ en:
|
||||||
name: Name
|
name: Name
|
||||||
permissions_as_keys: Permissions
|
permissions_as_keys: Permissions
|
||||||
position: Priority
|
position: Priority
|
||||||
|
username_block:
|
||||||
|
allow_with_approval: Allow registrations with approval
|
||||||
|
comparison: Method of comparison
|
||||||
|
username: Word to match
|
||||||
webhook:
|
webhook:
|
||||||
events: Enabled events
|
events: Enabled events
|
||||||
template: Payload template
|
template: Payload template
|
||||||
|
|
|
@ -59,6 +59,7 @@ SimpleNavigation::Configuration.run do |navigation|
|
||||||
current_user.can?(:manage_federation)
|
current_user.can?(:manage_federation)
|
||||||
}
|
}
|
||||||
s.item :email_domain_blocks, safe_join([material_symbol('mail'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_path, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.can?(:manage_blocks) }
|
s.item :email_domain_blocks, safe_join([material_symbol('mail'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_path, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.can?(:manage_blocks) }
|
||||||
|
s.item :username_blocks, safe_join([material_symbol('supervised_user_circle_off'), t('admin.username_blocks.title')]), admin_username_blocks_path, highlights_on: %r{/admin/username_blocks}, if: -> { current_user.can?(:manage_blocks) }
|
||||||
s.item :ip_blocks, safe_join([material_symbol('hide_source'), t('admin.ip_blocks.title')]), admin_ip_blocks_path, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.can?(:manage_blocks) }
|
s.item :ip_blocks, safe_join([material_symbol('hide_source'), t('admin.ip_blocks.title')]), admin_ip_blocks_path, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.can?(:manage_blocks) }
|
||||||
s.item :action_logs, safe_join([material_symbol('list'), t('admin.action_logs.title')]), admin_action_logs_path, if: -> { current_user.can?(:view_audit_log) }
|
s.item :action_logs, safe_join([material_symbol('list'), t('admin.action_logs.title')]), admin_action_logs_path, if: -> { current_user.can?(:view_audit_log) }
|
||||||
end
|
end
|
||||||
|
|
|
@ -230,4 +230,10 @@ namespace :admin do
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :software_updates, only: [:index]
|
resources :software_updates, only: [:index]
|
||||||
|
|
||||||
|
resources :username_blocks, except: [:show, :destroy] do
|
||||||
|
collection do
|
||||||
|
post :batch
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,28 +20,6 @@ defaults: &defaults
|
||||||
trends: true
|
trends: true
|
||||||
trends_as_landing_page: true
|
trends_as_landing_page: true
|
||||||
trendable_by_default: false
|
trendable_by_default: false
|
||||||
reserved_usernames:
|
|
||||||
- abuse
|
|
||||||
- account
|
|
||||||
- accounts
|
|
||||||
- admin
|
|
||||||
- administration
|
|
||||||
- administrator
|
|
||||||
- admins
|
|
||||||
- help
|
|
||||||
- helpdesk
|
|
||||||
- instance
|
|
||||||
- mod
|
|
||||||
- moderator
|
|
||||||
- moderators
|
|
||||||
- mods
|
|
||||||
- owner
|
|
||||||
- root
|
|
||||||
- security
|
|
||||||
- server
|
|
||||||
- staff
|
|
||||||
- support
|
|
||||||
- webmaster
|
|
||||||
disallowed_hashtags: # space separated string or list of hashtags without the hash
|
disallowed_hashtags: # space separated string or list of hashtags without the hash
|
||||||
bootstrap_timeline_accounts: ''
|
bootstrap_timeline_accounts: ''
|
||||||
activity_api_enabled: true
|
activity_api_enabled: true
|
||||||
|
|
23
db/migrate/20250717003848_create_username_blocks.rb
Normal file
23
db/migrate/20250717003848_create_username_blocks.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateUsernameBlocks < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
create_table :username_blocks do |t|
|
||||||
|
t.string :username, null: false
|
||||||
|
t.string :normalized_username, null: false
|
||||||
|
t.boolean :exact, null: false, default: false
|
||||||
|
t.boolean :allow_with_approval, null: false, default: false
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :username_blocks, 'lower(username)', unique: true, name: 'index_username_blocks_on_username_lower_btree'
|
||||||
|
add_index :username_blocks, :normalized_username
|
||||||
|
|
||||||
|
reversible do |dir|
|
||||||
|
dir.up do
|
||||||
|
load Rails.root.join('db', 'seeds', '05_blocked_usernames.rb')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
db/schema.rb
13
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_06_27_132728) do
|
ActiveRecord::Schema[8.0].define(version: 2025_07_17_003848) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
|
|
||||||
|
@ -1238,6 +1238,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_27_132728) do
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "username_blocks", force: :cascade do |t|
|
||||||
|
t.string "username", null: false
|
||||||
|
t.string "normalized_username", null: false
|
||||||
|
t.boolean "exact", default: false, null: false
|
||||||
|
t.boolean "allow_with_approval", default: false, null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index "lower((username)::text)", name: "index_username_blocks_on_username_lower_btree", unique: true
|
||||||
|
t.index ["normalized_username"], name: "index_username_blocks_on_normalized_username"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "users", force: :cascade do |t|
|
create_table "users", force: :cascade do |t|
|
||||||
t.string "email", default: "", null: false
|
t.string "email", default: "", null: false
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
|
|
34
db/seeds/05_blocked_usernames.rb
Normal file
34
db/seeds/05_blocked_usernames.rb
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
%w(
|
||||||
|
abuse
|
||||||
|
account
|
||||||
|
accounts
|
||||||
|
admin
|
||||||
|
administration
|
||||||
|
administrator
|
||||||
|
admins
|
||||||
|
help
|
||||||
|
helpdesk
|
||||||
|
instance
|
||||||
|
mod
|
||||||
|
moderator
|
||||||
|
moderators
|
||||||
|
mods
|
||||||
|
owner
|
||||||
|
root
|
||||||
|
security
|
||||||
|
server
|
||||||
|
staff
|
||||||
|
support
|
||||||
|
webmaster
|
||||||
|
).each do |str|
|
||||||
|
UsernameBlock.create_with(username: str, exact: true).find_or_create_by(username: str)
|
||||||
|
end
|
||||||
|
|
||||||
|
%w(
|
||||||
|
mastodon
|
||||||
|
mastadon
|
||||||
|
).each do |str|
|
||||||
|
UsernameBlock.create_with(username: str, exact: false).find_or_create_by(username: str)
|
||||||
|
end
|
7
spec/fabricators/username_block_fabricator.rb
Normal file
7
spec/fabricators/username_block_fabricator.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:username_block) do
|
||||||
|
username { sequence(:email) { |i| "#{i}#{Faker::Internet.username}" } }
|
||||||
|
exact false
|
||||||
|
allow_with_approval false
|
||||||
|
end
|
63
spec/models/username_block_spec.rb
Normal file
63
spec/models/username_block_spec.rb
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe UsernameBlock do
|
||||||
|
describe '.matches?' do
|
||||||
|
context 'when there is an exact block' do
|
||||||
|
before do
|
||||||
|
Fabricate(:username_block, username: 'carriage', exact: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true on exact match' do
|
||||||
|
expect(described_class.matches?('carriage')).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true on case insensitive match' do
|
||||||
|
expect(described_class.matches?('CaRRiagE')).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true on homoglyph match' do
|
||||||
|
expect(described_class.matches?('c4rr14g3')).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false on partial match' do
|
||||||
|
expect(described_class.matches?('foo_carriage')).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false on no match' do
|
||||||
|
expect(described_class.matches?('foo')).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is a partial block' do
|
||||||
|
before do
|
||||||
|
Fabricate(:username_block, username: 'carriage', exact: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true on exact match' do
|
||||||
|
expect(described_class.matches?('carriage')).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true on case insensitive match' do
|
||||||
|
expect(described_class.matches?('CaRRiagE')).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true on homoglyph match' do
|
||||||
|
expect(described_class.matches?('c4rr14g3')).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true on suffix match' do
|
||||||
|
expect(described_class.matches?('foo_carriage')).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true on prefix match' do
|
||||||
|
expect(described_class.matches?('carriage_foo')).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false on no match' do
|
||||||
|
expect(described_class.matches?('foo')).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
97
spec/requests/admin/username_blocks_spec.rb
Normal file
97
spec/requests/admin/username_blocks_spec.rb
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Admin Username Blocks' do
|
||||||
|
describe 'GET /admin/username_blocks' do
|
||||||
|
before { sign_in Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
get admin_username_blocks_path
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /admin/username_blocks' do
|
||||||
|
before { sign_in Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
it 'gracefully handles invalid nested params' do
|
||||||
|
post admin_username_blocks_path(username_block: 'invalid')
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(400)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a username block' do
|
||||||
|
post admin_username_blocks_path(username_block: { username: 'banana', comparison: 'contains', allow_with_approval: '0' })
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to redirect_to(admin_username_blocks_path)
|
||||||
|
expect(UsernameBlock.find_by(username: 'banana'))
|
||||||
|
.to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /admin/username_blocks/batch' do
|
||||||
|
before { sign_in Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
let(:username_blocks) { Fabricate.times(2, :username_block) }
|
||||||
|
|
||||||
|
it 'gracefully handles invalid nested params' do
|
||||||
|
post batch_admin_username_blocks_path(form_username_block_batch: 'invalid')
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to redirect_to(admin_username_blocks_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes selected username blocks' do
|
||||||
|
post batch_admin_username_blocks_path(form_username_block_batch: { username_block_ids: username_blocks.map(&:id) }, delete: '1')
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to redirect_to(admin_username_blocks_path)
|
||||||
|
expect(UsernameBlock.where(id: username_blocks.map(&:id)))
|
||||||
|
.to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /admin/username_blocks/new' do
|
||||||
|
before { sign_in Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
get new_admin_username_block_path
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /admin/username_blocks/:id/edit' do
|
||||||
|
before { sign_in Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
let(:username_block) { Fabricate(:username_block) }
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
get edit_admin_username_block_path(username_block)
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PUT /admin/username_blocks/:id' do
|
||||||
|
before { sign_in Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
let(:username_block) { Fabricate(:username_block, username: 'banana') }
|
||||||
|
|
||||||
|
it 'updates username block' do
|
||||||
|
put admin_username_block_path(username_block, username_block: { username: 'bebebe' })
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to redirect_to(admin_username_blocks_path)
|
||||||
|
expect(username_block.reload.username)
|
||||||
|
.to eq 'bebebe'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,8 +10,13 @@ RSpec.describe UnreservedUsernameValidator do
|
||||||
attr_accessor :username
|
attr_accessor :username
|
||||||
|
|
||||||
validates_with UnreservedUsernameValidator
|
validates_with UnreservedUsernameValidator
|
||||||
|
|
||||||
|
def self.name
|
||||||
|
'Foo'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:record) { record_class.new }
|
let(:record) { record_class.new }
|
||||||
|
|
||||||
describe '#validate' do
|
describe '#validate' do
|
||||||
|
@ -114,7 +119,7 @@ RSpec.describe UnreservedUsernameValidator do
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_reserved_usernames(value)
|
def stub_reserved_usernames(value)
|
||||||
allow(Setting).to receive(:[]).with('reserved_usernames').and_return(value)
|
value&.each { |str| Fabricate(:username_block, username: str, exact: true) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue
Block a user