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
include RateLimitHeaders
skip_before_action :store_current_location
- skip_before_action :check_user_permissions
+ skip_before_action :require_functional!
before_action :set_cache_headers
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]}"
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)
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
if resource.email_verified?
root_path
else
- finish_signup_path
+ auth_setup_path(missing_email: '1')
end
end
end
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)
end
def after_sign_up_path_for(_resource)
- new_user_session_path
+ auth_setup_path
end
def after_sign_in_path_for(_resource)
def set_sessions
@sessions = current_user.session_activations
end
+
+ def require_not_suspended!
+ forbidden if current_account.suspended?
+ end
end
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
--- /dev/null
+# 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
before_action :authenticate_resource_owner!
before_action :set_body_classes
+ skip_before_action :require_functional!
+
include Localized
def destroy
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
def delete_params
params.require(:form_delete_confirmation).permit(:password)
end
+
+ def require_not_suspended!
+ forbidden if current_account.suspended?
+ end
end
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')
before_action :authenticate_user!
before_action :ensure_otp_secret
+ skip_before_action :require_functional!
+
def new
prepare_two_factor_form
end
before_action :authenticate_user!
+ skip_before_action :require_functional!
+
def create
@recovery_codes = current_user.generate_otp_backup_codes!
current_user.save!
before_action :authenticate_user!
before_action :verify_otp_required, only: [:create]
+ skip_before_action :require_functional!
+
def show
@confirmation = Form::TwoFactorConfirmation.new
end
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) {
}
}
+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;
}
}
+ .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],
# 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/
end
def active_for_authentication?
- super && approved?
+ true
+ end
+
+ def functional?
+ confirmed? && approved? && !disabled? && !account.suspended?
end
def inactive_message
+++ /dev/null
-- 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'
-%h4= t 'sessions.title'
+%h3= t 'sessions.title'
%p.muted-hint= t 'sessions.explanation'
+%hr.spacer/
+
.table-wrapper
%table.table.inline-table
%thead
--- /dev/null
+%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/
- 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')
= 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)
--- /dev/null
+- 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 }
= 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?
apply_for_account: Request an invite
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>
- confirm_email: Confirm email
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.
didnt_get_confirmation: Didn't receive confirmation instructions?
reset_password: Reset password
security: Security
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?
authorize_follow:
already_following: You are already following this account
devise_scope :user do
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
devise_for :users, path: 'auth', controllers: {
-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
account = Account.find_or_initialize_by(id: -99, actor_type: 'Application', locked: true, username: domain)
end
end
- describe 'Forgery protection' do
+ describe 'forgery protection' do
before do
routes.draw { post 'success' => 'api/base#success' }
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 = {
ActiveRecord::RecordInvalid => 422,
Mastodon::ValidationError => 422,
expect(response).to have_http_status(200)
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)))
get 'success'
- expect(response).to have_http_status(403)
+ expect(response).to redirect_to(edit_user_registration_path)
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
post :update
expect(response).to have_http_status(200)
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
describe 'GET #new' do
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } }
end
- it 'redirects to login page' do
+ it 'redirects to setup' do
subject
- expect(response).to redirect_to new_user_session_path
+ expect(response).to redirect_to auth_setup_path
end
it 'creates user' do
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } }
end
- it 'redirects to login page' do
+ it 'redirects to setup' do
subject
- expect(response).to redirect_to new_user_session_path
+ expect(response).to redirect_to auth_setup_path
end
it 'creates user' do
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code } }
end
- it 'redirects to login page' do
+ it 'redirects to setup' do
subject
- expect(response).to redirect_to new_user_session_path
+ expect(response).to redirect_to auth_setup_path
end
it 'creates user' do
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code } }
end
- it 'redirects to login page' do
+ it 'redirects to setup' do
subject
- expect(response).to redirect_to new_user_session_path
+ expect(response).to redirect_to auth_setup_path
end
it 'creates user' do
let(:unconfirmed_user) { user.tap { |u| u.update!(confirmed_at: nil) } }
let(:accept_language) { 'fr' }
- it 'shows a translated login error' do
- expect(flash[:alert]).to eq(I18n.t('devise.failure.unconfirmed', locale: accept_language))
+ it 'redirects to home' do
+ expect(response).to redirect_to(root_path)
end
end
get :show
expect(response).to have_http_status(200)
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
context 'when not signed in' do
it 'marks account as suspended' do
expect(user.account.reload).to be_suspended
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
context 'with incorrect password' do
context do
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_password", with: password
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
context 'when user is not confirmed' do
let(:confirmed_at) { nil }
- it { is_expected.to be false }
+ it { is_expected.to be true }
end
end
context 'when user is not confirmed' do
let(:confirmed_at) { nil }
- it { is_expected.to be false }
+ it { is_expected.to be true }
end
end
end