Skip to content

Commit

Permalink
Change unconfirmed user login behaviour (#11375)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Gargron committed Jul 22, 2019
1 parent fea903f commit 964ae8e
Show file tree
Hide file tree
Showing 35 changed files with 298 additions and 148 deletions.
2 changes: 1 addition & 1 deletion app/controllers/about_controller.rb
Expand Up @@ -7,7 +7,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

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/base_controller.rb
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions app/controllers/application_controller.rb
Expand Up @@ -25,7 +25,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]}"
Expand Down Expand Up @@ -57,8 +57,8 @@ def require_staff!
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)
Expand Down
21 changes: 1 addition & 20 deletions app/controllers/auth/confirmations_controller.rb
Expand Up @@ -4,34 +4,15 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
layout 'auth'

before_action :set_body_classes
before_action :set_user, only: [:finish_signup]

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

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
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/auth/omniauth_callbacks_controller.rb
Expand Up @@ -27,7 +27,7 @@ def after_sign_in_path_for(resource)
if resource.email_verified?
root_path
else
finish_signup_path
auth_setup_path(missing_email: '1')
end
end
end
9 changes: 8 additions & 1 deletion app/controllers/auth/registrations_controller.rb
Expand Up @@ -9,6 +9,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)
Expand Down Expand Up @@ -43,7 +46,7 @@ def configure_sign_up_params
end

def after_sign_up_path_for(_resource)
new_user_session_path
auth_setup_path
end

def after_sign_in_path_for(_resource)
Expand Down Expand Up @@ -102,4 +105,8 @@ def determine_layout
def set_sessions
@sessions = current_user.session_activations
end

def require_not_suspended!
forbidden if current_account.suspended?
end
end
4 changes: 3 additions & 1 deletion app/controllers/auth/sessions_controller.rb
Expand Up @@ -6,8 +6,10 @@ 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]

before_action :set_instance_presenter, only: [:new]
before_action :set_body_classes

Expand Down
58 changes: 58 additions & 0 deletions app/controllers/auth/setup_controller.rb
@@ -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
2 changes: 2 additions & 0 deletions app/controllers/oauth/authorized_applications_controller.rb
Expand Up @@ -7,6 +7,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :authenticate_resource_owner!
before_action :set_body_classes

skip_before_action :require_functional!

include Localized

def destroy
Expand Down
7 changes: 7 additions & 0 deletions app/controllers/settings/deletes_controller.rb
Expand Up @@ -5,6 +5,9 @@ class Settings::DeletesController < Settings::BaseController

before_action :check_enabled_deletion
before_action :authenticate_user!
before_action :require_not_suspended!

skip_before_action :require_functional!

def show
@confirmation = Form::DeleteConfirmation.new
Expand All @@ -29,4 +32,8 @@ def check_enabled_deletion
def delete_params
params.require(:form_delete_confirmation).permit(:password)
end

def require_not_suspended!
forbidden if current_account.suspended?
end
end
2 changes: 2 additions & 0 deletions app/controllers/settings/sessions_controller.rb
Expand Up @@ -4,6 +4,8 @@ class Settings::SessionsController < Settings::BaseController
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')
Expand Down
Expand Up @@ -8,6 +8,8 @@ class ConfirmationsController < BaseController
before_action :authenticate_user!
before_action :ensure_otp_secret

skip_before_action :require_functional!

def new
prepare_two_factor_form
end
Expand Down
Expand Up @@ -7,6 +7,8 @@ class RecoveryCodesController < BaseController

before_action :authenticate_user!

skip_before_action :require_functional!

def create
@recovery_codes = current_user.generate_otp_backup_codes!
current_user.save!
Expand Down
Expand Up @@ -7,6 +7,8 @@ class TwoFactorAuthenticationsController < BaseController
before_action :authenticate_user!
before_action :verify_otp_required, only: [:create]

skip_before_action :require_functional!

def show
@confirmation = Form::TwoFactorConfirmation.new
end
Expand Down
58 changes: 35 additions & 23 deletions app/javascript/styles/mastodon/admin.scss
Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions app/javascript/styles/mastodon/forms.scss
Expand Up @@ -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],
Expand Down
2 changes: 1 addition & 1 deletion app/models/concerns/omniauthable.rb
Expand Up @@ -43,7 +43,7 @@ def create_for_oauth(auth)
# 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/
Expand Down
6 changes: 5 additions & 1 deletion app/models/user.rb
Expand Up @@ -161,7 +161,11 @@ def pending?
end

def active_for_authentication?
super && approved?
true
end

def functional?
confirmed? && approved? && !disabled? && !account.suspended?
end

def inactive_message
Expand Down
15 changes: 0 additions & 15 deletions app/views/auth/confirmations/finish_signup.html.haml

This file was deleted.

4 changes: 3 additions & 1 deletion app/views/auth/registrations/_sessions.html.haml
@@ -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
Expand Down
16 changes: 16 additions & 0 deletions app/views/auth/registrations/_status.html.haml
@@ -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/

0 comments on commit 964ae8e

Please sign in to comment.