Change unconfirmed user login behaviour (#11375)
Allow access to account settings, 2FA, authorized applications, and account deletions to unconfirmed and pending users, as well as users who had their accounts disabled. Suspended users cannot update their e-mail or password or delete their account. Display account status on account settings page, for example, when an account is frozen, limited, unconfirmed or pending review. After sign up, login users straight away and show a simple page that tells them the status of their account with links to account settings and logout, to reduce onboarding friction and allow users to correct wrongly typed e-mail addresses. Move the final sign-up step of SSO integrations to be the same as above to reduce code duplication.
This commit is contained in:
parent
fea903f574
commit
964ae8eee5
35 changed files with 297 additions and 147 deletions
|
@ -7,7 +7,7 @@ class AboutController < ApplicationController
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
before_action :set_expires_in
|
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
|
def show; end
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ class Api::BaseController < ApplicationController
|
||||||
include RateLimitHeaders
|
include RateLimitHeaders
|
||||||
|
|
||||||
skip_before_action :store_current_location
|
skip_before_action :store_current_location
|
||||||
skip_before_action :check_user_permissions
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base
|
||||||
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
||||||
|
|
||||||
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
|
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
|
def raise_not_found
|
||||||
raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}"
|
raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}"
|
||||||
|
@ -57,8 +57,8 @@ class ApplicationController < ActionController::Base
|
||||||
forbidden unless current_user&.staff?
|
forbidden unless current_user&.staff?
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_user_permissions
|
def require_functional!
|
||||||
forbidden if current_user.disabled? || current_user.account.suspended?
|
redirect_to edit_user_registration_path unless current_user.functional?
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_sign_out_path_for(_resource_or_scope)
|
def after_sign_out_path_for(_resource_or_scope)
|
||||||
|
|
|
@ -4,34 +4,15 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_user, only: [:finish_signup]
|
|
||||||
|
|
||||||
def finish_signup
|
skip_before_action :require_functional!
|
||||||
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
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_user
|
|
||||||
@user = current_user
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
@body_classes = 'lighter'
|
@body_classes = 'lighter'
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
|
||||||
params.require(:user).permit(:email)
|
|
||||||
end
|
|
||||||
|
|
||||||
def after_confirmation_path_for(_resource_name, user)
|
def after_confirmation_path_for(_resource_name, user)
|
||||||
if user.created_by_application && truthy_param?(:redirect_to_app)
|
if user.created_by_application && truthy_param?(:redirect_to_app)
|
||||||
user.created_by_application.redirect_uri
|
user.created_by_application.redirect_uri
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
if resource.email_verified?
|
if resource.email_verified?
|
||||||
root_path
|
root_path
|
||||||
else
|
else
|
||||||
finish_signup_path
|
auth_setup_path(missing_email: '1')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,9 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
before_action :set_sessions, only: [:edit, :update]
|
before_action :set_sessions, only: [:edit, :update]
|
||||||
before_action :set_instance_presenter, only: [:new, :create, :update]
|
before_action :set_instance_presenter, only: [:new, :create, :update]
|
||||||
before_action :set_body_classes, only: [:new, :create, :edit, :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
|
def new
|
||||||
super(&:build_invite_request)
|
super(&:build_invite_request)
|
||||||
|
@ -43,7 +46,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_sign_up_path_for(_resource)
|
def after_sign_up_path_for(_resource)
|
||||||
new_user_session_path
|
auth_setup_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_sign_in_path_for(_resource)
|
def after_sign_in_path_for(_resource)
|
||||||
|
@ -102,4 +105,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
def set_sessions
|
def set_sessions
|
||||||
@sessions = current_user.session_activations
|
@sessions = current_user.session_activations
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_not_suspended!
|
||||||
|
forbidden if current_account.suspended?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,8 +6,10 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
skip_before_action :require_no_authentication, only: [:create]
|
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 :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
|
||||||
|
|
||||||
before_action :set_instance_presenter, only: [:new]
|
before_action :set_instance_presenter, only: [:new]
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
|
|
58
app/controllers/auth/setup_controller.rb
Normal file
58
app/controllers/auth/setup_controller.rb
Normal 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
|
|
@ -7,6 +7,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
||||||
before_action :authenticate_resource_owner!
|
before_action :authenticate_resource_owner!
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
include Localized
|
include Localized
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
|
|
@ -5,6 +5,9 @@ class Settings::DeletesController < Settings::BaseController
|
||||||
|
|
||||||
before_action :check_enabled_deletion
|
before_action :check_enabled_deletion
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
before_action :require_not_suspended!
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@confirmation = Form::DeleteConfirmation.new
|
@confirmation = Form::DeleteConfirmation.new
|
||||||
|
@ -29,4 +32,8 @@ class Settings::DeletesController < Settings::BaseController
|
||||||
def delete_params
|
def delete_params
|
||||||
params.require(:form_delete_confirmation).permit(:password)
|
params.require(:form_delete_confirmation).permit(:password)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_not_suspended!
|
||||||
|
forbidden if current_account.suspended?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,8 @@ class Settings::SessionsController < Settings::BaseController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_session, only: :destroy
|
before_action :set_session, only: :destroy
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@session.destroy!
|
@session.destroy!
|
||||||
flash[:notice] = I18n.t('sessions.revoke_success')
|
flash[:notice] = I18n.t('sessions.revoke_success')
|
||||||
|
|
|
@ -8,6 +8,8 @@ module Settings
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :ensure_otp_secret
|
before_action :ensure_otp_secret
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def new
|
def new
|
||||||
prepare_two_factor_form
|
prepare_two_factor_form
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,8 @@ module Settings
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@recovery_codes = current_user.generate_otp_backup_codes!
|
@recovery_codes = current_user.generate_otp_backup_codes!
|
||||||
current_user.save!
|
current_user.save!
|
||||||
|
|
|
@ -7,6 +7,8 @@ module Settings
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :verify_otp_required, only: [:create]
|
before_action :verify_otp_required, only: [:create]
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@confirmation = Form::TwoFactorConfirmation.new
|
@confirmation = Form::TwoFactorConfirmation.new
|
||||||
end
|
end
|
||||||
|
|
|
@ -204,6 +204,34 @@ $content-width: 840px;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $no-columns-breakpoint) {
|
||||||
|
display: block;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
||||||
|
.sidebar-wrapper,
|
||||||
|
.content-wrapper {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: auto;
|
||||||
|
overflow: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr.spacer {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
margin: 20px 0;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.muted-hint {
|
.muted-hint {
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
|
@ -227,26 +255,10 @@ $content-width: 840px;
|
||||||
color: $dark-text-color;
|
color: $dark-text-color;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: $no-columns-breakpoint) {
|
.warning-hint {
|
||||||
display: block;
|
color: $gold-star;
|
||||||
overflow-y: auto;
|
font-weight: 500;
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
|
|
||||||
.sidebar-wrapper,
|
|
||||||
.content-wrapper {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
height: auto;
|
|
||||||
overflow: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
|
|
|
@ -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=text],
|
||||||
input[type=number],
|
input[type=number],
|
||||||
input[type=email],
|
input[type=email],
|
||||||
|
|
|
@ -43,7 +43,7 @@ module Omniauthable
|
||||||
# Check if the user exists with provided email if the provider gives us a
|
# 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
|
# 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
|
# 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 = 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/
|
user.account.avatar_remote_url = auth.info.image if auth.info.image =~ /\A#{URI.regexp(%w(http https))}\z/
|
||||||
|
|
|
@ -161,7 +161,11 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def active_for_authentication?
|
def active_for_authentication?
|
||||||
super && approved?
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def functional?
|
||||||
|
confirmed? && approved? && !disabled? && !account.suspended?
|
||||||
end
|
end
|
||||||
|
|
||||||
def inactive_message
|
def inactive_message
|
||||||
|
|
|
@ -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'
|
|
|
@ -1,6 +1,8 @@
|
||||||
%h4= t 'sessions.title'
|
%h3= t 'sessions.title'
|
||||||
%p.muted-hint= t 'sessions.explanation'
|
%p.muted-hint= t 'sessions.explanation'
|
||||||
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
.table-wrapper
|
.table-wrapper
|
||||||
%table.table.inline-table
|
%table.table.inline-table
|
||||||
%thead
|
%thead
|
||||||
|
|
16
app/views/auth/registrations/_status.html.haml
Normal file
16
app/views/auth/registrations/_status.html.haml
Normal 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/
|
|
@ -1,25 +1,28 @@
|
||||||
- content_for :page_title do
|
- 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|
|
= 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
|
= render 'shared/error_messages', object: resource
|
||||||
|
|
||||||
- if !use_seamless_external_login? || resource.encrypted_password.present?
|
- if !use_seamless_external_login? || resource.encrypted_password.present?
|
||||||
.fields-group
|
.fields-row
|
||||||
= f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, hint: false
|
.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-group
|
.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
|
= 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-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 :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
|
.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
|
- else
|
||||||
%p.hint= t('users.seamless_external_login')
|
%p.hint= t('users.seamless_external_login')
|
||||||
|
|
||||||
|
@ -27,7 +30,7 @@
|
||||||
|
|
||||||
= render 'sessions'
|
= render 'sessions'
|
||||||
|
|
||||||
- if open_deletion?
|
- if open_deletion? && !current_account.suspended?
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
%h4= t('auth.delete_account')
|
%h3= t('auth.delete_account')
|
||||||
%p.muted-hint= t('auth.delete_account_html', path: settings_delete_path)
|
%p.muted-hint= t('auth.delete_account_html', path: settings_delete_path)
|
||||||
|
|
23
app/views/auth/setup/show.html.haml
Normal file
23
app/views/auth/setup/show.html.haml
Normal 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 }
|
|
@ -17,7 +17,7 @@
|
||||||
= application.name
|
= application.name
|
||||||
- else
|
- else
|
||||||
= link_to application.name, application.website, target: '_blank', rel: 'noopener'
|
= 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= l application.created_at
|
||||||
%td
|
%td
|
||||||
- unless application.superapp?
|
- unless application.superapp?
|
||||||
|
|
|
@ -524,7 +524,6 @@ en:
|
||||||
apply_for_account: Request an invite
|
apply_for_account: Request an invite
|
||||||
change_password: Password
|
change_password: Password
|
||||||
checkbox_agreement_html: I agree to the <a href="%{rules_path}" target="_blank">server rules</a> and <a href="%{terms_path}" target="_blank">terms of service</a>
|
checkbox_agreement_html: I agree to the <a href="%{rules_path}" target="_blank">server rules</a> and <a href="%{terms_path}" target="_blank">terms of service</a>
|
||||||
confirm_email: Confirm email
|
|
||||||
delete_account: Delete account
|
delete_account: Delete account
|
||||||
delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
|
delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
|
||||||
didnt_get_confirmation: Didn't receive confirmation instructions?
|
didnt_get_confirmation: Didn't receive confirmation instructions?
|
||||||
|
@ -544,6 +543,14 @@ en:
|
||||||
reset_password: Reset password
|
reset_password: Reset password
|
||||||
security: Security
|
security: Security
|
||||||
set_new_password: Set new password
|
set_new_password: Set new password
|
||||||
|
setup:
|
||||||
|
email_below_hint_html: If the below e-mail address is incorrect, you can change it here and receive a new confirmation e-mail.
|
||||||
|
email_settings_hint_html: The confirmation e-mail was sent to %{email}. If that e-mail address is not correct, you can change it in account settings.
|
||||||
|
title: Setup
|
||||||
|
status:
|
||||||
|
account_status: Account status
|
||||||
|
confirming: Waiting for e-mail confirmation to be completed.
|
||||||
|
pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved.
|
||||||
trouble_logging_in: Trouble logging in?
|
trouble_logging_in: Trouble logging in?
|
||||||
authorize_follow:
|
authorize_follow:
|
||||||
already_following: You are already following this account
|
already_following: You are already following this account
|
||||||
|
|
|
@ -34,7 +34,10 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
devise_scope :user do
|
devise_scope :user do
|
||||||
get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite
|
get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite
|
||||||
match '/auth/finish_signup' => 'auth/confirmations#finish_signup', via: [:get, :patch], as: :finish_signup
|
|
||||||
|
namespace :auth do
|
||||||
|
resource :setup, only: [:show, :update], controller: :setup
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
devise_for :users, path: 'auth', controllers: {
|
devise_for :users, path: 'auth', controllers: {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
Doorkeeper::Application.create!(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri, scopes: 'read write follow')
|
Doorkeeper::Application.create!(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri, scopes: 'read write follow push')
|
||||||
|
|
||||||
domain = ENV['LOCAL_DOMAIN'] || Rails.configuration.x.local_domain
|
domain = ENV['LOCAL_DOMAIN'] || Rails.configuration.x.local_domain
|
||||||
account = Account.find_or_initialize_by(id: -99, actor_type: 'Application', locked: true, username: domain)
|
account = Account.find_or_initialize_by(id: -99, actor_type: 'Application', locked: true, username: domain)
|
||||||
|
|
|
@ -15,7 +15,7 @@ describe Api::BaseController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Forgery protection' do
|
describe 'forgery protection' do
|
||||||
before do
|
before do
|
||||||
routes.draw { post 'success' => 'api/base#success' }
|
routes.draw { post 'success' => 'api/base#success' }
|
||||||
end
|
end
|
||||||
|
@ -27,7 +27,45 @@ describe Api::BaseController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Error handling' do
|
describe 'non-functional accounts handling' do
|
||||||
|
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
|
||||||
|
|
||||||
|
controller do
|
||||||
|
before_action :require_user!
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
routes.draw { post 'success' => 'api/base#success' }
|
||||||
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http forbidden for unconfirmed accounts' do
|
||||||
|
user.update(confirmed_at: nil)
|
||||||
|
post 'success'
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http forbidden for pending accounts' do
|
||||||
|
user.update(approved: false)
|
||||||
|
post 'success'
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http forbidden for disabled accounts' do
|
||||||
|
user.update(disabled: true)
|
||||||
|
post 'success'
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http forbidden for suspended accounts' do
|
||||||
|
user.account.suspend!
|
||||||
|
post 'success'
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'error handling' do
|
||||||
ERRORS_WITH_CODES = {
|
ERRORS_WITH_CODES = {
|
||||||
ActiveRecord::RecordInvalid => 422,
|
ActiveRecord::RecordInvalid => 422,
|
||||||
Mastodon::ValidationError => 422,
|
Mastodon::ValidationError => 422,
|
||||||
|
|
|
@ -187,10 +187,10 @@ describe ApplicationController, type: :controller do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns http 403 if user who signed in is suspended' do
|
it 'redirects to account status page' do
|
||||||
sign_in(Fabricate(:user, account: Fabricate(:account, suspended: true)))
|
sign_in(Fabricate(:user, account: Fabricate(:account, suspended: true)))
|
||||||
get 'success'
|
get 'success'
|
||||||
expect(response).to have_http_status(403)
|
expect(response).to redirect_to(edit_user_registration_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -50,45 +50,4 @@ describe Auth::ConfirmationsController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET #finish_signup' do
|
|
||||||
subject { get :finish_signup }
|
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
|
||||||
before do
|
|
||||||
sign_in user, scope: :user
|
|
||||||
@request.env['devise.mapping'] = Devise.mappings[:user]
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders finish_signup' do
|
|
||||||
is_expected.to render_template :finish_signup
|
|
||||||
expect(assigns(:user)).to have_attributes id: user.id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'PATCH #finish_signup' do
|
|
||||||
subject { patch :finish_signup, params: { user: { email: email } } }
|
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
|
||||||
before do
|
|
||||||
sign_in user, scope: :user
|
|
||||||
@request.env['devise.mapping'] = Devise.mappings[:user]
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when email is valid' do
|
|
||||||
let(:email) { 'new_' + user.email }
|
|
||||||
|
|
||||||
it 'redirects to root_path' do
|
|
||||||
is_expected.to redirect_to root_path
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when email is invalid' do
|
|
||||||
let(:email) { '' }
|
|
||||||
|
|
||||||
it 'renders finish_signup' do
|
|
||||||
is_expected.to render_template :finish_signup
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,6 +46,15 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
|
||||||
post :update
|
post :update
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when suspended' do
|
||||||
|
it 'returns http forbidden' do
|
||||||
|
request.env["devise.mapping"] = Devise.mappings[:user]
|
||||||
|
sign_in(Fabricate(:user, account_attributes: { username: 'test', suspended_at: Time.now.utc }), scope: :user)
|
||||||
|
post :update
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET #new' do
|
describe 'GET #new' do
|
||||||
|
@ -94,9 +103,9 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
|
||||||
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } }
|
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redirects to login page' do
|
it 'redirects to setup' do
|
||||||
subject
|
subject
|
||||||
expect(response).to redirect_to new_user_session_path
|
expect(response).to redirect_to auth_setup_path
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates user' do
|
it 'creates user' do
|
||||||
|
@ -120,9 +129,9 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
|
||||||
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } }
|
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redirects to login page' do
|
it 'redirects to setup' do
|
||||||
subject
|
subject
|
||||||
expect(response).to redirect_to new_user_session_path
|
expect(response).to redirect_to auth_setup_path
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates user' do
|
it 'creates user' do
|
||||||
|
@ -148,9 +157,9 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
|
||||||
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code } }
|
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code } }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redirects to login page' do
|
it 'redirects to setup' do
|
||||||
subject
|
subject
|
||||||
expect(response).to redirect_to new_user_session_path
|
expect(response).to redirect_to auth_setup_path
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates user' do
|
it 'creates user' do
|
||||||
|
@ -176,9 +185,9 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
|
||||||
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code } }
|
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code } }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redirects to login page' do
|
it 'redirects to setup' do
|
||||||
subject
|
subject
|
||||||
expect(response).to redirect_to new_user_session_path
|
expect(response).to redirect_to auth_setup_path
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates user' do
|
it 'creates user' do
|
||||||
|
|
|
@ -160,8 +160,8 @@ RSpec.describe Auth::SessionsController, type: :controller do
|
||||||
let(:unconfirmed_user) { user.tap { |u| u.update!(confirmed_at: nil) } }
|
let(:unconfirmed_user) { user.tap { |u| u.update!(confirmed_at: nil) } }
|
||||||
let(:accept_language) { 'fr' }
|
let(:accept_language) { 'fr' }
|
||||||
|
|
||||||
it 'shows a translated login error' do
|
it 'redirects to home' do
|
||||||
expect(flash[:alert]).to eq(I18n.t('devise.failure.unconfirmed', locale: accept_language))
|
expect(response).to redirect_to(root_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,15 @@ describe Settings::DeletesController do
|
||||||
get :show
|
get :show
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when suspended' do
|
||||||
|
let(:user) { Fabricate(:user, account_attributes: { username: 'alice', suspended_at: Time.now.utc }) }
|
||||||
|
|
||||||
|
it 'returns http forbidden' do
|
||||||
|
get :show
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when not signed in' do
|
context 'when not signed in' do
|
||||||
|
@ -49,6 +58,14 @@ describe Settings::DeletesController do
|
||||||
it 'marks account as suspended' do
|
it 'marks account as suspended' do
|
||||||
expect(user.account.reload).to be_suspended
|
expect(user.account.reload).to be_suspended
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when suspended' do
|
||||||
|
let(:user) { Fabricate(:user, account_attributes: { username: 'alice', suspended_at: Time.now.utc }) }
|
||||||
|
|
||||||
|
it 'returns http forbidden' do
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with incorrect password' do
|
context 'with incorrect password' do
|
||||||
|
|
|
@ -31,12 +31,12 @@ feature "Log in" do
|
||||||
context do
|
context do
|
||||||
given(:confirmed_at) { nil }
|
given(:confirmed_at) { nil }
|
||||||
|
|
||||||
scenario "A unconfirmed user is not able to log in" do
|
scenario "A unconfirmed user is able to log in" do
|
||||||
fill_in "user_email", with: email
|
fill_in "user_email", with: email
|
||||||
fill_in "user_password", with: password
|
fill_in "user_password", with: password
|
||||||
click_on I18n.t('auth.login')
|
click_on I18n.t('auth.login')
|
||||||
|
|
||||||
is_expected.to have_css(".flash-message", text: failure_message("unconfirmed"))
|
is_expected.to have_css("div.admin-wrapper")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -506,7 +506,7 @@ RSpec.describe User, type: :model do
|
||||||
context 'when user is not confirmed' do
|
context 'when user is not confirmed' do
|
||||||
let(:confirmed_at) { nil }
|
let(:confirmed_at) { nil }
|
||||||
|
|
||||||
it { is_expected.to be false }
|
it { is_expected.to be true }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -522,7 +522,7 @@ RSpec.describe User, type: :model do
|
||||||
context 'when user is not confirmed' do
|
context 'when user is not confirmed' do
|
||||||
let(:confirmed_at) { nil }
|
let(:confirmed_at) { nil }
|
||||||
|
|
||||||
it { is_expected.to be false }
|
it { is_expected.to be true }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue