Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for invite codes in the registration API #27805

Merged
merged 4 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 7 additions & 9 deletions app/controllers/api/v1/accounts_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

class Api::V1::AccountsController < Api::BaseController
include RegistrationHelper

before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :remove_from_followers, :block, :unblock, :mute, :unmute]
before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers]
before_action -> { doorkeeper_authorize! :follow, :write, :'write:mutes' }, only: [:mute, :unmute]
Expand Down Expand Up @@ -90,18 +92,14 @@ def relationships(**options)
end

def account_params
params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone)
params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code)
end

def check_enabled_registrations
forbidden if single_user_mode? || omniauth_only? || !allowed_registrations?
def invite
Invite.find_by(code: params[:invite_code]) if params[:invite_code].present?
end

def allowed_registrations?
Setting.registrations_mode != 'none'
end

def omniauth_only?
ENV['OMNIAUTH_ONLY'] == 'true'
def check_enabled_registrations
forbidden unless allowed_registration?(request.remote_ip, invite)
end
end
30 changes: 30 additions & 0 deletions app/controllers/api/v1/invites_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

class Api::V1::InvitesController < Api::BaseController
include RegistrationHelper

skip_before_action :require_authenticated_user!
skip_around_action :set_locale

before_action :set_invite
before_action :check_enabled_registrations!

# Override `current_user` to avoid reading session cookies
def current_user; end

def show
render json: { invite_code: params[:invite_code], instance_api_url: api_v2_instance_url }, status: 200
end

private

def set_invite
@invite = Invite.find_by!(code: params[:invite_code])
end

def check_enabled_registrations!
return render json: { error: I18n.t('invites.invalid') }, status: 401 unless @invite.valid_for_use?

raise Mastodon::NotPermittedError unless allowed_registration?(request.remote_ip, @invite)
Gargron marked this conversation as resolved.
Show resolved Hide resolved
end
end
15 changes: 2 additions & 13 deletions app/controllers/auth/registrations_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

class Auth::RegistrationsController < Devise::RegistrationsController
include RegistrationHelper
include RegistrationSpamConcern

layout :determine_layout
Expand Down Expand Up @@ -82,19 +83,7 @@ def after_update_path_for(_resource)
end

def check_enabled_registrations
redirect_to root_path if single_user_mode? || omniauth_only? || !allowed_registrations? || ip_blocked?
end

def allowed_registrations?
Setting.registrations_mode != 'none' || @invite&.valid_for_use?
end

def omniauth_only?
ENV['OMNIAUTH_ONLY'] == 'true'
end

def ip_blocked?
IpBlock.where(severity: :sign_up_block).where('ip >>= ?', request.remote_ip.to_s).exists?
redirect_to root_path unless allowed_registration?(request.remote_ip, @invite)
end

def invite_code
Expand Down
21 changes: 21 additions & 0 deletions app/helpers/registration_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module RegistrationHelper
extend ActiveSupport::Concern

def allowed_registration?(remote_ip, invite)
!Rails.configuration.x.single_user_mode && !omniauth_only? && (registrations_open? || invite&.valid_for_use?) && !ip_blocked?(remote_ip)
end

def registrations_open?
Setting.registrations_mode != 'none'
end

def omniauth_only?
ENV['OMNIAUTH_ONLY'] == 'true'
end

def ip_blocked?(remote_ip)
IpBlock.where(severity: :sign_up_block).exists?(['ip >>= ?', remote_ip.to_s])
end
end
30 changes: 8 additions & 22 deletions app/services/app_sign_up_service.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# frozen_string_literal: true

class AppSignUpService < BaseService
include RegistrationHelper

def call(app, remote_ip, params)
@app = app
@remote_ip = remote_ip
@params = params

raise Mastodon::NotPermittedError unless allowed_registrations?
raise Mastodon::NotPermittedError unless allowed_registration?(remote_ip, invite)

ApplicationRecord.transaction do
create_user!
Expand Down Expand Up @@ -34,8 +36,12 @@ def create_access_token!
)
end

def invite
Invite.find_by(code: @params[:invite_code]) if @params[:invite_code].present?
end

def user_params
@params.slice(:email, :password, :agreement, :locale, :time_zone)
@params.slice(:email, :password, :agreement, :locale, :time_zone, :invite_code)
end

def account_params
Expand All @@ -45,24 +51,4 @@ def account_params
def invite_request_params
{ text: @params[:reason] }
end

def allowed_registrations?
registrations_open? && !single_user_mode? && !omniauth_only? && !ip_blocked?
end

def registrations_open?
Setting.registrations_mode != 'none'
end

def single_user_mode?
Rails.configuration.x.single_user_mode
end

def omniauth_only?
ENV['OMNIAUTH_ONLY'] == 'true'
end

def ip_blocked?
IpBlock.where(severity: :sign_up_block).where('ip >>= ?', @remote_ip.to_s).exists?
end
end
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,7 @@ en:
'86400': 1 day
expires_in_prompt: Never
generate: Generate invite link
invalid: This invite is not valid
invited_by: 'You were invited by:'
max_uses:
one: 1 use
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ def redirect_with_vary(path)
resource :outbox, only: [:show], module: :activitypub
end

get '/invite/:invite_code', constraints: ->(req) { req.format == :json }, to: 'api/v1/invites#show'

devise_scope :user do
get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite

Expand Down
27 changes: 27 additions & 0 deletions spec/requests/invite_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

require 'rails_helper'

describe 'invites' do
let(:invite) { Fabricate(:invite) }

context 'when requesting a JSON document' do
it 'returns a JSON document with expected attributes' do
get "/invite/#{invite.code}", headers: { 'Accept' => 'application/activity+json' }

expect(response).to have_http_status(200)
expect(response.media_type).to eq 'application/json'

expect(body_as_json[:invite_code]).to eq invite.code
end
end

context 'when not requesting a JSON document' do
it 'returns an HTML page' do
get "/invite/#{invite.code}"

expect(response).to have_http_status(200)
expect(response.media_type).to eq 'text/html'
end
end
end
90 changes: 58 additions & 32 deletions spec/services/app_sign_up_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,72 @@
let(:remote_ip) { IPAddr.new('198.0.2.1') }

describe '#call' do
it 'returns nil when registrations are closed' do
tmp = Setting.registrations_mode
Setting.registrations_mode = 'none'
expect { subject.call(app, remote_ip, good_params) }.to raise_error Mastodon::NotPermittedError
Setting.registrations_mode = tmp
end
let(:params) { good_params }

it 'raises an error when params are missing' do
expect { subject.call(app, remote_ip, {}) }.to raise_error ActiveRecord::RecordInvalid
end
shared_examples 'successful registration' do
it 'creates an unconfirmed user with access token and the app\'s scope', :aggregate_failures do
access_token = subject.call(app, remote_ip, params)
expect(access_token).to_not be_nil
expect(access_token.scopes.to_s).to eq 'read write'

it 'creates an unconfirmed user with access token' do
access_token = subject.call(app, remote_ip, good_params)
expect(access_token).to_not be_nil
user = User.find_by(id: access_token.resource_owner_id)
expect(user).to_not be_nil
expect(user.confirmed?).to be false
user = User.find_by(id: access_token.resource_owner_id)
expect(user).to_not be_nil
expect(user.confirmed?).to be false

expect(user.account).to_not be_nil
expect(user.invite_request).to be_nil
end
end

it 'creates access token with the app\'s scopes' do
access_token = subject.call(app, remote_ip, good_params)
expect(access_token).to_not be_nil
expect(access_token.scopes.to_s).to eq 'read write'
context 'when registrations are closed' do
around do |example|
tmp = Setting.registrations_mode
Setting.registrations_mode = 'none'

example.run

Setting.registrations_mode = tmp
end

it 'raises an error', :aggregate_failures do
expect { subject.call(app, remote_ip, good_params) }.to raise_error Mastodon::NotPermittedError
end

context 'when using a valid invite' do
let(:params) { good_params.merge({ invite_code: invite.code }) }
let(:invite) { Fabricate(:invite) }

before do
invite.user.approve!
end

it_behaves_like 'successful registration'
end

context 'when using an invalid invite' do
let(:params) { good_params.merge({ invite_code: invite.code }) }
let(:invite) { Fabricate(:invite, uses: 1, max_uses: 1) }

it 'raises an error', :aggregate_failures do
expect { subject.call(app, remote_ip, params) }.to raise_error Mastodon::NotPermittedError
end
end
end

it 'creates an account' do
access_token = subject.call(app, remote_ip, good_params)
expect(access_token).to_not be_nil
user = User.find_by(id: access_token.resource_owner_id)
expect(user).to_not be_nil
expect(user.account).to_not be_nil
expect(user.invite_request).to be_nil
it 'raises an error when params are missing' do
expect { subject.call(app, remote_ip, {}) }.to raise_error ActiveRecord::RecordInvalid
end

it 'creates an account with invite request text' do
access_token = subject.call(app, remote_ip, good_params.merge(reason: 'Foo bar'))
expect(access_token).to_not be_nil
user = User.find_by(id: access_token.resource_owner_id)
expect(user).to_not be_nil
expect(user.invite_request&.text).to eq 'Foo bar'
it_behaves_like 'successful registration'

context 'when given an invite request text' do
it 'creates an account with invite request text' do
access_token = subject.call(app, remote_ip, good_params.merge(reason: 'Foo bar'))
expect(access_token).to_not be_nil
user = User.find_by(id: access_token.resource_owner_id)
expect(user).to_not be_nil
expect(user.invite_request&.text).to eq 'Foo bar'
end
end
end
end