Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- app/controllers/application_controller.rb
- app/controllers/auth/confirmations_controller.rb
- app/controllers/auth/sessions_controller.rb
- app/controllers/settings/deletes_controller.rb
- app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb
This commit is contained in:
Thibaut Girka
2019-07-23 10:17:06 +02:00
76 changed files with 662 additions and 590 deletions

View File

@@ -8,7 +8,7 @@ class AboutController < ApplicationController
before_action :set_instance_presenter
before_action :set_expires_in
skip_before_action :check_user_permissions, only: [:more, :terms]
skip_before_action :require_functional!, only: [:more, :terms]
def show; end

View File

@@ -42,7 +42,7 @@ class AccountsController < ApplicationController
format.json do
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
end
end
end

View File

@@ -11,7 +11,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def show
expires_in 3.minutes, public: public_fetch_mode?
render json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true
end
private

View File

@@ -7,7 +7,7 @@ class Api::BaseController < ApplicationController
include RateLimitHeaders
skip_before_action :store_current_location
skip_before_action :check_user_permissions
skip_before_action :require_functional!
before_action :set_cache_headers

View File

@@ -6,8 +6,7 @@ class Api::V1::CustomEmojisController < Api::BaseController
skip_before_action :set_cache_headers
def index
render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do
ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false).includes(:category), each_serializer: REST::CustomEmojiSerializer)
end
expires_in 3.minutes, public: true
render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.local.where(disabled: false).includes(:category) }
end
end

View File

@@ -7,7 +7,8 @@ class Api::V1::Instances::ActivityController < Api::BaseController
respond_to :json
def show
render_cached_json('api:v1:instances:activity:show', expires_in: 1.day) { activity }
expires_in 1.day, public: true
render_with_cache json: :activity, expires_in: 1.day
end
private

View File

@@ -7,7 +7,8 @@ class Api::V1::Instances::PeersController < Api::BaseController
respond_to :json
def index
render_cached_json('api:v1:instances:peers:index', expires_in: 1.day) { Account.remote.domains }
expires_in 1.day, public: true
render_with_cache(expires_in: 1.day) { Account.remote.domains }
end
private

View File

@@ -5,8 +5,7 @@ class Api::V1::InstancesController < Api::BaseController
skip_before_action :set_cache_headers
def show
render_cached_json('api:v1:instances', expires_in: 5.minutes) do
ActiveModelSerializers::SerializableResource.new({}, serializer: REST::InstanceSerializer)
end
expires_in 3.minutes, public: true
render_with_cache json: {}, serializer: REST::InstanceSerializer
end
end

View File

@@ -10,6 +10,7 @@ class ApplicationController < ActionController::Base
include Localized
include UserTrackingConcern
include SessionTrackingConcern
include CacheConcern
helper_method :current_account
helper_method :current_session
@@ -25,7 +26,7 @@ class ApplicationController < ActionController::Base
rescue_from Mastodon::NotPermittedError, with: :forbidden
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :check_user_permissions, if: :user_signed_in?
before_action :require_functional!, if: :user_signed_in?
def raise_not_found
raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}"
@@ -57,8 +58,8 @@ class ApplicationController < ActionController::Base
forbidden unless current_user&.staff?
end
def check_user_permissions
forbidden if current_user.disabled? || current_user.account.suspended?
def require_functional!
redirect_to edit_user_registration_path unless current_user.functional?
end
def after_sign_out_path_for(_resource_or_scope)
@@ -190,52 +191,14 @@ class ApplicationController < ActionController::Base
current_user.setting_skin
end
def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes)
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
unless uncached_ids.empty?
uncached = klass.where(id: uncached_ids).with_includes.each_with_object({}) { |item, h| h[item.id] = item }
uncached.each_value do |item|
Rails.cache.write(item, item)
end
end
raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact
end
def respond_with_error(code)
respond_to do |format|
format.any { head code }
format.html do
set_locale
use_pack 'error'
render "errors/#{code}", layout: 'error', status: code
end
end
end
def render_cached_json(cache_key, **options)
options[:expires_in] ||= 3.minutes
cache_public = options.key?(:public) ? options.delete(:public) : true
content_type = options.delete(:content_type) || 'application/json'
data = Rails.cache.fetch(cache_key, { raw: true }.merge(options)) do
yield.to_json
end
expires_in options[:expires_in], public: cache_public
render json: data, content_type: content_type
end
def set_cache_headers
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
end
end

View File

@@ -4,20 +4,9 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
layout 'auth'
before_action :set_body_classes
before_action :set_user, only: [:finish_signup]
before_action :set_pack
def finish_signup
return unless request.patch? && params[:user]
if @user.update(user_params)
@user.skip_reconfirmation!
bypass_sign_in(@user)
redirect_to root_path, notice: I18n.t('devise.confirmations.send_instructions')
else
@show_errors = true
end
end
skip_before_action :require_functional!
private
@@ -25,18 +14,10 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
use_pack 'auth'
end
def set_user
@user = current_user
end
def set_body_classes
@body_classes = 'lighter'
end
def user_params
params.require(:user).permit(:email)
end
def after_confirmation_path_for(_resource_name, user)
if user.created_by_application && truthy_param?(:redirect_to_app)
user.created_by_application.redirect_uri

View File

@@ -27,7 +27,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
if resource.email_verified?
root_path
else
finish_signup_path
auth_setup_path(missing_email: '1')
end
end
end

View File

@@ -10,6 +10,9 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :set_sessions, only: [:edit, :update]
before_action :set_instance_presenter, only: [:new, :create, :update]
before_action :set_body_classes, only: [:new, :create, :edit, :update]
before_action :require_not_suspended!, only: [:update]
skip_before_action :require_functional!, only: [:edit, :update]
def new
super(&:build_invite_request)
@@ -44,7 +47,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def after_sign_up_path_for(_resource)
new_user_session_path
auth_setup_path
end
def after_sign_in_path_for(_resource)
@@ -107,4 +110,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def set_sessions
@sessions = current_user.session_activations
end
def require_not_suspended!
forbidden if current_account.suspended?
end
end

View File

@@ -6,9 +6,11 @@ class Auth::SessionsController < Devise::SessionsController
layout 'auth'
skip_before_action :require_no_authentication, only: [:create]
skip_before_action :check_user_permissions, only: [:destroy]
skip_before_action :require_functional!
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
prepend_before_action :set_pack
before_action :set_instance_presenter, only: [:new]
before_action :set_body_classes

View File

@@ -0,0 +1,58 @@
# frozen_string_literal: true
class Auth::SetupController < ApplicationController
layout 'auth'
before_action :authenticate_user!
before_action :require_unconfirmed_or_pending!
before_action :set_body_classes
before_action :set_user
skip_before_action :require_functional!
def show
flash.now[:notice] = begin
if @user.pending?
I18n.t('devise.registrations.signed_up_but_pending')
else
I18n.t('devise.registrations.signed_up_but_unconfirmed')
end
end
end
def update
# This allows updating the e-mail without entering a password as is required
# on the account settings page; however, we only allow this for accounts
# that were not confirmed yet
if @user.update(user_params)
redirect_to auth_setup_path, notice: I18n.t('devise.confirmations.send_instructions')
else
render :show
end
end
helper_method :missing_email?
private
def require_unconfirmed_or_pending!
redirect_to root_path if current_user.confirmed? && current_user.approved?
end
def set_user
@user = current_user
end
def set_body_classes
@body_classes = 'lighter'
end
def user_params
params.require(:user).permit(:email)
end
def missing_email?
truthy_param?(:missing_email)
end
end

View File

@@ -0,0 +1,50 @@
# frozen_string_literal: true
module CacheConcern
extend ActiveSupport::Concern
def render_with_cache(**options)
raise ArgumentError, 'only JSON render calls are supported' unless options.key?(:json) || block_given?
key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':')
expires_in = options.delete(:expires_in) || 3.minutes
body = Rails.cache.read(key, raw: true)
if body
render(options.except(:json, :serializer, :each_serializer, :adapter, :fields).merge(json: body))
else
if block_given?
options[:json] = yield
elsif options[:json].is_a?(Symbol)
options[:json] = send(options[:json])
end
render(options)
Rails.cache.write(key, response.body, expires_in: expires_in, raw: true)
end
end
def set_cache_headers
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
end
def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes)
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
unless uncached_ids.empty?
uncached = klass.where(id: uncached_ids).with_includes.each_with_object({}) { |item, h| h[item.id] = item }
uncached.each_value do |item|
Rails.cache.write(item, item)
end
end
raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact
end
end

View File

@@ -4,16 +4,19 @@ module Localized
extend ActiveSupport::Concern
included do
before_action :set_locale
around_action :set_locale
end
private
def set_locale
I18n.locale = default_locale
I18n.locale = current_user.locale if user_signed_in?
rescue I18n::InvalidLocale
I18n.locale = default_locale
locale = current_user.locale if respond_to?(:user_signed_in?) && user_signed_in?
locale ||= session[:locale] ||= default_locale
locale = default_locale unless I18n.available_locales.include?(locale.to_sym)
I18n.with_locale(locale) do
yield
end
end
def default_locale

View File

@@ -8,7 +8,7 @@ class EmojisController < ApplicationController
respond_to do |format|
format.json do
expires_in 3.minutes, public: true
render json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter
render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter
end
end
end

View File

@@ -8,6 +8,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :set_pack
before_action :set_body_classes
skip_before_action :require_functional!
include Localized
def destroy

View File

@@ -3,6 +3,9 @@
class Settings::DeletesController < Settings::BaseController
prepend_before_action :check_enabled_deletion
before_action :require_not_suspended!
skip_before_action :require_functional!
def show
@confirmation = Form::DeleteConfirmation.new
@@ -27,4 +30,8 @@ class Settings::DeletesController < Settings::BaseController
def delete_params
params.require(:form_delete_confirmation).permit(:password)
end
def require_not_suspended!
forbidden if current_account.suspended?
end
end

View File

@@ -5,6 +5,8 @@ class Settings::SessionsController < ApplicationController
before_action :authenticate_user!
before_action :set_session, only: :destroy
skip_before_action :require_functional!
def destroy
@session.destroy!
flash[:notice] = I18n.t('sessions.revoke_success')

View File

@@ -5,6 +5,8 @@ module Settings
class ConfirmationsController < BaseController
before_action :ensure_otp_secret
skip_before_action :require_functional!
def new
prepare_two_factor_form
end

View File

@@ -3,6 +3,8 @@
module Settings
module TwoFactorAuthentication
class RecoveryCodesController < BaseController
skip_before_action :require_functional!
def create
@recovery_codes = current_user.generate_otp_backup_codes!
current_user.save!

View File

@@ -4,6 +4,8 @@ module Settings
class TwoFactorAuthenticationsController < BaseController
before_action :verify_otp_required, only: [:create]
skip_before_action :require_functional!
def show
@confirmation = Form::TwoFactorConfirmation.new
end

View File

@@ -34,14 +34,14 @@ class StatusesController < ApplicationController
format.json do
expires_in 3.minutes, public: @status.distributable? && public_fetch_mode?
render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
end
end
end
def activity
expires_in 3.minutes, public: @status.distributable? && public_fetch_mode?
render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
end
def embed

View File

@@ -22,7 +22,7 @@ export function normalizeAccount(account) {
if (account.fields) {
account.fields = account.fields.map(pair => ({
...pair,
name_emojified: emojify(escapeTextContentForBrowser(pair.name)),
name_emojified: emojify(escapeTextContentForBrowser(pair.name), emojiMap),
value_emojified: emojify(pair.value, emojiMap),
value_plain: unescapeHTML(pair.value),
}));

View File

@@ -1,6 +1,7 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { autoPlayGif } from 'mastodon/initial_state';
export default class DisplayName extends React.PureComponent {
@@ -10,6 +11,47 @@ export default class DisplayName extends React.PureComponent {
localDomain: PropTypes.string,
};
_updateEmojis () {
const node = this.node;
if (!node || autoPlayGif) {
return;
}
const emojis = node.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
if (emoji.classList.contains('status-emoji')) {
continue;
}
emoji.classList.add('status-emoji');
emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
}
}
componentDidMount () {
this._updateEmojis();
}
componentDidUpdate () {
this._updateEmojis();
}
handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original');
}
handleEmojiMouseLeave = ({ target }) => {
target.src = target.getAttribute('data-static');
}
setRef = (c) => {
this.node = c;
}
render () {
const { others, localDomain } = this.props;
@@ -39,7 +81,7 @@ export default class DisplayName extends React.PureComponent {
}
return (
<span className='display-name'>
<span className='display-name' ref={this.setRef}>
{displayName} {suffix}
</span>
);

View File

@@ -7,6 +7,7 @@ import Permalink from './permalink';
import classnames from 'classnames';
import PollContainer from 'mastodon/containers/poll_container';
import Icon from 'mastodon/components/icon';
import { autoPlayGif } from 'mastodon/initial_state';
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
@@ -71,12 +72,35 @@ export default class StatusContent extends React.PureComponent {
}
}
_updateStatusEmojis () {
const node = this.node;
if (!node || autoPlayGif) {
return;
}
const emojis = node.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
if (emoji.classList.contains('status-emoji')) {
continue;
}
emoji.classList.add('status-emoji');
emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
}
}
componentDidMount () {
this._updateStatusLinks();
this._updateStatusEmojis();
}
componentDidUpdate () {
this._updateStatusLinks();
this._updateStatusEmojis();
}
onMentionClick = (mention, e) => {
@@ -95,6 +119,14 @@ export default class StatusContent extends React.PureComponent {
}
}
handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original');
}
handleEmojiMouseLeave = ({ target }) => {
target.src = target.getAttribute('data-static');
}
handleMouseDown = (e) => {
this.startXY = [e.clientX, e.clientY];
}

View File

@@ -77,7 +77,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
onReblog (status, e) {
if (e.shiftKey || !boostModal) {
if ((e && e.shiftKey) || !boostModal) {
this.onModalReblog(status);
} else {
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));

View File

@@ -79,6 +79,47 @@ class Header extends ImmutablePureComponent {
return !location.pathname.match(/\/(followers|following)\/?$/);
}
_updateEmojis () {
const node = this.node;
if (!node || autoPlayGif) {
return;
}
const emojis = node.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
if (emoji.classList.contains('status-emoji')) {
continue;
}
emoji.classList.add('status-emoji');
emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
}
}
componentDidMount () {
this._updateEmojis();
}
componentDidUpdate () {
this._updateEmojis();
}
handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original');
}
handleEmojiMouseLeave = ({ target }) => {
target.src = target.getAttribute('data-static');
}
setRef = (c) => {
this.node = c;
}
render () {
const { account, intl, domain, identity_proofs } = this.props;
@@ -200,7 +241,7 @@ class Header extends ImmutablePureComponent {
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
return (
<div className={classNames('account__header', { inactive: !!account.get('moved') })}>
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
<div className='account__header__image'>
<div className='account__header__info'>
{info}

View File

@@ -29,7 +29,7 @@ const emojify = (str, customEmojis = {}) => {
// if you want additional emoji handler, add statements below which set replacement and return true.
if (shortname in customEmojis) {
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
replacement = `<img draggable="false" class="emojione custom-emoji" alt="${shortname}" title="${shortname}" src="${filename}" data-original="${customEmojis[shortname].url}" data-static="${customEmojis[shortname].static_url}" />`;
return true;
}
return false;

View File

@@ -27,6 +27,12 @@ function main() {
}
};
const getEmojiAnimationHandler = (swapTo) => {
return ({ target }) => {
target.src = target.getAttribute(swapTo);
};
};
ready(() => {
const locale = document.documentElement.lang;
@@ -91,6 +97,9 @@ function main() {
if (parallaxComponents.length > 0 ) {
new Rellax('.parallax', { speed: -1 });
}
delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
});
}

View File

@@ -204,29 +204,6 @@ $content-width: 840px;
border: 0;
}
}
.muted-hint {
color: $darker-text-color;
a {
color: $highlight-text-color;
}
}
.positive-hint {
color: $valid-value-color;
font-weight: 500;
}
.negative-hint {
color: $error-value-color;
font-weight: 500;
}
.neutral-hint {
color: $dark-text-color;
font-weight: 500;
}
}
@media screen and (max-width: $no-columns-breakpoint) {
@@ -249,6 +226,41 @@ $content-width: 840px;
}
}
hr.spacer {
width: 100%;
border: 0;
margin: 20px 0;
height: 1px;
}
.muted-hint {
color: $darker-text-color;
a {
color: $highlight-text-color;
}
}
.positive-hint {
color: $valid-value-color;
font-weight: 500;
}
.negative-hint {
color: $error-value-color;
font-weight: 500;
}
.neutral-hint {
color: $dark-text-color;
font-weight: 500;
}
.warning-hint {
color: $gold-star;
font-weight: 500;
}
.filters {
display: flex;
flex-wrap: wrap;

View File

@@ -300,6 +300,13 @@ code {
}
}
.input.static .label_input__wrapper {
font-size: 16px;
padding: 10px;
border: 1px solid $dark-text-color;
border-radius: 4px;
}
input[type=text],
input[type=number],
input[type=email],

View File

@@ -200,11 +200,7 @@ class Formatter
def encode_custom_emojis(html, emojis, animate = false)
return html if emojis.empty?
emoji_map = if animate
emojis.each_with_object({}) { |e, h| h[e.shortcode] = full_asset_url(e.image.url) }
else
emojis.each_with_object({}) { |e, h| h[e.shortcode] = full_asset_url(e.image.url(:static)) }
end
emoji_map = emojis.each_with_object({}) { |e, h| h[e.shortcode] = [full_asset_url(e.image.url), full_asset_url(e.image.url(:static))] }
i = -1
tag_open_index = nil
@@ -220,7 +216,14 @@ class Formatter
emoji = emoji_map[shortcode]
if emoji
replacement = "<img draggable=\"false\" class=\"emojione\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(emoji)}\" />"
original_url, static_url = emoji
replacement = begin
if animate
"<img draggable=\"false\" class=\"emojione\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(original_url)}\" />"
else
"<img draggable=\"false\" class=\"emojione custom-emoji\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(static_url)}\" data-original=\"#{original_url}\" data-static=\"#{static_url}\" />"
end
end
before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : ''
html = before_html + replacement + html[i + 1..-1]
i += replacement.size - (shortcode.size + 2) - 1

View File

@@ -31,9 +31,6 @@ module AccountAssociations
has_many :media_attachments, dependent: :destroy
has_many :polls, dependent: :destroy
# PuSH subscriptions
has_many :subscriptions, dependent: :destroy
# Report relationships
has_many :reports, dependent: :destroy, inverse_of: :account
has_many :targeted_reports, class_name: 'Report', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account

View File

@@ -43,7 +43,7 @@ module Omniauthable
# Check if the user exists with provided email if the provider gives us a
# verified email. If no verified email was provided or the user already
# exists, we assign a temporary email and ask the user to verify it on
# the next step via Auth::ConfirmationsController.finish_signup
# the next step via Auth::SetupController.show
user = User.new(user_params_from_auth(auth))
user.account.avatar_remote_url = auth.info.image if auth.info.image =~ /\A#{URI.regexp(%w(http https))}\z/

View File

@@ -1,62 +0,0 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: subscriptions
#
# id :bigint(8) not null, primary key
# callback_url :string default(""), not null
# secret :string
# expires_at :datetime
# confirmed :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
# last_successful_delivery_at :datetime
# domain :string
# account_id :bigint(8) not null
#
class Subscription < ApplicationRecord
MIN_EXPIRATION = 1.day.to_i
MAX_EXPIRATION = 30.days.to_i
belongs_to :account
validates :callback_url, presence: true
validates :callback_url, uniqueness: { scope: :account_id }
scope :confirmed, -> { where(confirmed: true) }
scope :future_expiration, -> { where(arel_table[:expires_at].gt(Time.now.utc)) }
scope :expired, -> { where(arel_table[:expires_at].lt(Time.now.utc)) }
scope :active, -> { confirmed.future_expiration }
def lease_seconds=(value)
self.expires_at = future_expiration(value)
end
def lease_seconds
(expires_at - Time.now.utc).to_i
end
def expired?
Time.now.utc > expires_at
end
before_validation :set_min_expiration
private
def future_expiration(value)
Time.now.utc + future_offset(value).seconds
end
def future_offset(seconds)
[
[MIN_EXPIRATION, seconds.to_i].max,
MAX_EXPIRATION,
].min
end
def set_min_expiration
self.lease_seconds = 0 unless expires_at
end
end

View File

@@ -161,7 +161,11 @@ class User < ApplicationRecord
end
def active_for_authentication?
super && approved?
true
end
def functional?
confirmed? && approved? && !disabled? && !account.suspended?
end
def inactive_message

View File

@@ -1,7 +0,0 @@
# frozen_string_literal: true
class SubscriptionPolicy < ApplicationPolicy
def index?
admin?
end
end

View File

@@ -4,7 +4,7 @@ class REST::WebPushSubscriptionSerializer < ActiveModel::Serializer
attributes :id, :endpoint, :alerts, :server_key
def alerts
object.data&.dig('alerts') || {}
(object.data&.dig('alerts') || {}).each_with_object({}) { |(k, v), h| h[k] = ActiveModel::Type::Boolean.new.cast(v) }
end
def server_key

View File

@@ -24,7 +24,6 @@ class SuspendAccountService < BaseService
report_notes
scheduled_statuses
status_pins
subscriptions
).freeze
ASSOCIATIONS_ON_DESTROY = %w(

View File

@@ -1,15 +0,0 @@
- content_for :page_title do
= t('auth.confirm_email')
= simple_form_for(current_user, as: 'user', url: finish_signup_path, html: { role: 'form'}) do |f|
- if @show_errors && current_user.errors.any?
#error_explanation
- current_user.errors.full_messages.each do |msg|
= msg
%br
.fields-group
= f.input :email, wrapper: :with_label, required: true, hint: false
.actions
= f.submit t('auth.confirm_email'), class: 'button'

View File

@@ -1,6 +1,8 @@
%h4= t 'sessions.title'
%h3= t 'sessions.title'
%p.muted-hint= t 'sessions.explanation'
%hr.spacer/
.table-wrapper
%table.table.inline-table
%thead

View File

@@ -0,0 +1,16 @@
%h3= t('auth.status.account_status')
- if @user.account.suspended?
%span.negative-hint= t('user_mailer.warning.explanation.suspend')
- elsif @user.disabled?
%span.negative-hint= t('user_mailer.warning.explanation.disable')
- elsif @user.account.silenced?
%span.warning-hint= t('user_mailer.warning.explanation.silence')
- elsif !@user.confirmed?
%span.warning-hint= t('auth.status.confirming')
- elsif !@user.approved?
%span.warning-hint= t('auth.status.pending')
- else
%span.positive-hint= t('auth.status.functional')
%hr.spacer/

View File

@@ -1,25 +1,28 @@
- content_for :page_title do
= t('auth.security')
= t('settings.account_settings')
= render 'status'
%h3= t('auth.security')
= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'auth_edit' }) do |f|
= render 'shared/error_messages', object: resource
- if !use_seamless_external_login? || resource.encrypted_password.present?
.fields-group
= f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, hint: false
.fields-group
= f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true
.fields-group
= f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }, hint: false
.fields-group
= f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
.fields-row
.fields-row__column.fields-group.fields-row__column-6
= f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, disabled: current_account.suspended?
.fields-row__column.fields-group.fields-row__column-6
= f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true, disabled: current_account.suspended?
.fields-row
.fields-row__column.fields-group.fields-row__column-6
= f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }, hint: t('simple_form.hints.defaults.password'), disabled: current_account.suspended?
.fields-row__column.fields-group.fields-row__column-6
= f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }, disabled: current_account.suspended?
.actions
= f.button :button, t('generic.save_changes'), type: :submit
= f.button :button, t('generic.save_changes'), type: :submit, class: 'button', disabled: current_account.suspended?
- else
%p.hint= t('users.seamless_external_login')
@@ -27,7 +30,7 @@
= render 'sessions'
- if open_deletion?
- if open_deletion? && !current_account.suspended?
%hr.spacer/
%h4= t('auth.delete_account')
%h3= t('auth.delete_account')
%p.muted-hint= t('auth.delete_account_html', path: settings_delete_path)

View File

@@ -0,0 +1,23 @@
- content_for :page_title do
= t('auth.setup.title')
- if missing_email?
= simple_form_for(@user, url: auth_setup_path) do |f|
= render 'shared/error_messages', object: @user
.fields-group
%p.hint= t('auth.setup.email_below_hint_html')
.fields-group
= f.input :email, required: true, hint: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }
.actions
= f.submit t('admin.accounts.change_email.label'), class: 'button'
- else
.simple_form
%p.hint= t('auth.setup.email_settings_hint_html', email: content_tag(:strong, @user.email))
.form-footer
%ul.no-list
%li= link_to t('settings.account_settings'), edit_user_registration_path
%li= link_to t('auth.logout'), destroy_user_session_path, data: { method: :delete }

View File

@@ -17,7 +17,7 @@
= application.name
- else
= link_to application.name, application.website, target: '_blank', rel: 'noopener'
%th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join('<br />')
%th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join(', ')
%td= l application.created_at
%td
- unless application.superapp?

View File

@@ -5,7 +5,5 @@ class Scheduler::SubscriptionsCleanupScheduler
sidekiq_options unique: :until_executed, retry: 0
def perform
Subscription.expired.in_batches.delete_all
end
def perform; end
end