From 054e15e4f03eecb174374466581b9662a6b38e24 Mon Sep 17 00:00:00 2001 From: Fire Demon Date: Tue, 28 Jul 2020 20:40:25 -0500 Subject: [Privacy] Add options for private accounts --- app/controllers/accounts_controller.rb | 24 +++++++++---- app/controllers/activitypub/outboxes_controller.rb | 11 +++--- app/controllers/activitypub/replies_controller.rb | 8 ++--- .../api/v1/accounts/statuses_controller.rb | 6 ++-- app/controllers/application_controller.rb | 42 +++++++++++++++++++++- app/controllers/settings/profiles_controller.rb | 2 +- app/controllers/statuses_controller.rb | 8 +++-- app/controllers/tags_controller.rb | 12 +++---- app/lib/activitypub/adapter.rb | 2 ++ app/models/account.rb | 2 ++ app/models/status.rb | 2 +- app/policies/status_policy.rb | 6 ++-- app/serializers/activitypub/actor_serializer.rb | 4 +-- .../activitypub/process_account_service.rb | 2 ++ app/views/settings/profiles/show.html.haml | 12 +++++-- config/locales/simple_form.en-MP.yml | 6 +++- .../20200728171900_add_private_to_accounts.rb | 7 ++++ .../20200728173757_add_require_auth_to_accounts.rb | 7 ++++ db/schema.rb | 4 ++- 19 files changed, 123 insertions(+), 44 deletions(-) create mode 100644 db/migrate/20200728171900_add_private_to_accounts.rb create mode 100644 db/migrate/20200728173757_add_require_auth_to_accounts.rb diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 651da89ad..ebc472087 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -10,6 +10,8 @@ class AccountsController < ApplicationController before_action :set_cache_headers before_action :set_body_classes + before_action :require_authenticated!, if: -> { @account.require_auth? || @account.private? } + skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) } skip_before_action :require_functional! #, unless: :whitelist_mode? @@ -20,10 +22,10 @@ class AccountsController < ApplicationController expires_in 0, public: true unless user_signed_in? || signed_request_account.present? @pinned_statuses = [] - @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) - @featured_hashtags = @account.featured_tags.order(statuses_count: :desc) + @endorsed_accounts = unauthorized? ? [] : @account.endorsed_accounts.to_a.sample(4) + @featured_hashtags = unauthorized? ? [] : @account.featured_tags.order(statuses_count: :desc) - if current_account && @account.blocking?(current_account) + if unauthorized? @statuses = [] return end @@ -40,7 +42,9 @@ class AccountsController < ApplicationController end format.rss do - expires_in 1.minute, public: !(user_signed_in? || signed_request_account.present?) + return forbidden if unauthorized? + + expires_in 1.minute, public: !current_account? limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE @statuses = filtered_statuses.without_reblogs.limit(limit) @@ -49,7 +53,7 @@ class AccountsController < ApplicationController end format.json do - expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?) + expires_in 3.minutes, public: !current_account? render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to end end @@ -152,10 +156,18 @@ class AccountsController < ApplicationController end def restrict_fields_to - if signed_request_account.present? || public_fetch_mode? + if signed_request_account.present? && !blocked? # Return all fields else %i(id type preferred_username inbox public_key endpoints) end end + + def blocked? + @blocked ||= current_account && @account.blocking?(current_account) + end + + def unauthorized? + @unauthorized ||= blocked? || (@account.private? && !following?(@account)) + end end diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 60f1c526b..c4c0ce0c9 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -10,9 +10,12 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController before_action :set_statuses before_action :set_cache_headers + before_action :require_authenticated!, if: -> { @account.require_auth? } + before_action -> { require_following!(@account) }, if: -> { @account.private? } + def show expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?)) - render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', target_domain: signed_request_account&.domain + render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', target_domain: current_account&.domain end private @@ -49,7 +52,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def set_statuses return unless page_requested? - @statuses = if known_visitor? + @statuses = if authenticated_or_following?(@account) @account.statuses.without_semiprivate.permitted_for(@account, signed_request_account) else @account.statuses.permitted_for(@account, signed_request_account, user_signed_in: true) @@ -66,8 +69,4 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def page_params { page: true, max_id: params[:max_id], min_id: params[:min_id] }.compact end - - def known_visitor? - @known_visitor ||= user_signed_in? || (signed_request_account.present? && signed_request_account.following?(@account)) - end end diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index cec571e8a..4d553fc07 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -14,7 +14,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController def index expires_in 0, public: public_fetch_mode? - render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true, target_domain: signed_request_account&.domain + render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true, target_domain: current_account&.domain end private @@ -33,7 +33,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController def set_replies @replies = only_other_accounts? ? Status.where.not(account_id: @account.id) : @account.statuses @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) - @replies = @replies.without_semiprivate unless known_visitor? + @replies = @replies.without_semiprivate unless authenticated_or_following?(@account) @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) end @@ -78,8 +78,4 @@ class ActivityPub::RepliesController < ActivityPub::BaseController def page_params params_slice(:only_other_accounts, :min_id).merge(page: true) end - - def known_visitor? - @known_visitor ||= user_signed_in? || (signed_request_account.present? && signed_request_account.following?(@account)) - end end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 4735fea8c..1c744ad73 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -26,6 +26,8 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def account_statuses + return [] if (@account.private && !following?(@account)) || (@account.require_auth && !current_account?) + statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses statuses.merge!(only_media_scope) if truthy_param?(:only_media) @@ -37,7 +39,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def permitted_account_statuses - @account.statuses.permitted_for(@account, current_account, user_signed_in: user_signed_in?) + @account.statuses.permitted_for(@account, current_account, user_signed_in: authenticated_or_following?(@account)) end def only_media_scope @@ -49,7 +51,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController # Also, Avoid getting slow by not narrowing down by `statuses.account_id`. # When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used # and the table will be joined by `Merge Semi Join`, so the query will be slow. - @account.statuses.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account) + @account.statuses.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account, user_signed_in: authenticated_or_following?(@account)) .paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) .reorder(id: :desc).distinct(:id).pluck(:id) end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e996c2217..9608f1cf9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base include SessionTrackingConcern include CacheConcern include DomainControlHelper + include SignatureVerification helper_method :current_account helper_method :current_session @@ -71,6 +72,28 @@ class ApplicationController < ActionController::Base redirect_to edit_user_registration_path unless current_user.functional? end + def require_authenticated! + return if current_account? + + respond_to do |format| + format.any { redirect_to edit_user_registration_path } + format.json { forbidden } + end + end + + def require_known!(account) + return if authenticated_or_following?(account) + + respond_to do |format| + format.any { redirect_to edit_user_registration_path } + format.json { forbidden } + end + end + + def require_following!(account) + forbidden unless following?(account) + end + def after_sign_out_path_for(_resource_or_scope) new_user_session_path end @@ -197,7 +220,7 @@ class ApplicationController < ActionController::Base def current_account return @current_account if defined?(@current_account) - @current_account = current_user&.account + @current_account = current_user&.account.presence || signed_request_account end def current_session @@ -225,4 +248,21 @@ class ApplicationController < ActionController::Base format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code } end end + + def following?(account) + return if account.blank? + + @account_following ||= {} + return @account_following[account.id] if @account_following[account.id].present? + + @account_following[account.id] = current_account.present? && (current_account.id == account.id || current_account.following?(account)) + end + + def authenticated_or_following?(account) + current_user.functional? || following?(account) + end + + def current_account? + current_account.present? + end end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index d6e3c9863..8c4efa21d 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -24,7 +24,7 @@ class Settings::ProfilesController < Settings::BaseController def account_params params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, - :require_dereference, :show_replies, :show_unlisted, + :require_dereference, :show_replies, :show_unlisted, :private, :require_auth, fields_attributes: [:name, :value]) end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 23cbb8c37..5c977d212 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -9,6 +9,8 @@ class StatusesController < ApplicationController layout 'public' before_action :require_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :require_authenticated!, if: -> { @account.require_auth? } + before_action -> { require_following!(@account) }, if: -> { request.format != :json && @account.private? } before_action :set_status before_action :set_instance_presenter before_action :set_link_headers @@ -19,7 +21,7 @@ class StatusesController < ApplicationController before_action :set_autoplay, only: :embed skip_around_action :set_locale, if: -> { request.format == :json } - skip_before_action :require_functional!, only: [:show, :embed] #, unless: :whitelist_mode? + skip_before_action :require_functional!, only: [:show, :embed] # , unless: :whitelist_mode? content_security_policy only: :embed do |p| p.frame_ancestors(false) @@ -37,7 +39,7 @@ class StatusesController < ApplicationController format.json do expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? - render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter, target_domain: signed_request_account&.domain + render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter, target_domain: current_account&.domain end end end @@ -48,7 +50,7 @@ class StatusesController < ApplicationController content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter, - target_domain: signed_request_account&.domain + target_domain: current_account&.domain end def embed diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 9cba38771..368419ef5 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -9,13 +9,13 @@ class TagsController < ApplicationController layout 'public' before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - #before_action :authenticate_user!, if: :whitelist_mode? + # before_action :authenticate_user!, if: :whitelist_mode? before_action :set_tag before_action :set_local before_action :set_body_classes before_action :set_instance_presenter - skip_before_action :require_functional! #, unless: :whitelist_mode? + skip_before_action :require_functional! # , unless: :whitelist_mode? def show respond_to do |format| @@ -38,11 +38,11 @@ class TagsController < ApplicationController expires_in 3.minutes, public: public_fetch_mode? @statuses = HashtagQueryService.new.call(@tag, filter_params, current_account, @local) - @statuses = @statuses.without_semiprivate unless known_visitor? + @statuses = @statuses.without_semiprivate unless authenticated_or_following?(@account) @statuses = @statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id]) @statuses = cache_collection(@statuses, Status) - render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', target_domain: signed_request_account&.domain + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', target_domain: current_account&.domain end end end @@ -77,8 +77,4 @@ class TagsController < ApplicationController def filter_params params.slice(:any, :all, :none).permit(:any, :all, :none) end - - def known_visitor? - @known_visitor ||= user_signed_in? || (signed_request_account.present? && signed_request_account.following?(@account)) - end end diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index ef46ae4ae..fb8257c77 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -12,6 +12,8 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base require_dereference: { 'mp' => 'http://the.monsterpit.net/ns#', 'requireDereference' => 'mp:requireDereference' }, show_replies: { 'mp' => 'http://the.monsterpit.net/ns#', 'showReplies' => 'mp:showReplies' }, show_unlisted: { 'mp' => 'http://the.monsterpit.net/ns#', 'showUnlisted' => 'mp:showUnlisted' }, + private: { 'mp' => 'http://the.monsterpit.net/ns#', 'private' => 'mp:private' }, + require_auth: { 'mp' => 'http://the.monsterpit.net/ns#', 'requireAuth' => 'mp:requireAuth' }, manually_approves_followers: { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers' }, sensitive: { 'sensitive' => 'as:sensitive' }, hashtag: { 'Hashtag' => 'as:Hashtag' }, diff --git a/app/models/account.rb b/app/models/account.rb index 860f93f03..7d19aac08 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -53,6 +53,8 @@ # require_dereference :boolean default(FALSE), not null # show_replies :boolean default(TRUE), not null # show_unlisted :boolean default(TRUE), not null +# private :boolean default(FALSE), not null +# require_auth :boolean default(FALSE), not null # class Account < ApplicationRecord diff --git a/app/models/status.rb b/app/models/status.rb index 33e566737..a5f133506 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -234,7 +234,7 @@ class Status < ApplicationRecord end def hidden? - !distributable? + !published? || !distributable? end def distributable? diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 69d18c4bf..317f450eb 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -91,13 +91,13 @@ class StatusPolicy < ApplicationPolicy end def author_blocking? - return false if current_account.nil? + return author.require_auth? if current_account.nil? @preloaded_relations[:blocked_by] ? @preloaded_relations[:blocked_by][author.id] : author.blocking?(current_account) end def parent_author_blocking? - return false if current_account.nil? || parent_author.nil? + return parent_author&.require_auth? if current_account.nil? || parent_author.nil? @preloaded_relations[:blocked_by] ? @preloaded_relations[:blocked_by][parent_author.id] : parent_author.blocking?(current_account) end @@ -162,7 +162,7 @@ class StatusPolicy < ApplicationPolicy end def public_conversation? - @public_conversation ||= (record.conversation&.public? || false) + @public_conversation ||= record.conversation&.public? || false end def visibility_for_remote_domain diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 5a63d15ea..8e5e5b4bb 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -24,8 +24,8 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer attribute :moved_to, if: :moved? attribute :also_known_as, if: :also_known_as? - context_extensions :require_dereference, :show_replies - attributes :require_dereference, :show_replies, :show_unlisted + context_extensions :require_dereference, :show_replies, :private, :require_auth + attributes :require_dereference, :show_replies, :show_unlisted, :private, :require_auth class EndpointsSerializer < ActivityPub::Serializer include RoutingHelper diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 2a5980c79..39e777b32 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -89,6 +89,8 @@ class ActivityPub::ProcessAccountService < BaseService @account.require_dereference = @json['requireDereference'] || false @account.show_replies = @json['showReplies'] || true @account.show_unlisted = @json['showUnlisted'] || true + @account.private = @json['private'] || false + @account.require_auth = @json['require_auth'] || false end def set_fetchable_attributes! diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index 828b3ee4c..1b7765f32 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -23,9 +23,6 @@ %hr.spacer/ - .fields-group - = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked') - .fields-group = f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot') @@ -33,6 +30,9 @@ %p.hint= t 'settings.profiles.privacy_html' + .fields-group + = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked') + - if Setting.profile_directory .fields-group = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable') @@ -43,6 +43,12 @@ .fields-group = f.input :show_unlisted, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.show_unlisted') + .fields-group + = f.input :private, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.private') + + .fields-group + = f.input :require_auth, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.require_auth') + %h4= t 'settings.profiles.advanced_privacy' %p.hint= t 'settings.profiles.advanced_privacy_html' diff --git a/config/locales/simple_form.en-MP.yml b/config/locales/simple_form.en-MP.yml index 21a614794..671cbda0c 100644 --- a/config/locales/simple_form.en-MP.yml +++ b/config/locales/simple_form.en-MP.yml @@ -13,7 +13,9 @@ en-MP: defaults: irreversible: Filtered roars will disappear irreversibly, even if filter is later removed phrase: Will be matched regardless of casing in text or content warning of a roar - require_dereference_html: "When enabled, Monsterpit will deliver your roars to other servers as pointers and require an authenticated request to access their (non-public) content. This allows permissions and blocks you've set to be enforced more stringently. Note that only the lastest versions of Mastodon and Glitch-Soc servers are compatable with this feature." + private: Only allow authenticated followers to view your local profile. + require_auth: Require viewers to log in to access your profile, roars, and threads from Monsterpit. + require_dereference_html: "When enabled, Monsterpit will deliver your roars to other servers as pointers and require an authenticated request to access their (non-public) content. This allows permissions and blocks you've set to be enforced more stringently. This feature will make your roars inaccessible from Mastodon servers older than 3.2.0." setting_aggregate_reblogs: Do not show new boosts for roars that have been recently boosted (only affects newly-received boosts) setting_default_content_type_html: When composing roars, assume they are written in raw HTML, unless specified otherwise setting_default_content_type_markdown: When composing roars, assume they are using Markdown for rich text formatting, unless specified otherwise @@ -37,6 +39,8 @@ en-MP: include_statuses: Include reported roars in the e-mail defaults: bot: This is an automated account + private: Private mode + require_auth: Disallow anonymous access require_dereference: Indirect federation mode setting_crop_images: Crop images in non-expanded roars to 16x9 setting_default_content_type: Default format for roars diff --git a/db/migrate/20200728171900_add_private_to_accounts.rb b/db/migrate/20200728171900_add_private_to_accounts.rb new file mode 100644 index 000000000..482d09576 --- /dev/null +++ b/db/migrate/20200728171900_add_private_to_accounts.rb @@ -0,0 +1,7 @@ +class AddPrivateToAccounts < ActiveRecord::Migration[5.2] + def change + safety_assured do + add_column :accounts, :private, :boolean, default: false, null: false + end + end +end diff --git a/db/migrate/20200728173757_add_require_auth_to_accounts.rb b/db/migrate/20200728173757_add_require_auth_to_accounts.rb new file mode 100644 index 000000000..00a3c1642 --- /dev/null +++ b/db/migrate/20200728173757_add_require_auth_to_accounts.rb @@ -0,0 +1,7 @@ +class AddRequireAuthToAccounts < ActiveRecord::Migration[5.2] + def change + safety_assured do + add_column :accounts, :require_auth, :boolean, default: false, null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 3bf2d2e9e..4fcdd7e2f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_07_28_135753) do +ActiveRecord::Schema.define(version: 2020_07_28_173757) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -195,6 +195,8 @@ ActiveRecord::Schema.define(version: 2020_07_28_135753) do t.boolean "require_dereference", default: false, null: false t.boolean "show_replies", default: true, null: false t.boolean "show_unlisted", default: true, null: false + t.boolean "private", default: false, null: false + t.boolean "require_auth", default: false, null: false t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id" -- cgit