From 81e1cc5fece9a431c28ca648c2dd4b1b5f643f13 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 26 Aug 2022 03:10:01 +0900 Subject: Replace to `workbox-webpack-plugin` from `offline-plugin` (#18409) --- app/javascript/mastodon/main.js | 26 ++- app/javascript/mastodon/service_worker/entry.js | 81 +++++--- .../service_worker/web_push_notifications.js | 7 +- app/javascript/mastodon/storage/db.js | 27 --- app/javascript/mastodon/storage/modifier.js | 211 --------------------- 5 files changed, 71 insertions(+), 281 deletions(-) delete mode 100644 app/javascript/mastodon/storage/db.js delete mode 100644 app/javascript/mastodon/storage/modifier.js (limited to 'app') diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js index bda51f692..a66975bfd 100644 --- a/app/javascript/mastodon/main.js +++ b/app/javascript/mastodon/main.js @@ -1,9 +1,9 @@ -import * as registerPushNotifications from './actions/push_notifications'; -import { setupBrowserNotifications } from './actions/notifications'; -import { default as Mastodon, store } from './containers/mastodon'; import React from 'react'; import ReactDOM from 'react-dom'; -import ready from './ready'; +import * as registerPushNotifications from 'mastodon/actions/push_notifications'; +import { setupBrowserNotifications } from 'mastodon/actions/notifications'; +import Mastodon, { store } from 'mastodon/containers/mastodon'; +import ready from 'mastodon/ready'; const perf = require('./performance'); @@ -24,10 +24,20 @@ function main() { ReactDOM.render(, mountNode); store.dispatch(setupBrowserNotifications()); - if (process.env.NODE_ENV === 'production') { - // avoid offline in dev mode because it's harder to debug - require('offline-plugin/runtime').install(); - store.dispatch(registerPushNotifications.register()); + + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + import('workbox-window') + .then(({ Workbox }) => { + const wb = new Workbox('/sw.js'); + + return wb.register(); + }) + .then(() => { + store.dispatch(registerPushNotifications.register()); + }) + .catch(err => { + console.error(err); + }); } perf.stop('main()'); }); diff --git a/app/javascript/mastodon/service_worker/entry.js b/app/javascript/mastodon/service_worker/entry.js index b354f3b33..e4c66cc00 100644 --- a/app/javascript/mastodon/service_worker/entry.js +++ b/app/javascript/mastodon/service_worker/entry.js @@ -1,20 +1,59 @@ -// import { freeStorage, storageFreeable } from '../storage/modifier'; -import './web_push_notifications'; +import { ExpirationPlugin } from 'workbox-expiration'; +import { precacheAndRoute } from 'workbox-precaching'; +import { registerRoute } from 'workbox-routing'; +import { CacheFirst } from 'workbox-strategies'; +import { handleNotificationClick, handlePush } from './web_push_notifications'; -// function openSystemCache() { -// return caches.open('mastodon-system'); -// } +const CACHE_NAME_PREFIX = 'mastodon-'; function openWebCache() { - return caches.open('mastodon-web'); + return caches.open(`${CACHE_NAME_PREFIX}web`); } function fetchRoot() { return fetch('/', { credentials: 'include', redirect: 'manual' }); } -// const firefox = navigator.userAgent.match(/Firefox\/(\d+)/); -// const invalidOnlyIfCached = firefox && firefox[1] < 60; +precacheAndRoute(self.__WB_MANIFEST); + +registerRoute( + /locale_.*\.js$/, + new CacheFirst({ + cacheName: `${CACHE_NAME_PREFIX}locales`, + plugins: [ + new ExpirationPlugin({ + maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month + maxEntries: 5, + }), + ], + }), +); + +registerRoute( + ({ request }) => request.destination === 'font', + new CacheFirst({ + cacheName: `${CACHE_NAME_PREFIX}fonts`, + plugins: [ + new ExpirationPlugin({ + maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month + maxEntries: 5, + }), + ], + }), +); + +registerRoute( + ({ request }) => ['audio', 'image', 'track', 'video'].includes(request.destination), + new CacheFirst({ + cacheName: `m${CACHE_NAME_PREFIX}media`, + plugins: [ + new ExpirationPlugin({ + maxAgeSeconds: 7 * 24 * 60 * 60, // 1 week + maxEntries: 256, + }), + ], + }), +); // Cause a new version of a registered Service Worker to replace an existing one // that is already installed, and replace the currently active worker on open pages. @@ -52,26 +91,8 @@ self.addEventListener('fetch', function(event) { return response; })); - } /* else if (storageFreeable && (ATTACHMENT_HOST ? url.host === ATTACHMENT_HOST : url.pathname.startsWith('/system/'))) { - event.respondWith(openSystemCache().then(cache => { - return cache.match(event.request.url).then(cached => { - if (cached === undefined) { - const asyncResponse = invalidOnlyIfCached && event.request.cache === 'only-if-cached' ? - fetch(event.request, { cache: 'no-cache' }) : fetch(event.request); - - return asyncResponse.then(response => { - if (response.ok) { - cache - .put(event.request.url, response.clone()) - .catch(()=>{}).then(freeStorage()).catch(); - } - - return response; - }); - } - - return cached; - }); - })); - } */ + } }); + +self.addEventListener('push', handlePush); +self.addEventListener('notificationclick', handleNotificationClick); diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js index 48a2be7e7..9b75e9b9d 100644 --- a/app/javascript/mastodon/service_worker/web_push_notifications.js +++ b/app/javascript/mastodon/service_worker/web_push_notifications.js @@ -75,7 +75,7 @@ const formatMessage = (messageId, locale, values = {}) => const htmlToPlainText = html => unescape(html.replace(//g, '\n').replace(/<\/p>

/g, '\n\n').replace(/<[^>]*>/g, '')); -const handlePush = (event) => { +export const handlePush = (event) => { const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json(); // Placeholder until more information can be loaded @@ -189,7 +189,7 @@ const openUrl = url => return self.clients.openWindow(url); }); -const handleNotificationClick = (event) => { +export const handleNotificationClick = (event) => { const reactToNotificationClick = new Promise((resolve, reject) => { if (event.action) { if (event.action === 'expand') { @@ -211,6 +211,3 @@ const handleNotificationClick = (event) => { event.waitUntil(reactToNotificationClick); }; - -self.addEventListener('push', handlePush); -self.addEventListener('notificationclick', handleNotificationClick); diff --git a/app/javascript/mastodon/storage/db.js b/app/javascript/mastodon/storage/db.js deleted file mode 100644 index 377a792a7..000000000 --- a/app/javascript/mastodon/storage/db.js +++ /dev/null @@ -1,27 +0,0 @@ -export default () => new Promise((resolve, reject) => { - // ServiceWorker is required to synchronize the login state. - // Microsoft Edge 17 does not support getAll according to: - // Catalog of standard and vendor APIs across browsers - Microsoft Edge Development - // https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?q=specName%3Aindexeddb - if (!('caches' in self && 'getAll' in IDBObjectStore.prototype)) { - reject(); - return; - } - - const request = indexedDB.open('mastodon'); - - request.onerror = reject; - request.onsuccess = ({ target }) => resolve(target.result); - - request.onupgradeneeded = ({ target }) => { - const accounts = target.result.createObjectStore('accounts', { autoIncrement: true }); - const statuses = target.result.createObjectStore('statuses', { autoIncrement: true }); - - accounts.createIndex('id', 'id', { unique: true }); - accounts.createIndex('moved', 'moved'); - - statuses.createIndex('id', 'id', { unique: true }); - statuses.createIndex('account', 'account'); - statuses.createIndex('reblog', 'reblog'); - }; -}); diff --git a/app/javascript/mastodon/storage/modifier.js b/app/javascript/mastodon/storage/modifier.js deleted file mode 100644 index 9fadabef4..000000000 --- a/app/javascript/mastodon/storage/modifier.js +++ /dev/null @@ -1,211 +0,0 @@ -import openDB from './db'; - -const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static']; -const storageMargin = 8388608; -const storeLimit = 1024; - -// navigator.storage is not present on: -// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299 -// estimate method is not present on Chrome 57.0.2987.98 on Linux. -export const storageFreeable = 'storage' in navigator && 'estimate' in navigator.storage; - -function openCache() { - // ServiceWorker and Cache API is not available on iOS 11 - // https://webkit.org/status/#specification-service-workers - return self.caches ? caches.open('mastodon-system') : Promise.reject(); -} - -function printErrorIfAvailable(error) { - if (error) { - console.warn(error); - } -} - -function put(name, objects, onupdate, oncreate) { - return openDB().then(db => (new Promise((resolve, reject) => { - const putTransaction = db.transaction(name, 'readwrite'); - const putStore = putTransaction.objectStore(name); - const putIndex = putStore.index('id'); - - objects.forEach(object => { - putIndex.getKey(object.id).onsuccess = retrieval => { - function addObject() { - putStore.add(object); - } - - function deleteObject() { - putStore.delete(retrieval.target.result).onsuccess = addObject; - } - - if (retrieval.target.result) { - if (onupdate) { - onupdate(object, retrieval.target.result, putStore, deleteObject); - } else { - deleteObject(); - } - } else { - if (oncreate) { - oncreate(object, addObject); - } else { - addObject(); - } - } - }; - }); - - putTransaction.oncomplete = () => { - const readTransaction = db.transaction(name, 'readonly'); - const readStore = readTransaction.objectStore(name); - const count = readStore.count(); - - count.onsuccess = () => { - const excess = count.result - storeLimit; - - if (excess > 0) { - const retrieval = readStore.getAll(null, excess); - - retrieval.onsuccess = () => resolve(retrieval.result); - retrieval.onerror = reject; - } else { - resolve([]); - } - }; - - count.onerror = reject; - }; - - putTransaction.onerror = reject; - })).then(resolved => { - db.close(); - return resolved; - }, error => { - db.close(); - throw error; - })); -} - -function evictAccountsByRecords(records) { - return openDB().then(db => { - const transaction = db.transaction(['accounts', 'statuses'], 'readwrite'); - const accounts = transaction.objectStore('accounts'); - const accountsIdIndex = accounts.index('id'); - const accountsMovedIndex = accounts.index('moved'); - const statuses = transaction.objectStore('statuses'); - const statusesIndex = statuses.index('account'); - - function evict(toEvict) { - toEvict.forEach(record => { - openCache() - .then(cache => accountAssetKeys.forEach(key => cache.delete(records[key]))) - .catch(printErrorIfAvailable); - - accountsMovedIndex.getAll(record.id).onsuccess = ({ target }) => evict(target.result); - - statusesIndex.getAll(record.id).onsuccess = - ({ target }) => evictStatusesByRecords(target.result); - - accountsIdIndex.getKey(record.id).onsuccess = - ({ target }) => target.result && accounts.delete(target.result); - }); - } - - evict(records); - - db.close(); - }).catch(printErrorIfAvailable); -} - -export function evictStatus(id) { - evictStatuses([id]); -} - -export function evictStatuses(ids) { - return openDB().then(db => { - const transaction = db.transaction('statuses', 'readwrite'); - const store = transaction.objectStore('statuses'); - const idIndex = store.index('id'); - const reblogIndex = store.index('reblog'); - - ids.forEach(id => { - reblogIndex.getAllKeys(id).onsuccess = - ({ target }) => target.result.forEach(reblogKey => store.delete(reblogKey)); - - idIndex.getKey(id).onsuccess = - ({ target }) => target.result && store.delete(target.result); - }); - - db.close(); - }).catch(printErrorIfAvailable); -} - -function evictStatusesByRecords(records) { - return evictStatuses(records.map(({ id }) => id)); -} - -export function putAccounts(records, avatarStatic) { - const avatarKey = avatarStatic ? 'avatar_static' : 'avatar'; - const newURLs = []; - - put('accounts', records, (newRecord, oldKey, store, oncomplete) => { - store.get(oldKey).onsuccess = ({ target }) => { - accountAssetKeys.forEach(key => { - const newURL = newRecord[key]; - const oldURL = target.result[key]; - - if (newURL !== oldURL) { - openCache() - .then(cache => cache.delete(oldURL)) - .catch(printErrorIfAvailable); - } - }); - - const newURL = newRecord[avatarKey]; - const oldURL = target.result[avatarKey]; - - if (newURL !== oldURL) { - newURLs.push(newURL); - } - - oncomplete(); - }; - }, (newRecord, oncomplete) => { - newURLs.push(newRecord[avatarKey]); - oncomplete(); - }).then(records => Promise.all([ - evictAccountsByRecords(records), - openCache().then(cache => cache.addAll(newURLs)), - ])).then(freeStorage, error => { - freeStorage(); - throw error; - }).catch(printErrorIfAvailable); -} - -export function putStatuses(records) { - put('statuses', records) - .then(evictStatusesByRecords) - .catch(printErrorIfAvailable); -} - -export function freeStorage() { - return storageFreeable && navigator.storage.estimate().then(({ quota, usage }) => { - if (usage + storageMargin < quota) { - return null; - } - - return openDB().then(db => new Promise((resolve, reject) => { - const retrieval = db.transaction('accounts', 'readonly').objectStore('accounts').getAll(null, 1); - - retrieval.onsuccess = () => { - if (retrieval.result.length > 0) { - resolve(evictAccountsByRecords(retrieval.result).then(freeStorage)); - } else { - resolve(caches.delete('mastodon-system')); - } - }; - - retrieval.onerror = reject; - - db.close(); - })); - }); -} -- cgit From 0396acf39ea902688374fac65fa7ef5dc4c05512 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 25 Aug 2022 20:39:40 +0200 Subject: Add audit log entries for user roles (#19040) * Refactor audit log schema * Add audit log entries for user roles --- app/controllers/admin/roles_controller.rb | 3 ++ app/controllers/admin/users/roles_controller.rb | 1 + app/controllers/concerns/accountable_concern.rb | 8 ++- app/helpers/admin/action_logs_helper.rb | 61 +++++----------------- app/models/account.rb | 4 ++ app/models/account_warning.rb | 4 ++ app/models/admin/action_log.rb | 36 +++++++------ app/models/admin/action_log_filter.rb | 4 ++ app/models/announcement.rb | 4 ++ app/models/appeal.rb | 8 +++ app/models/custom_emoji.rb | 6 ++- app/models/domain_allow.rb | 4 ++ app/models/domain_block.rb | 4 ++ app/models/email_domain_block.rb | 4 ++ app/models/form/account_batch.rb | 2 +- app/models/instance.rb | 2 + app/models/ip_block.rb | 4 ++ app/models/report.rb | 6 +++ app/models/status.rb | 8 +++ app/models/unavailable_domain.rb | 4 ++ app/models/user.rb | 8 +++ app/models/user_role.rb | 4 ++ config/locales/en.yml | 11 +++- ...33_add_human_identifier_to_admin_action_logs.rb | 7 +++ ...move_recorded_changes_from_admin_action_logs.rb | 9 ++++ db/schema.rb | 6 ++- spec/helpers/admin/action_log_helper_spec.rb | 28 ---------- 27 files changed, 151 insertions(+), 99 deletions(-) create mode 100644 db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb create mode 100644 db/post_migrate/20220824164532_remove_recorded_changes_from_admin_action_logs.rb (limited to 'app') diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index 3e502ccc4..d76aa745b 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -23,6 +23,7 @@ module Admin @role.current_account = current_account if @role.save + log_action :create, @role redirect_to admin_roles_path else render :new @@ -39,6 +40,7 @@ module Admin @role.current_account = current_account if @role.update(resource_params) + log_action :update, @role redirect_to admin_roles_path else render :edit @@ -48,6 +50,7 @@ module Admin def destroy authorize @role, :destroy? @role.destroy! + log_action :destroy, @role redirect_to admin_roles_path end diff --git a/app/controllers/admin/users/roles_controller.rb b/app/controllers/admin/users/roles_controller.rb index 0db50cee9..f5dfc643d 100644 --- a/app/controllers/admin/users/roles_controller.rb +++ b/app/controllers/admin/users/roles_controller.rb @@ -14,6 +14,7 @@ module Admin @user.current_account = current_account if @user.update(resource_params) + log_action :change_role, @user redirect_to admin_account_path(@user.account_id), notice: I18n.t('admin.accounts.change_role.changed_msg') else render :show diff --git a/app/controllers/concerns/accountable_concern.rb b/app/controllers/concerns/accountable_concern.rb index 87d62478d..c1349915f 100644 --- a/app/controllers/concerns/accountable_concern.rb +++ b/app/controllers/concerns/accountable_concern.rb @@ -3,7 +3,11 @@ module AccountableConcern extend ActiveSupport::Concern - def log_action(action, target, options = {}) - Admin::ActionLog.create(account: current_account, action: action, target: target, recorded_changes: options.stringify_keys) + def log_action(action, target) + Admin::ActionLog.create( + account: current_account, + action: action, + target: target + ) end end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 47eeeaac3..3e9fe17f4 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -2,64 +2,31 @@ module Admin::ActionLogsHelper def log_target(log) - if log.target - linkable_log_target(log.target) - else - log_target_from_history(log.target_type, log.recorded_changes) - end - end - - private - - def linkable_log_target(record) - case record.class.name + case log.target_type when 'Account' - link_to record.acct, admin_account_path(record.id) + link_to log.human_identifier, admin_account_path(log.target_id) when 'User' - link_to record.account.acct, admin_account_path(record.account_id) + link_to log.human_identifier, admin_account_path(log.route_param) + when 'UserRole' + link_to log.human_identifier, admin_roles_path(log.target_id) when 'CustomEmoji' - record.shortcode + log.human_identifier when 'Report' - link_to "##{record.id}", admin_report_path(record) + link_to "##{log.human_identifier}", admin_report_path(log.target_id) when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' - link_to record.domain, "https://#{record.domain}" + link_to log.human_identifier, "https://#{log.human_identifier}" when 'Status' - link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record) + link_to log.human_identifier, log.permalink when 'AccountWarning' - link_to record.target_account.acct, admin_account_path(record.target_account_id) + link_to log.human_identifier, admin_account_path(log.target_id) when 'Announcement' - link_to truncate(record.text), edit_admin_announcement_path(record.id) + link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id) when 'IpBlock' - "#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})" + log.human_identifier when 'Instance' - record.domain + log.human_identifier when 'Appeal' - link_to record.account.acct, disputes_strike_path(record.strike) - end - end - - def log_target_from_history(type, attributes) - case type - when 'User' - attributes['username'] - when 'CustomEmoji' - attributes['shortcode'] - when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' - link_to attributes['domain'], "https://#{attributes['domain']}" - when 'Status' - tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count')) - - if tmp_status.account - link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id) - else - I18n.t('admin.action_logs.deleted_status') - end - when 'Announcement' - truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text']) - when 'IpBlock' - "#{attributes['ip']}/#{attributes['ip'].prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{attributes['severity']}")})" - when 'Instance' - attributes['domain'] + link_to log.human_identifier, disputes_strike_path(log.route_param) end end end diff --git a/app/models/account.rb b/app/models/account.rb index 628692d22..d25afeb89 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -362,6 +362,10 @@ class Account < ApplicationRecord username end + def to_log_human_identifier + acct + end + def excluded_from_timeline_account_ids Rails.cache.fetch("exclude_account_ids_for:#{id}") { block_relationships.pluck(:target_account_id) + blocked_by_relationships.pluck(:account_id) + mute_relationships.pluck(:target_account_id) } end diff --git a/app/models/account_warning.rb b/app/models/account_warning.rb index 6067b54b7..961a078b9 100644 --- a/app/models/account_warning.rb +++ b/app/models/account_warning.rb @@ -43,4 +43,8 @@ class AccountWarning < ApplicationRecord def overruled? overruled_at.present? end + + def to_log_human_identifier + target_account.acct + end end diff --git a/app/models/admin/action_log.rb b/app/models/admin/action_log.rb index 401bfd9ac..4fa8008f5 100644 --- a/app/models/admin/action_log.rb +++ b/app/models/admin/action_log.rb @@ -9,38 +9,42 @@ # action :string default(""), not null # target_type :string # target_id :bigint(8) -# recorded_changes :text default(""), not null # created_at :datetime not null # updated_at :datetime not null +# human_identifier :string +# route_param :string +# permalink :string # class Admin::ActionLog < ApplicationRecord - serialize :recorded_changes + self.ignored_columns = %w( + recorded_changes + ) belongs_to :account belongs_to :target, polymorphic: true, optional: true default_scope -> { order('id desc') } + before_validation :set_human_identifier + before_validation :set_route_param + before_validation :set_permalink + def action super.to_sym end - before_validation :set_changes - private - def set_changes - case action - when :destroy, :create - self.recorded_changes = target.attributes - when :update, :promote, :demote - self.recorded_changes = target.previous_changes - when :change_email - self.recorded_changes = ActiveSupport::HashWithIndifferentAccess.new( - email: [target.email, nil], - unconfirmed_email: [nil, target.unconfirmed_email] - ) - end + def set_human_identifier + self.human_identifier = target.to_log_human_identifier if target.respond_to?(:to_log_human_identifier) + end + + def set_route_param + self.route_param = target.to_log_route_param if target.respond_to?(:to_log_route_param) + end + + def set_permalink + self.permalink = target.to_log_permalink if target.respond_to?(:to_log_permalink) end end diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb index 0f2f712a2..6382cd782 100644 --- a/app/models/admin/action_log_filter.rb +++ b/app/models/admin/action_log_filter.rb @@ -12,6 +12,7 @@ class Admin::ActionLogFilter reject_appeal: { target_type: 'Appeal', action: 'reject' }.freeze, assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze, change_email_user: { target_type: 'User', action: 'change_email' }.freeze, + change_role_user: { target_type: 'User', action: 'change_role' }.freeze, confirm_user: { target_type: 'User', action: 'confirm' }.freeze, approve_user: { target_type: 'User', action: 'approve' }.freeze, reject_user: { target_type: 'User', action: 'reject' }.freeze, @@ -22,6 +23,7 @@ class Admin::ActionLogFilter create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze, create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze, create_unavailable_domain: { target_type: 'UnavailableDomain', action: 'create' }.freeze, + create_user_role: { target_type: 'UserRole', action: 'create' }.freeze, demote_user: { target_type: 'User', action: 'demote' }.freeze, destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze, destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze, @@ -31,6 +33,7 @@ class Admin::ActionLogFilter destroy_instance: { target_type: 'Instance', action: 'destroy' }.freeze, destroy_unavailable_domain: { target_type: 'UnavailableDomain', action: 'destroy' }.freeze, destroy_status: { target_type: 'Status', action: 'destroy' }.freeze, + destroy_user_role: { target_type: 'UserRole', action: 'destroy' }.freeze, disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze, disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze, disable_user: { target_type: 'User', action: 'disable' }.freeze, @@ -52,6 +55,7 @@ class Admin::ActionLogFilter update_announcement: { target_type: 'Announcement', action: 'update' }.freeze, update_custom_emoji: { target_type: 'CustomEmoji', action: 'update' }.freeze, update_status: { target_type: 'Status', action: 'update' }.freeze, + update_user_role: { target_type: 'UserRole', action: 'update' }.freeze, unblock_email_account: { target_type: 'Account', action: 'unblock_email' }.freeze, }.freeze diff --git a/app/models/announcement.rb b/app/models/announcement.rb index f8183aabc..bedced9de 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -34,6 +34,10 @@ class Announcement < ApplicationRecord before_validation :set_all_day before_validation :set_published, on: :create + def to_log_human_identifier + text + end + def publish! update!(published: true, published_at: Time.now.utc, scheduled_at: nil) end diff --git a/app/models/appeal.rb b/app/models/appeal.rb index 1f32cfa8b..6fbf60b39 100644 --- a/app/models/appeal.rb +++ b/app/models/appeal.rb @@ -52,6 +52,14 @@ class Appeal < ApplicationRecord update!(rejected_at: Time.now.utc, rejected_by_account: current_account) end + def to_log_human_identifier + account.acct + end + + def to_log_route_param + account_warning_id + end + private def validate_time_frame diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 289e3b66f..077ce559a 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -46,7 +46,7 @@ class CustomEmoji < ApplicationRecord scope :local, -> { where(domain: nil) } scope :remote, -> { where.not(domain: nil) } scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) } - scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) } + scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches("%.#{domain}"))) } scope :listed, -> { local.where(disabled: false).where(visible_in_picker: true) } remotable_attachment :image, LIMIT @@ -67,6 +67,10 @@ class CustomEmoji < ApplicationRecord copy.tap(&:save!) end + def to_log_human_identifier + shortcode + end + class << self def from_text(text, domain = nil) return [] if text.blank? diff --git a/app/models/domain_allow.rb b/app/models/domain_allow.rb index 6aa9267fe..65f494fed 100644 --- a/app/models/domain_allow.rb +++ b/app/models/domain_allow.rb @@ -19,6 +19,10 @@ class DomainAllow < ApplicationRecord scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } + def to_log_human_identifier + domain + end + class << self def allowed?(domain) !rule_for(domain).nil? diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index a15206b5e..b08687787 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -31,6 +31,10 @@ class DomainBlock < ApplicationRecord scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) } scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain')) } + def to_log_human_identifier + domain + end + def policies if suspend? [:suspend] diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb index f9d74332b..661f6727d 100644 --- a/app/models/email_domain_block.rb +++ b/app/models/email_domain_block.rb @@ -26,6 +26,10 @@ class EmailDomainBlock < ApplicationRecord # Used for adding multiple blocks at once attr_accessor :other_domains + def to_log_human_identifier + domain + end + def history @history ||= Trends::History.new('email_domain_blocks', id) end diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index dcf155840..98f2cad3e 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -101,7 +101,7 @@ class Form::AccountBatch def reject_account(account) authorize(account.user, :reject?) - log_action(:reject, account.user, username: account.username) + log_action(:reject, account.user) account.suspend!(origin: :local) AccountDeletionWorker.perform_async(account.id, { 'reserve_username' => false }) end diff --git a/app/models/instance.rb b/app/models/instance.rb index 36110ee40..edbf02a6d 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -48,6 +48,8 @@ class Instance < ApplicationRecord domain end + alias to_log_human_identifier to_param + delegate :exhausted_deliveries_days, to: :delivery_failure_tracker def availability_over_days(num_days, end_date = Time.now.utc.to_date) diff --git a/app/models/ip_block.rb b/app/models/ip_block.rb index e1ab59806..f40c8a0b1 100644 --- a/app/models/ip_block.rb +++ b/app/models/ip_block.rb @@ -27,6 +27,10 @@ class IpBlock < ApplicationRecord after_commit :reset_cache + def to_log_human_identifier + "#{record.ip}/#{record.ip.prefix}" + end + class << self def blocked?(remote_ip) blocked_ips_map = Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(IpBlock.where(severity: :no_access).pluck(:ip)) } diff --git a/app/models/report.rb b/app/models/report.rb index 2efb6d4a7..42c869dd4 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -115,6 +115,10 @@ class Report < ApplicationRecord Report.where.not(id: id).where(target_account_id: target_account_id).unresolved.exists? end + def to_log_human_identifier + id + end + def history subquery = [ Admin::ActionLog.where( @@ -136,6 +140,8 @@ class Report < ApplicationRecord Admin::ActionLog.from(Arel::Nodes::As.new(subquery, Admin::ActionLog.arel_table)) end + private + def set_uri self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? && account.local? end diff --git a/app/models/status.rb b/app/models/status.rb index 4828d6340..7eff990aa 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -166,6 +166,14 @@ class Status < ApplicationRecord ].compact.join("\n\n") end + def to_log_human_identifier + account.acct + end + + def to_log_permalink + ActivityPub::TagManager.instance.uri_for(self) + end + def reply? !in_reply_to_id.nil? || attributes['reply'] end diff --git a/app/models/unavailable_domain.rb b/app/models/unavailable_domain.rb index 5e8870bde..dfc0ef14e 100644 --- a/app/models/unavailable_domain.rb +++ b/app/models/unavailable_domain.rb @@ -16,6 +16,10 @@ class UnavailableDomain < ApplicationRecord after_commit :reset_cache! + def to_log_human_identifier + domain + end + private def reset_cache! diff --git a/app/models/user.rb b/app/models/user.rb index 9833300cd..18b9d5928 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -181,6 +181,14 @@ class User < ApplicationRecord update!(disabled: false) end + def to_log_human_identifier + account.acct + end + + def to_log_route_param + account_id + end + def confirm new_user = !confirmed? self.approved = true if open_registrations? && !sign_up_from_ip_requires_approval? diff --git a/app/models/user_role.rb b/app/models/user_role.rb index 57a56c0b0..74dfdc220 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -155,6 +155,10 @@ class UserRole < ApplicationRecord end end + def to_log_human_identifier + name + end + private def in_permissions?(privilege) diff --git a/config/locales/en.yml b/config/locales/en.yml index e495ef841..5c309ab11 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -235,6 +235,7 @@ en: approve_user: Approve User assigned_to_self_report: Assign Report change_email_user: Change E-mail for User + change_role_user: Change Role of User confirm_user: Confirm User create_account_warning: Create Warning create_announcement: Create Announcement @@ -244,6 +245,7 @@ en: create_email_domain_block: Create E-mail Domain Block create_ip_block: Create IP rule create_unavailable_domain: Create Unavailable Domain + create_user_role: Create Role demote_user: Demote User destroy_announcement: Delete Announcement destroy_custom_emoji: Delete Custom Emoji @@ -254,6 +256,7 @@ en: destroy_ip_block: Delete IP rule destroy_status: Delete Post destroy_unavailable_domain: Delete Unavailable Domain + destroy_user_role: Destroy Role disable_2fa_user: Disable 2FA disable_custom_emoji: Disable Custom Emoji disable_sign_in_token_auth_user: Disable E-mail Token Authentication for User @@ -281,11 +284,13 @@ en: update_custom_emoji: Update Custom Emoji update_domain_block: Update Domain Block update_status: Update Post + update_user_role: Update Role actions: approve_appeal_html: "%{name} approved moderation decision appeal from %{target}" approve_user_html: "%{name} approved sign-up from %{target}" assigned_to_self_report_html: "%{name} assigned report %{target} to themselves" change_email_user_html: "%{name} changed the e-mail address of user %{target}" + change_role_user_html: "%{name} changed role of %{target}" confirm_user_html: "%{name} confirmed e-mail address of user %{target}" create_account_warning_html: "%{name} sent a warning to %{target}" create_announcement_html: "%{name} created new announcement %{target}" @@ -295,9 +300,10 @@ en: create_email_domain_block_html: "%{name} blocked e-mail domain %{target}" create_ip_block_html: "%{name} created rule for IP %{target}" create_unavailable_domain_html: "%{name} stopped delivery to domain %{target}" + create_user_role_html: "%{name} created %{target} role" demote_user_html: "%{name} demoted user %{target}" destroy_announcement_html: "%{name} deleted announcement %{target}" - destroy_custom_emoji_html: "%{name} destroyed emoji %{target}" + destroy_custom_emoji_html: "%{name} deleted emoji %{target}" destroy_domain_allow_html: "%{name} disallowed federation with domain %{target}" destroy_domain_block_html: "%{name} unblocked domain %{target}" destroy_email_domain_block_html: "%{name} unblocked e-mail domain %{target}" @@ -305,6 +311,7 @@ en: destroy_ip_block_html: "%{name} deleted rule for IP %{target}" destroy_status_html: "%{name} removed post by %{target}" destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}" + destroy_user_role_html: "%{name} deleted %{target} role" disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}" disable_custom_emoji_html: "%{name} disabled emoji %{target}" disable_sign_in_token_auth_user_html: "%{name} disabled e-mail token authentication for %{target}" @@ -332,7 +339,7 @@ en: update_custom_emoji_html: "%{name} updated emoji %{target}" update_domain_block_html: "%{name} updated domain block for %{target}" update_status_html: "%{name} updated post by %{target}" - deleted_status: "(deleted post)" + update_user_role_html: "%{name} changed %{target} role" empty: No logs found. filter_by_action: Filter by action filter_by_user: Filter by user diff --git a/db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb b/db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb new file mode 100644 index 000000000..2cb8cddf1 --- /dev/null +++ b/db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb @@ -0,0 +1,7 @@ +class AddHumanIdentifierToAdminActionLogs < ActiveRecord::Migration[6.1] + def change + add_column :admin_action_logs, :human_identifier, :string + add_column :admin_action_logs, :route_param, :string + add_column :admin_action_logs, :permalink, :string + end +end diff --git a/db/post_migrate/20220824164532_remove_recorded_changes_from_admin_action_logs.rb b/db/post_migrate/20220824164532_remove_recorded_changes_from_admin_action_logs.rb new file mode 100644 index 000000000..c42d5df8f --- /dev/null +++ b/db/post_migrate/20220824164532_remove_recorded_changes_from_admin_action_logs.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class RemoveRecordedChangesFromAdminActionLogs < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + safety_assured { remove_column :admin_action_logs, :recorded_changes, :text } + end +end diff --git a/db/schema.rb b/db/schema.rb index 15ab2e85e..83fd9549c 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: 2022_08_08_101323) do +ActiveRecord::Schema.define(version: 2022_08_24_164532) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -205,9 +205,11 @@ ActiveRecord::Schema.define(version: 2022_08_08_101323) do t.string "action", default: "", null: false t.string "target_type" t.bigint "target_id" - t.text "recorded_changes", default: "", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "human_identifier" + t.string "route_param" + t.string "permalink" t.index ["account_id"], name: "index_admin_action_logs_on_account_id" t.index ["target_type", "target_id"], name: "index_admin_action_logs_on_target_type_and_target_id" end diff --git a/spec/helpers/admin/action_log_helper_spec.rb b/spec/helpers/admin/action_log_helper_spec.rb index 60f5ecdcc..9d7ed4ab7 100644 --- a/spec/helpers/admin/action_log_helper_spec.rb +++ b/spec/helpers/admin/action_log_helper_spec.rb @@ -3,32 +3,4 @@ require 'rails_helper' RSpec.describe Admin::ActionLogsHelper, type: :helper do - klass = Class.new do - include ActionView::Helpers - include Admin::ActionLogsHelper - end - - let(:hoge) { klass.new } - - describe '#log_target' do - after do - hoge.log_target(log) - end - - context 'log.target' do - let(:log) { double(target: true) } - - it 'calls linkable_log_target' do - expect(hoge).to receive(:linkable_log_target).with(log.target) - end - end - - context '!log.target' do - let(:log) { double(target: false, target_type: :type, recorded_changes: :change) } - - it 'calls log_target_from_history' do - expect(hoge).to receive(:log_target_from_history).with(log.target_type, log.recorded_changes) - end - end - end end -- cgit From d696f729f1dc27c9d92b896c651e1ea73bf93d75 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 25 Aug 2022 23:27:10 +0200 Subject: Add meta tag for official iOS app (#16599) --- app/views/layouts/application.html.haml | 1 + 1 file changed, 1 insertion(+) (limited to 'app') diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index bf164223c..3e50de3fb 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -23,6 +23,7 @@ %link{ rel: 'manifest', href: manifest_path(format: :json) }/ %meta{ name: 'theme-color', content: '#6364FF' }/ %meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/ + %meta{ name: 'apple-itunes-app', content: 'app-id=1571998974' }/ %title= content_for?(:page_title) ? safe_join([yield(:page_title).chomp.html_safe, title], ' - ') : title -- cgit From 5b0e8cc92b9ca0ab0dc24366d95f67a88c470173 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 25 Aug 2022 23:33:34 +0200 Subject: Add ability to select all accounts matching search for batch actions (#19053) --- app/controllers/admin/accounts_controller.rb | 6 +++- app/javascript/packs/admin.js | 53 ++++++++++++++++++++++++++++ app/javascript/styles/mastodon/tables.scss | 49 +++++++++++++++++++++++++ app/models/custom_filter_status.rb | 2 +- app/models/form/account_batch.rb | 13 +++++-- app/views/admin/accounts/index.html.haml | 9 +++++ config/locales/en.yml | 10 ++++++ 7 files changed, 138 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 46c9aba91..40bf685c5 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -16,7 +16,11 @@ module Admin def batch authorize :account, :index? - @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button)) + @form = Form::AccountBatch.new(form_account_batch_params) + @form.current_account = current_account + @form.action = action_from_button + @form.select_all_matching = params[:select_all_matching] + @form.query = filtered_accounts @form.save rescue ActionController::ParameterMissing flash[:alert] = I18n.t('admin.accounts.no_account_selected') diff --git a/app/javascript/packs/admin.js b/app/javascript/packs/admin.js index a3ed1ffed..b733d6b18 100644 --- a/app/javascript/packs/admin.js +++ b/app/javascript/packs/admin.js @@ -4,18 +4,71 @@ import ready from '../mastodon/ready'; const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]'; +const showSelectAll = () => { + const selectAllMatchingElement = document.querySelector('.batch-table__select-all'); + selectAllMatchingElement.classList.add('active'); +}; + +const hideSelectAll = () => { + const selectAllMatchingElement = document.querySelector('.batch-table__select-all'); + const hiddenField = document.querySelector('#select_all_matching'); + const selectedMsg = document.querySelector('.batch-table__select-all .selected'); + const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected'); + + selectAllMatchingElement.classList.remove('active'); + selectedMsg.classList.remove('active'); + notSelectedMsg.classList.add('active'); + hiddenField.value = '0'; +}; + delegate(document, '#batch_checkbox_all', 'change', ({ target }) => { + const selectAllMatchingElement = document.querySelector('.batch-table__select-all'); + [].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => { content.checked = target.checked; }); + + if (selectAllMatchingElement) { + if (target.checked) { + showSelectAll(); + } else { + hideSelectAll(); + } + } +}); + +delegate(document, '.batch-table__select-all button', 'click', () => { + const hiddenField = document.querySelector('#select_all_matching'); + const active = hiddenField.value === '1'; + const selectedMsg = document.querySelector('.batch-table__select-all .selected'); + const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected'); + + if (active) { + hiddenField.value = '0'; + selectedMsg.classList.remove('active'); + notSelectedMsg.classList.add('active'); + } else { + hiddenField.value = '1'; + notSelectedMsg.classList.remove('active'); + selectedMsg.classList.add('active'); + } }); delegate(document, batchCheckboxClassName, 'change', () => { const checkAllElement = document.querySelector('#batch_checkbox_all'); + const selectAllMatchingElement = document.querySelector('.batch-table__select-all'); if (checkAllElement) { checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked); checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked); + + if (selectAllMatchingElement) { + if (checkAllElement.checked) { + showSelectAll(); + } else { + hideSelectAll(); + } + } } }); diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 431b8a73a..39211910f 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -190,6 +190,55 @@ a.table-action-link { } } + &__select-all { + background: $ui-base-color; + height: 47px; + align-items: center; + justify-content: center; + border: 1px solid darken($ui-base-color, 8%); + border-top: 0; + color: $secondary-text-color; + display: none; + + &.active { + display: flex; + } + + .selected, + .not-selected { + display: none; + + &.active { + display: block; + } + } + + strong { + font-weight: 700; + } + + span { + padding: 8px; + display: inline-block; + } + + button { + background: transparent; + border: 0; + font: inherit; + color: $highlight-text-color; + border-radius: 4px; + font-weight: 700; + padding: 8px; + + &:hover, + &:focus, + &:active { + background: lighten($ui-base-color, 8%); + } + } + } + &__form { padding: 16px; border: 1px solid darken($ui-base-color, 8%); diff --git a/app/models/custom_filter_status.rb b/app/models/custom_filter_status.rb index b6bea1394..e748d6963 100644 --- a/app/models/custom_filter_status.rb +++ b/app/models/custom_filter_status.rb @@ -5,7 +5,7 @@ # # id :bigint(8) not null, primary key # custom_filter_id :bigint(8) not null -# status_id :bigint(8) default(""), not null +# status_id :bigint(8) not null # created_at :datetime not null # updated_at :datetime not null # diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index 98f2cad3e..5cfcf7205 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -6,7 +6,8 @@ class Form::AccountBatch include AccountableConcern include Payloadable - attr_accessor :account_ids, :action, :current_account + attr_accessor :account_ids, :action, :current_account, + :select_all_matching, :query def save case action @@ -60,7 +61,11 @@ class Form::AccountBatch end def accounts - Account.where(id: account_ids) + if select_all_matching? + query + else + Account.where(id: account_ids) + end end def approve! @@ -118,4 +123,8 @@ class Form::AccountBatch log_action(:approve, account.user) account.user.approve! end + + def select_all_matching? + select_all_matching == '1' + end end diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index cb378f0ed..670a09a2d 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -37,6 +37,7 @@ = form_for(@form, url: batch_admin_accounts_path) do |f| = hidden_field_tag :page, params[:page] || 1 + = hidden_field_tag :select_all_matching, '0' - AccountFilter::KEYS.each do |key| = hidden_field_tag key, params[key] if params[key].present? @@ -52,6 +53,14 @@ = f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } = f.button safe_join([fa_icon('lock'), t('admin.accounts.perform_full_suspension')]), name: :suspend, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + - if true || @accounts.total_count > @accounts.size + .batch-table__select-all + .not-selected.active + %span= t('generic.all_items_on_page_selected_html', count: @accounts.size) + %button{ type: 'button' }= t('generic.select_all_matching_items', count: @accounts.total_count) + .selected + %span= t('generic.all_matching_items_selected_html', count: @accounts.total_count) + %button{ type: 'button' }= t('generic.deselect') .batch-table__body - if @accounts.empty? = nothing_here 'nothing-here--under-tabs' diff --git a/config/locales/en.yml b/config/locales/en.yml index 5c309ab11..6aa87e4a0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1227,12 +1227,22 @@ en: trending_now: Trending now generic: all: All + all_items_on_page_selected_html: + one: "%{count} item on this page is selected." + other: All %{count} items on this page are selected. + all_matching_items_selected_html: + one: "%{count} item matching your search is selected." + other: All %{count} items matching your search are selected. changes_saved_msg: Changes successfully saved! copy: Copy delete: Delete + deselect: Deselect all none: None order_by: Order by save_changes: Save changes + select_all_matching_items: + one: Select %{count} item matching your search. + other: Select all %{count} items matching your search. today: today validation_errors: one: Something isn't quite right yet! Please review the error below -- cgit From 0b3e4fd5de392969b624719b2eb3f86277b6ac1f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 25 Aug 2022 23:38:22 +0200 Subject: Remove digest e-mails (#17985) * Remove digest e-mails * Remove digest-related code --- app/controllers/settings/preferences_controller.rb | 2 +- app/mailers/notification_mailer.rb | 18 --------- app/models/user.rb | 4 -- app/views/notification_mailer/digest.html.haml | 44 ---------------------- app/views/notification_mailer/digest.text.erb | 15 -------- .../preferences/notifications/show.html.haml | 4 -- app/workers/digest_mailer_worker.rb | 21 ----------- app/workers/scheduler/email_scheduler.rb | 25 ------------ config/locales/en.yml | 11 ------ config/sidekiq.yml | 4 -- spec/mailers/notification_mailer_spec.rb | 31 --------------- spec/workers/digest_mailer_worker_spec.rb | 36 ------------------ 12 files changed, 1 insertion(+), 214 deletions(-) delete mode 100644 app/views/notification_mailer/digest.html.haml delete mode 100644 app/views/notification_mailer/digest.text.erb delete mode 100644 app/workers/digest_mailer_worker.rb delete mode 100644 app/workers/scheduler/email_scheduler.rb delete mode 100644 spec/workers/digest_mailer_worker_spec.rb (limited to 'app') diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index bfe651bc6..f5d5c1244 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -55,7 +55,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_trends, :setting_crop_images, :setting_always_send_emails, - notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag appeal), + notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag appeal), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) end diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 9e683b6a1..ab73826ab 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -66,24 +66,6 @@ class NotificationMailer < ApplicationMailer end end - def digest(recipient, **opts) - return unless recipient.user.functional? - - @me = recipient - @since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max - @notifications_count = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since).count - - return if @notifications_count.zero? - - @notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since).limit(40) - @follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count - - locale_for_account(@me) do - mail to: @me.user.email, - subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications_count) - end - end - private def thread_by_conversation(conversation) diff --git a/app/models/user.rb b/app/models/user.rb index 18b9d5928..342f5e6cc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -289,10 +289,6 @@ class User < ApplicationRecord settings.default_privacy || (account.locked? ? 'private' : 'public') end - def allows_digest_emails? - settings.notification_emails['digest'] - end - def allows_report_emails? settings.notification_emails['report'] end diff --git a/app/views/notification_mailer/digest.html.haml b/app/views/notification_mailer/digest.html.haml deleted file mode 100644 index a94ace228..000000000 --- a/app/views/notification_mailer/digest.html.haml +++ /dev/null @@ -1,44 +0,0 @@ -%table.email-table{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.email-body - .email-container - %table.content-section{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.content-cell.darker.hero-with-button - .email-row - .col-6 - %table.column{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.column-cell.text-center.padded - %h1= t 'notification_mailer.digest.title' - %p.lead= t('notification_mailer.digest.body', since: l((@me.user_current_sign_in_at || @since).to_date, format: :short), instance: site_hostname) - %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.button-primary - = link_to web_url do - %span= t 'notification_mailer.digest.action' - -- @notifications.each_with_index do |n, i| - = render 'status', status: n.target_status, i: i - -- unless @follows_since.zero? - %table.email-table{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.email-body - .email-container - %table.content-section{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.content-cell.content-start.border-top - .email-row - .col-6 - %table.column{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.column-cell.text-center - %p= t('notification_mailer.digest.new_followers_summary', count: @follows_since) diff --git a/app/views/notification_mailer/digest.text.erb b/app/views/notification_mailer/digest.text.erb deleted file mode 100644 index 0f84a4ef0..000000000 --- a/app/views/notification_mailer/digest.text.erb +++ /dev/null @@ -1,15 +0,0 @@ -<%= raw t('application_mailer.salutation', name: display_name(@me)) %> - -<%= raw t('notification_mailer.digest.body', since: l(@me.user_current_sign_in_at || @since), instance: root_url) %> -<% @notifications.each do |notification| %> - -* <%= raw t('notification_mailer.digest.mention', name: notification.from_account.pretty_acct) %> - - <%= raw extract_status_plain_text(notification.target_status) %> - - <%= raw t('application_mailer.view')%> <%= web_url("statuses/#{notification.target_status.id}") %> -<% end %> -<% if @follows_since > 0 %> - -<%= raw t('notification_mailer.digest.new_followers_summary', count: @follows_since) %> -<% end %> diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index bc7afb993..f00dbadd4 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -26,10 +26,6 @@ .fields-group = f.input :setting_always_send_emails, as: :boolean, wrapper: :with_label - .fields-group - = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| - = ff.input :digest, as: :boolean, wrapper: :with_label - %h4= t 'notifications.other_settings' .fields-group diff --git a/app/workers/digest_mailer_worker.rb b/app/workers/digest_mailer_worker.rb deleted file mode 100644 index 21f1c357a..000000000 --- a/app/workers/digest_mailer_worker.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -class DigestMailerWorker - include Sidekiq::Worker - - sidekiq_options queue: 'mailers' - - attr_reader :user - - def perform(user_id) - @user = User.find(user_id) - deliver_digest if @user.allows_digest_emails? - end - - private - - def deliver_digest - NotificationMailer.digest(user.account).deliver_now! - user.touch(:last_emailed_at) - end -end diff --git a/app/workers/scheduler/email_scheduler.rb b/app/workers/scheduler/email_scheduler.rb deleted file mode 100644 index c052f2fce..000000000 --- a/app/workers/scheduler/email_scheduler.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -class Scheduler::EmailScheduler - include Sidekiq::Worker - - sidekiq_options retry: 0 - - FREQUENCY = 7.days.freeze - SIGN_IN_OFFSET = 1.day.freeze - - def perform - eligible_users.reorder(nil).find_each do |user| - next unless user.allows_digest_emails? - DigestMailerWorker.perform_async(user.id) - end - end - - private - - def eligible_users - User.emailable - .where('current_sign_in_at < ?', (FREQUENCY + SIGN_IN_OFFSET).ago) - .where('last_emailed_at IS NULL OR last_emailed_at < ?', FREQUENCY.ago) - end -end diff --git a/config/locales/en.yml b/config/locales/en.yml index 6aa87e4a0..72ebfafba 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1351,17 +1351,6 @@ en: subject: "%{name} submitted a report" sign_up: subject: "%{name} signed up" - digest: - action: View all notifications - body: Here is a brief summary of the messages you missed since your last visit on %{since} - mention: "%{name} mentioned you in:" - new_followers_summary: - one: Also, you have acquired one new follower while being away! Yay! - other: Also, you have acquired %{count} new followers while being away! Amazing! - subject: - one: "1 new notification since your last visit 🐘" - other: "%{count} new notifications since your last visit 🐘" - title: In your absence... favourite: body: 'Your post was favourited by %{name}:' subject: "%{name} favourited your post" diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 2a3871468..9ec6eb5ec 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -49,10 +49,6 @@ cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' class: Scheduler::IpCleanupScheduler queue: scheduler - email_scheduler: - cron: '0 10 * * 2' - class: Scheduler::EmailScheduler - queue: scheduler backup_cleanup_scheduler: cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' class: Scheduler::BackupCleanupScheduler diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb index 2ca4e26fa..29bdc349b 100644 --- a/spec/mailers/notification_mailer_spec.rb +++ b/spec/mailers/notification_mailer_spec.rb @@ -101,35 +101,4 @@ RSpec.describe NotificationMailer, type: :mailer do expect(mail.body.encoded).to match("bob has requested to follow you") end end - - describe 'digest' do - before do - mention = Fabricate(:mention, account: receiver.account, status: foreign_status) - Fabricate(:notification, account: receiver.account, activity: mention) - sender.follow!(receiver.account) - end - - context do - let!(:mail) { NotificationMailer.digest(receiver.account, since: 5.days.ago) } - - include_examples 'localized subject', 'notification_mailer.digest.subject', count: 1, name: 'bob' - - it 'renders the headers' do - expect(mail.subject).to match('notification since your last') - expect(mail.to).to eq([receiver.email]) - end - - it 'renders the body' do - expect(mail.body.encoded).to match('brief summary') - expect(mail.body.encoded).to include 'The body of the foreign status' - expect(mail.body.encoded).to include sender.username - end - end - - it 'includes activities since the receiver last signed in' do - receiver.update!(last_emailed_at: nil, current_sign_in_at: '2000-03-01T00:00:00Z') - mail = NotificationMailer.digest(receiver.account) - expect(mail.body.encoded).to include 'Mar 01, 2000, 00:00' - end - end end diff --git a/spec/workers/digest_mailer_worker_spec.rb b/spec/workers/digest_mailer_worker_spec.rb deleted file mode 100644 index db3b1390d..000000000 --- a/spec/workers/digest_mailer_worker_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe DigestMailerWorker do - describe 'perform' do - let(:user) { Fabricate(:user, last_emailed_at: 3.days.ago) } - - context 'for a user who receives digests' do - it 'sends the email' do - service = double(deliver_now!: nil) - allow(NotificationMailer).to receive(:digest).and_return(service) - update_user_digest_setting(true) - described_class.perform_async(user.id) - - expect(NotificationMailer).to have_received(:digest) - expect(user.reload.last_emailed_at).to be_within(1).of(Time.now.utc) - end - end - - context 'for a user who does not receive digests' do - it 'does not send the email' do - allow(NotificationMailer).to receive(:digest) - update_user_digest_setting(false) - described_class.perform_async(user.id) - - expect(NotificationMailer).not_to have_received(:digest) - expect(user.last_emailed_at).to be_within(1).of(3.days.ago) - end - end - - def update_user_digest_setting(value) - user.settings['notification_emails'] = user.settings['notification_emails'].merge('digest' => value) - end - end -end -- cgit From af396fa35f589e1f759c7a3a0dad7932f1862309 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 25 Aug 2022 23:38:36 +0200 Subject: Remove preference to aggregate reblogs in home/list feeds (#18112) --- app/lib/feed_manager.rb | 18 +++++++++--------- app/views/settings/preferences/other/show.html.haml | 3 --- 2 files changed, 9 insertions(+), 12 deletions(-) (limited to 'app') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 145352fe8..ccff2667b 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -58,7 +58,7 @@ class FeedManager # @param [Boolean] update # @return [Boolean] def push_to_home(account, status, update: false) - return false unless add_to_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?) + return false unless add_to_feed(:home, account.id, status, aggregate_reblogs: true) trim(:home, account.id) PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}", { 'update' => update }) if push_update_required?("timeline:#{account.id}") @@ -71,7 +71,7 @@ class FeedManager # @param [Boolean] update # @return [Boolean] def unpush_from_home(account, status, update: false) - return false unless remove_from_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?) + return false unless remove_from_feed(:home, account.id, status, aggregate_reblogs: true) redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update true @@ -83,7 +83,7 @@ class FeedManager # @param [Boolean] update # @return [Boolean] def push_to_list(list, status, update: false) - return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?) + return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, aggregate_reblogs: true) trim(:list, list.id) PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}", { 'update' => update }) if push_update_required?("timeline:list:#{list.id}") @@ -96,7 +96,7 @@ class FeedManager # @param [Boolean] update # @return [Boolean] def unpush_from_list(list, status, update: false) - return false unless remove_from_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?) + return false unless remove_from_feed(:list, list.id, status, aggregate_reblogs: true) redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update true @@ -108,7 +108,7 @@ class FeedManager # @return [void] def merge_into_home(from_account, into_account) timeline_key = key(:home, into_account.id) - aggregate = into_account.user&.aggregates_reblogs? + aggregate = true query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4 @@ -134,7 +134,7 @@ class FeedManager # @return [void] def merge_into_list(from_account, list) timeline_key = key(:list, list.id) - aggregate = list.account.user&.aggregates_reblogs? + aggregate = true query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4 @@ -163,7 +163,7 @@ class FeedManager timeline_status_ids = redis.zrange(timeline_key, 0, -1) from_account.statuses.select('id, reblog_of_id').where(id: timeline_status_ids).reorder(nil).find_each do |status| - remove_from_feed(:home, into_account.id, status, aggregate_reblogs: into_account.user&.aggregates_reblogs?) + remove_from_feed(:home, into_account.id, status, aggregate_reblogs: true) end end @@ -176,7 +176,7 @@ class FeedManager timeline_status_ids = redis.zrange(timeline_key, 0, -1) from_account.statuses.select('id, reblog_of_id').where(id: timeline_status_ids).reorder(nil).find_each do |status| - remove_from_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?) + remove_from_feed(:list, list.id, status, aggregate_reblogs: true) end end @@ -235,7 +235,7 @@ class FeedManager # @return [void] def populate_home(account) limit = FeedManager::MAX_ITEMS / 2 - aggregate = account.user&.aggregates_reblogs? + aggregate = true timeline_key = key(:home, account.id) account.statuses.limit(limit).each do |status| diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 44f4af2eb..379678703 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -10,9 +10,6 @@ .fields-group = f.input :setting_noindex, as: :boolean, wrapper: :with_label - .fields-group - = f.input :setting_aggregate_reblogs, as: :boolean, wrapper: :with_label, recommended: true - %h4= t 'preferences.posting_defaults' .fields-row -- cgit From 1d84cbeaab7cecdfef10f53afe1e708b8a363603 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 26 Aug 2022 02:11:23 +0200 Subject: Fix "select all matching items" being offered when all items are on the page (#19054) --- app/views/admin/accounts/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 670a09a2d..f33f788ed 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -53,7 +53,7 @@ = f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } = f.button safe_join([fa_icon('lock'), t('admin.accounts.perform_full_suspension')]), name: :suspend, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } - - if true || @accounts.total_count > @accounts.size + - if @accounts.total_count > @accounts.size .batch-table__select-all .not-selected.active %span= t('generic.all_items_on_page_selected_html', count: @accounts.size) -- cgit From 1fac99e842c99e8f6aef9429ee1f19c7e63f7b07 Mon Sep 17 00:00:00 2001 From: Shlee Date: Fri, 26 Aug 2022 09:42:46 +0930 Subject: Add rel="noopener" to links to remote profiles (#19014) * Update timeline_hint.js * Update timeline_hint.js --- app/javascript/mastodon/components/timeline_hint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/mastodon/components/timeline_hint.js b/app/javascript/mastodon/components/timeline_hint.js index fb55a62cc..ac9a79dcc 100644 --- a/app/javascript/mastodon/components/timeline_hint.js +++ b/app/javascript/mastodon/components/timeline_hint.js @@ -6,7 +6,7 @@ const TimelineHint = ({ resource, url }) => (


- +
); -- cgit From c7147bab908aca4cefd17d527e7cbfe3b6e5d3a6 Mon Sep 17 00:00:00 2001 From: Claire Date: Sat, 27 Aug 2022 00:45:54 +0200 Subject: Fix incorrect and slow cache invalidation in ClearDomainMediaService (#19062) Fixes #19060 --- app/services/clear_domain_media_service.rb | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) (limited to 'app') diff --git a/app/services/clear_domain_media_service.rb b/app/services/clear_domain_media_service.rb index 704cfb71a..9e70ebe51 100644 --- a/app/services/clear_domain_media_service.rb +++ b/app/services/clear_domain_media_service.rb @@ -10,24 +10,18 @@ class ClearDomainMediaService < BaseService private - def invalidate_association_caches! + def invalidate_association_caches!(status_ids) # Normally, associated models of a status are immutable (except for accounts) # so they are aggressively cached. After updating the media attachments to no # longer point to a local file, we need to clear the cache to make those # changes appear in the API and UI - @affected_status_ids.each { |id| Rails.cache.delete_matched("statuses/#{id}-*") } + Rails.cache.delete_multi(status_ids.map { |id| "statuses/#{id}" }) end def clear_media! - @affected_status_ids = [] - - begin - clear_account_images! - clear_account_attachments! - clear_emojos! - ensure - invalidate_association_caches! - end + clear_account_images! + clear_account_attachments! + clear_emojos! end def clear_account_images! @@ -39,12 +33,18 @@ class ClearDomainMediaService < BaseService end def clear_account_attachments! - media_from_blocked_domain.reorder(nil).find_each do |attachment| - @affected_status_ids << attachment.status_id if attachment.status_id.present? + media_from_blocked_domain.reorder(nil).find_in_batches do |attachments| + affected_status_ids = [] + + attachments.each do |attachment| + affected_status_ids << attachment.status_id if attachment.status_id.present? + + attachment.file.destroy if attachment.file&.exists? + attachment.type = :unknown + attachment.save + end - attachment.file.destroy if attachment.file&.exists? - attachment.type = :unknown - attachment.save + invalidate_association_caches!(affected_status_ids) unless affected_status_ids.empty? end end -- cgit From b399d79545e5e5430cb9d6a2c936efc244b69a75 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 27 Aug 2022 20:56:47 +0200 Subject: Add admin API for managing IP blocks (#19065) --- .../api/v1/admin/ip_blocks_controller.rb | 99 ++++++++++++++++++++++ app/models/ip_block.rb | 3 +- app/policies/ip_block_policy.rb | 4 + app/serializers/rest/admin/ip_block_serializer.rb | 14 +++ config/routes.rb | 1 + 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/admin/ip_blocks_controller.rb create mode 100644 app/serializers/rest/admin/ip_block_serializer.rb (limited to 'app') diff --git a/app/controllers/api/v1/admin/ip_blocks_controller.rb b/app/controllers/api/v1/admin/ip_blocks_controller.rb new file mode 100644 index 000000000..f13d63267 --- /dev/null +++ b/app/controllers/api/v1/admin/ip_blocks_controller.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +class Api::V1::Admin::IpBlocksController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:ip_blocks' }, only: [:index, :show] + before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:ip_blocks' }, except: [:index, :show] + before_action :set_ip_blocks, only: :index + before_action :set_ip_block, only: [:show, :update, :destroy] + + after_action :verify_authorized + after_action :insert_pagination_headers, only: :index + + PAGINATION_PARAMS = %i( + limit + ).freeze + + def create + authorize :ip_block, :create? + + @ip_block = IpBlock.create!(resource_params) + log_action :create, @ip_block + + render json: @ip_block, serializer: REST::Admin::IpBlockSerializer + end + + def index + authorize :ip_block, :index? + render json: @ip_blocks, each_serializer: REST::Admin::IpBlockSerializer + end + + def show + authorize @ip_block, :show? + render json: @ip_block, serializer: REST::Admin::IpBlockSerializer + end + + def update + authorize @ip_block, :update? + + @ip_block.update(resource_params) + log_action :update, @ip_block + + render json: @ip_block, serializer: REST::Admin::IpBlockSerializer + end + + def destroy + authorize @ip_block, :destroy? + + @ip_block.destroy! + log_action :destroy, @ip_block + + render json: @ip_block, serializer: REST::Admin::IpBlockSerializer + end + + private + + def set_ip_blocks + @ip_blocks = IpBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_ip_block + @ip_block = IpBlock.find(params[:id]) + end + + def resource_params + params.permit(:ip, :severity, :comment, :expires_in) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_ip_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_ip_blocks_url(pagination_params(min_id: pagination_since_id)) unless @ip_blocks.empty? + end + + def pagination_max_id + @ip_blocks.last.id + end + + def pagination_since_id + @ip_blocks.first.id + end + + def records_continue? + @ip_blocks.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/models/ip_block.rb b/app/models/ip_block.rb index f40c8a0b1..8666f4248 100644 --- a/app/models/ip_block.rb +++ b/app/models/ip_block.rb @@ -16,6 +16,7 @@ class IpBlock < ApplicationRecord CACHE_KEY = 'blocked_ips' include Expireable + include Paginable enum severity: { sign_up_requires_approval: 5000, @@ -28,7 +29,7 @@ class IpBlock < ApplicationRecord after_commit :reset_cache def to_log_human_identifier - "#{record.ip}/#{record.ip.prefix}" + "#{ip}/#{ip.prefix}" end class << self diff --git a/app/policies/ip_block_policy.rb b/app/policies/ip_block_policy.rb index 1abc97ad8..2986a4fdb 100644 --- a/app/policies/ip_block_policy.rb +++ b/app/policies/ip_block_policy.rb @@ -9,6 +9,10 @@ class IpBlockPolicy < ApplicationPolicy role.can?(:manage_blocks) end + def update? + role.can?(:manage_blocks) + end + def destroy? role.can?(:manage_blocks) end diff --git a/app/serializers/rest/admin/ip_block_serializer.rb b/app/serializers/rest/admin/ip_block_serializer.rb new file mode 100644 index 000000000..6a38f8b56 --- /dev/null +++ b/app/serializers/rest/admin/ip_block_serializer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class REST::Admin::IpBlockSerializer < ActiveModel::Serializer + attributes :id, :ip, :severity, :comment, + :created_at, :expires_at + + def id + object.id.to_s + end + + def ip + "#{object.ip}/#{object.ip.prefix}" + end +end diff --git a/config/routes.rb b/config/routes.rb index dff0add3a..1168c9aee 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -591,6 +591,7 @@ Rails.application.routes.draw do resources :domain_allows, only: [:index, :show, :create, :destroy] resources :domain_blocks, only: [:index, :show, :update, :create, :destroy] + resources :ip_blocks, only: [:index, :show, :update, :create, :destroy] namespace :trends do resources :tags, only: [:index] -- cgit From c556c3a0d1e54a6b07bbdd8f76cbb43672a91fd1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 28 Aug 2022 03:31:54 +0200 Subject: Add admin API for managing canonical e-mail blocks (#19067) --- .../v1/admin/canonical_email_blocks_controller.rb | 99 ++++++++++++++++++++++ app/helpers/admin/action_logs_helper.rb | 8 +- app/models/admin/action_log_filter.rb | 5 ++ app/models/canonical_email_block.rb | 17 ++-- app/policies/canonical_email_block_policy.rb | 23 +++++ .../rest/admin/canonical_email_block_serializer.rb | 9 ++ config/locales/en.yml | 6 ++ config/routes.rb | 6 ++ ...95229_change_canonical_email_blocks_nullable.rb | 5 ++ db/schema.rb | 4 +- lib/mastodon/canonical_email_blocks_cli.rb | 31 ++----- 11 files changed, 177 insertions(+), 36 deletions(-) create mode 100644 app/controllers/api/v1/admin/canonical_email_blocks_controller.rb create mode 100644 app/policies/canonical_email_block_policy.rb create mode 100644 app/serializers/rest/admin/canonical_email_block_serializer.rb create mode 100644 db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb (limited to 'app') diff --git a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb new file mode 100644 index 000000000..bf8a6a131 --- /dev/null +++ b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:canonical_email_blocks' }, only: [:index, :show, :test] + before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:canonical_email_blocks' }, except: [:index, :show, :test] + + before_action :set_canonical_email_blocks, only: :index + before_action :set_canonical_email_blocks_from_test, only: [:test] + before_action :set_canonical_email_block, only: [:show, :destroy] + + after_action :verify_authorized + after_action :insert_pagination_headers, only: :index + + PAGINATION_PARAMS = %i(limit).freeze + + def index + authorize :canonical_email_block, :index? + render json: @canonical_email_blocks, each_serializer: REST::Admin::CanonicalEmailBlockSerializer + end + + def show + authorize @canonical_email_block, :show? + render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer + end + + def test + authorize :canonical_email_block, :test? + render json: @canonical_email_blocks, each_serializer: REST::Admin::CanonicalEmailBlockSerializer + end + + def create + authorize :canonical_email_block, :create? + + @canonical_email_block = CanonicalEmailBlock.create!(resource_params) + log_action :create, @canonical_email_block + + render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer + end + + def destroy + authorize @canonical_email_block, :destroy? + + @canonical_email_block.destroy! + log_action :destroy, @canonical_email_block + + render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer + end + + private + + def resource_params + params.permit(:canonical_email_hash, :email) + end + + def set_canonical_email_blocks + @canonical_email_blocks = CanonicalEmailBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_canonical_email_blocks_from_test + @canonical_email_blocks = CanonicalEmailBlock.matching_email(params[:email]) + end + + def set_canonical_email_block + @canonical_email_block = CanonicalEmailBlock.find(params[:id]) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_canonical_email_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_canonical_email_blocks_url(pagination_params(min_id: pagination_since_id)) unless @canonical_email_blocks.empty? + end + + def pagination_max_id + @canonical_email_blocks.last.id + end + + def pagination_since_id + @canonical_email_blocks.first.id + end + + def records_continue? + @canonical_email_blocks.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 3e9fe17f4..fd1977ac5 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -9,8 +9,6 @@ module Admin::ActionLogsHelper link_to log.human_identifier, admin_account_path(log.route_param) when 'UserRole' link_to log.human_identifier, admin_roles_path(log.target_id) - when 'CustomEmoji' - log.human_identifier when 'Report' link_to "##{log.human_identifier}", admin_report_path(log.target_id) when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' @@ -21,10 +19,10 @@ module Admin::ActionLogsHelper link_to log.human_identifier, admin_account_path(log.target_id) when 'Announcement' link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id) - when 'IpBlock' - log.human_identifier - when 'Instance' + when 'IpBlock', 'Instance', 'CustomEmoji' log.human_identifier + when 'CanonicalEmailBlock' + content_tag(:samp, log.human_identifier[0...7], title: log.human_identifier) when 'Appeal' link_to log.human_identifier, disputes_strike_path(log.route_param) end diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb index 6382cd782..c7a7e1a4c 100644 --- a/app/models/admin/action_log_filter.rb +++ b/app/models/admin/action_log_filter.rb @@ -22,18 +22,22 @@ class Admin::ActionLogFilter create_domain_allow: { target_type: 'DomainAllow', action: 'create' }.freeze, create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze, create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze, + create_ip_block: { target_type: 'IpBlock', action: 'create' }.freeze, create_unavailable_domain: { target_type: 'UnavailableDomain', action: 'create' }.freeze, create_user_role: { target_type: 'UserRole', action: 'create' }.freeze, + create_canonical_email_block: { target_type: 'CanonicalEmailBlock', action: 'create' }.freeze, demote_user: { target_type: 'User', action: 'demote' }.freeze, destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze, destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze, destroy_domain_allow: { target_type: 'DomainAllow', action: 'destroy' }.freeze, destroy_domain_block: { target_type: 'DomainBlock', action: 'destroy' }.freeze, + destroy_ip_block: { target_type: 'IpBlock', action: 'destroy' }.freeze, destroy_email_domain_block: { target_type: 'EmailDomainBlock', action: 'destroy' }.freeze, destroy_instance: { target_type: 'Instance', action: 'destroy' }.freeze, destroy_unavailable_domain: { target_type: 'UnavailableDomain', action: 'destroy' }.freeze, destroy_status: { target_type: 'Status', action: 'destroy' }.freeze, destroy_user_role: { target_type: 'UserRole', action: 'destroy' }.freeze, + destroy_canonical_email_block: { target_type: 'CanonicalEmailBlock', action: 'destroy' }.freeze, disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze, disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze, disable_user: { target_type: 'User', action: 'disable' }.freeze, @@ -56,6 +60,7 @@ class Admin::ActionLogFilter update_custom_emoji: { target_type: 'CustomEmoji', action: 'update' }.freeze, update_status: { target_type: 'Status', action: 'update' }.freeze, update_user_role: { target_type: 'UserRole', action: 'update' }.freeze, + update_ip_block: { target_type: 'IpBlock', action: 'update' }.freeze, unblock_email_account: { target_type: 'Account', action: 'unblock_email' }.freeze, }.freeze diff --git a/app/models/canonical_email_block.rb b/app/models/canonical_email_block.rb index 94781386c..1eb69ac67 100644 --- a/app/models/canonical_email_block.rb +++ b/app/models/canonical_email_block.rb @@ -5,27 +5,30 @@ # # id :bigint(8) not null, primary key # canonical_email_hash :string default(""), not null -# reference_account_id :bigint(8) not null +# reference_account_id :bigint(8) # created_at :datetime not null # updated_at :datetime not null # class CanonicalEmailBlock < ApplicationRecord include EmailHelper + include Paginable - belongs_to :reference_account, class_name: 'Account' + belongs_to :reference_account, class_name: 'Account', optional: true validates :canonical_email_hash, presence: true, uniqueness: true + scope :matching_email, ->(email) { where(canonical_email_hash: email_to_canonical_email_hash(email)) } + + def to_log_human_identifier + canonical_email_hash + end + def email=(email) self.canonical_email_hash = email_to_canonical_email_hash(email) end def self.block?(email) - where(canonical_email_hash: email_to_canonical_email_hash(email)).exists? - end - - def self.find_blocks(email) - where(canonical_email_hash: email_to_canonical_email_hash(email)) + matching_email(email).exists? end end diff --git a/app/policies/canonical_email_block_policy.rb b/app/policies/canonical_email_block_policy.rb new file mode 100644 index 000000000..8d76075c9 --- /dev/null +++ b/app/policies/canonical_email_block_policy.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class CanonicalEmailBlockPolicy < ApplicationPolicy + def index? + role.can?(:manage_blocks) + end + + def show? + role.can?(:manage_blocks) + end + + def test? + role.can?(:manage_blocks) + end + + def create? + role.can?(:manage_blocks) + end + + def destroy? + role.can?(:manage_blocks) + end +end diff --git a/app/serializers/rest/admin/canonical_email_block_serializer.rb b/app/serializers/rest/admin/canonical_email_block_serializer.rb new file mode 100644 index 000000000..fe385940a --- /dev/null +++ b/app/serializers/rest/admin/canonical_email_block_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class REST::Admin::CanonicalEmailBlockSerializer < ActiveModel::Serializer + attributes :id, :canonical_email_hash + + def id + object.id.to_s + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 72ebfafba..0b721c163 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -239,6 +239,7 @@ en: confirm_user: Confirm User create_account_warning: Create Warning create_announcement: Create Announcement + create_canonical_email_block: Create E-mail Block create_custom_emoji: Create Custom Emoji create_domain_allow: Create Domain Allow create_domain_block: Create Domain Block @@ -248,6 +249,7 @@ en: create_user_role: Create Role demote_user: Demote User destroy_announcement: Delete Announcement + destroy_canonical_email_block: Delete E-mail Block destroy_custom_emoji: Delete Custom Emoji destroy_domain_allow: Delete Domain Allow destroy_domain_block: Delete Domain Block @@ -283,6 +285,7 @@ en: update_announcement: Update Announcement update_custom_emoji: Update Custom Emoji update_domain_block: Update Domain Block + update_ip_block: Update IP rule update_status: Update Post update_user_role: Update Role actions: @@ -294,6 +297,7 @@ en: confirm_user_html: "%{name} confirmed e-mail address of user %{target}" create_account_warning_html: "%{name} sent a warning to %{target}" create_announcement_html: "%{name} created new announcement %{target}" + create_canonical_email_block_html: "%{name} blocked e-mail with the hash %{target}" create_custom_emoji_html: "%{name} uploaded new emoji %{target}" create_domain_allow_html: "%{name} allowed federation with domain %{target}" create_domain_block_html: "%{name} blocked domain %{target}" @@ -303,6 +307,7 @@ en: create_user_role_html: "%{name} created %{target} role" demote_user_html: "%{name} demoted user %{target}" destroy_announcement_html: "%{name} deleted announcement %{target}" + destroy_canonical_email_block_html: "%{name} unblocked e-mail with the hash %{target}" destroy_custom_emoji_html: "%{name} deleted emoji %{target}" destroy_domain_allow_html: "%{name} disallowed federation with domain %{target}" destroy_domain_block_html: "%{name} unblocked domain %{target}" @@ -338,6 +343,7 @@ en: update_announcement_html: "%{name} updated announcement %{target}" update_custom_emoji_html: "%{name} updated emoji %{target}" update_domain_block_html: "%{name} updated domain block for %{target}" + update_ip_block_html: "%{name} changed rule for IP %{target}" update_status_html: "%{name} updated post by %{target}" update_user_role_html: "%{name} changed %{target} role" empty: No logs found. diff --git a/config/routes.rb b/config/routes.rb index 1168c9aee..8694a6436 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -602,6 +602,12 @@ Rails.application.routes.draw do post :measures, to: 'measures#create' post :dimensions, to: 'dimensions#create' post :retention, to: 'retention#create' + + resources :canonical_email_blocks, only: [:index, :create, :show, :destroy] do + collection do + post :test + end + end end end diff --git a/db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb b/db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb new file mode 100644 index 000000000..5b3ec4727 --- /dev/null +++ b/db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb @@ -0,0 +1,5 @@ +class ChangeCanonicalEmailBlocksNullable < ActiveRecord::Migration[6.1] + def change + safety_assured { change_column :canonical_email_blocks, :reference_account_id, :bigint, null: true, default: nil } + end +end diff --git a/db/schema.rb b/db/schema.rb index 83fd9549c..db22f538a 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: 2022_08_24_164532) do +ActiveRecord::Schema.define(version: 2022_08_27_195229) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -296,7 +296,7 @@ ActiveRecord::Schema.define(version: 2022_08_24_164532) do create_table "canonical_email_blocks", force: :cascade do |t| t.string "canonical_email_hash", default: "", null: false - t.bigint "reference_account_id", null: false + t.bigint "reference_account_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["canonical_email_hash"], name: "index_canonical_email_blocks_on_canonical_email_hash", unique: true diff --git a/lib/mastodon/canonical_email_blocks_cli.rb b/lib/mastodon/canonical_email_blocks_cli.rb index 64b72e603..ec228d466 100644 --- a/lib/mastodon/canonical_email_blocks_cli.rb +++ b/lib/mastodon/canonical_email_blocks_cli.rb @@ -18,17 +18,15 @@ module Mastodon When suspending a local user, a hash of a "canonical" version of their e-mail address is stored to prevent them from signing up again. - This command can be used to find whether a known email address is blocked, - and if so, which account it was attached to. + This command can be used to find whether a known email address is blocked. LONG_DESC def find(email) - accts = CanonicalEmailBlock.find_blocks(email).map(&:reference_account).map(&:acct).to_a + accts = CanonicalEmailBlock.matching_email(email) + if accts.empty? - say("#{email} is not blocked", :yellow) + say("#{email} is not blocked", :green) else - accts.each do |acct| - say(acct, :white) - end + say("#{email} is blocked", :red) end end @@ -40,24 +38,13 @@ module Mastodon This command allows removing a canonical email block. LONG_DESC def remove(email) - blocks = CanonicalEmailBlock.find_blocks(email) + blocks = CanonicalEmailBlock.matching_email(email) + if blocks.empty? - say("#{email} is not blocked", :yellow) + say("#{email} is not blocked", :green) else blocks.destroy_all - say("Removed canonical email block for #{email}", :green) - end - end - - private - - def color(processed, failed) - if !processed.zero? && failed.zero? - :green - elsif failed.zero? - :yellow - else - :red + say("Unblocked #{email}", :green) end end end -- cgit From 2a7766dcc958ad18df761de50f9da5164f1a2e8f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 28 Aug 2022 03:37:55 +0200 Subject: Add admin API for managing e-mail domain blocks (#19066) --- .../api/v1/admin/email_domain_blocks_controller.rb | 90 ++++++++++++++++++++++ app/models/email_domain_block.rb | 1 + .../rest/admin/email_domain_block_serializer.rb | 9 +++ config/routes.rb | 1 + 4 files changed, 101 insertions(+) create mode 100644 app/controllers/api/v1/admin/email_domain_blocks_controller.rb create mode 100644 app/serializers/rest/admin/email_domain_block_serializer.rb (limited to 'app') diff --git a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb new file mode 100644 index 000000000..ac16f70b0 --- /dev/null +++ b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:email_domain_blocks' }, only: [:index, :show] + before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:email_domain_blocks' }, except: [:index, :show] + before_action :set_email_domain_blocks, only: :index + before_action :set_email_domain_block, only: [:show, :destroy] + + after_action :verify_authorized + after_action :insert_pagination_headers, only: :index + + PAGINATION_PARAMS = %i( + limit + ).freeze + + def create + authorize :email_domain_block, :create? + + @email_domain_block = EmailDomainBlock.create!(resource_params) + log_action :create, @email_domain_block + + render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer + end + + def index + authorize :email_domain_block, :index? + render json: @email_domain_blocks, each_serializer: REST::Admin::EmailDomainBlockSerializer + end + + def show + authorize @email_domain_block, :show? + render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer + end + + def destroy + authorize @email_domain_block, :destroy? + + @email_domain_block.destroy! + log_action :destroy, @email_domain_block + + render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer + end + + private + + def set_email_domain_blocks + @email_domain_blocks = EmailDomainBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_email_domain_block + @email_domain_block = EmailDomainBlock.find(params[:id]) + end + + def resource_params + params.permit(:domain) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_email_domain_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_email_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @email_domain_blocks.empty? + end + + def pagination_max_id + @email_domain_blocks.last.id + end + + def pagination_since_id + @email_domain_blocks.first.id + end + + def records_continue? + @email_domain_blocks.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb index 661f6727d..10a0e5102 100644 --- a/app/models/email_domain_block.rb +++ b/app/models/email_domain_block.rb @@ -17,6 +17,7 @@ class EmailDomainBlock < ApplicationRecord ) include DomainNormalizable + include Paginable belongs_to :parent, class_name: 'EmailDomainBlock', optional: true has_many :children, class_name: 'EmailDomainBlock', foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy diff --git a/app/serializers/rest/admin/email_domain_block_serializer.rb b/app/serializers/rest/admin/email_domain_block_serializer.rb new file mode 100644 index 000000000..a026ff680 --- /dev/null +++ b/app/serializers/rest/admin/email_domain_block_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class REST::Admin::EmailDomainBlockSerializer < ActiveModel::Serializer + attributes :id, :domain, :created_at, :history + + def id + object.id.to_s + end +end diff --git a/config/routes.rb b/config/routes.rb index 8694a6436..13a4a1618 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -591,6 +591,7 @@ Rails.application.routes.draw do resources :domain_allows, only: [:index, :show, :create, :destroy] resources :domain_blocks, only: [:index, :show, :update, :create, :destroy] + resources :email_domain_blocks, only: [:index, :show, :create, :destroy] resources :ip_blocks, only: [:index, :show, :update, :create, :destroy] namespace :trends do -- cgit