Skip to content

Commit 1b1fec4

Browse files
Gargronhiyuki2578
authored andcommitted
Add REST API for creating an account (mastodon#9572)
* Add REST API for creating an account The method is available to apps with a token obtained via the client credentials grant. It creates a user and account records, as well as an access token for the app that initiated the request. The user is unconfirmed, and an e-mail is sent as usual. The method returns the access token, which the app should save for later. The REST API is not available to users with unconfirmed accounts, so the app must be smart to wait for the user to click a link in their e-mail inbox. The method is rate-limited by IP to 5 requests per 30 minutes. * Redirect users back to app from confirmation if they were created with an app * Add tests * Return 403 on the method if registrations are not open * Require agreement param to be true in the API when creating an account
1 parent c336479 commit 1b1fec4

18 files changed

Lines changed: 171 additions & 20 deletions

app/controllers/api/base_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def current_user
6868
end
6969

7070
def require_user!
71-
if current_user && !current_user.disabled?
71+
if current_user && !current_user.disabled? && current_user.confirmed?
7272
set_user_activity
7373
elsif current_user
7474
render json: { error: 'Your login is currently disabled' }, status: 403

app/controllers/api/v1/accounts_controller.rb

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
11
# frozen_string_literal: true
22

33
class Api::V1::AccountsController < Api::BaseController
4-
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
4+
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute]
55
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow]
66
before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
77
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
8+
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
89

9-
before_action :require_user!, except: [:show]
10-
before_action :set_account
10+
before_action :require_user!, except: [:show, :create]
11+
before_action :set_account, except: [:create]
1112
before_action :check_account_suspension, only: [:show]
13+
before_action :check_enabled_registrations, only: [:create]
1214

1315
respond_to :json
1416

1517
def show
1618
render json: @account, serializer: REST::AccountSerializer
1719
end
1820

21+
def create
22+
token = AppSignUpService.new.call(doorkeeper_token.application, account_params)
23+
response = Doorkeeper::OAuth::TokenResponse.new(token)
24+
25+
headers.merge!(response.headers)
26+
27+
self.response_body = Oj.dump(response.body)
28+
self.status = response.status
29+
end
30+
1931
def follow
2032
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
2133

@@ -62,4 +74,12 @@ def relationships(**options)
6274
def check_account_suspension
6375
gone if @account.suspended?
6476
end
77+
78+
def account_params
79+
params.permit(:username, :email, :password, :agreement)
80+
end
81+
82+
def check_enabled_registrations
83+
forbidden if single_user_mode? || !Setting.open_registrations
84+
end
6585
end

app/controllers/auth/confirmations_controller.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
66
before_action :set_body_classes
77
before_action :set_user, only: [:finish_signup]
88

9-
# GET/PATCH /users/:id/finish_signup
109
def finish_signup
1110
return unless request.patch? && params[:user]
11+
1212
if @user.update(user_params)
1313
@user.skip_reconfirmation!
1414
bypass_sign_in(@user)
@@ -31,4 +31,12 @@ def set_body_classes
3131
def user_params
3232
params.require(:user).permit(:email)
3333
end
34+
35+
def after_confirmation_path_for(_resource_name, user)
36+
if user.created_by_application && truthy_param?(:redirect_to_app)
37+
user.created_by_application.redirect_uri
38+
else
39+
super
40+
end
41+
end
3442
end

app/controllers/auth/registrations_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def build_resource(hash = nil)
2626

2727
resource.locale = I18n.locale
2828
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
29+
resource.agreement = true
2930

3031
resource.build_account if resource.account.nil?
3132
end

app/models/user.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
# invite_id :bigint(8)
3737
# remember_token :string
3838
# chosen_languages :string is an Array
39+
# created_by_application_id :bigint(8)
3940
#
4041

4142
class User < ApplicationRecord
@@ -66,6 +67,7 @@ class User < ApplicationRecord
6667

6768
belongs_to :account, inverse_of: :user
6869
belongs_to :invite, counter_cache: :uses, optional: true
70+
belongs_to :created_by_application, class_name: 'Doorkeeper::Application', optional: true
6971
accepts_nested_attributes_for :account
7072

7173
has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
@@ -74,6 +76,7 @@ class User < ApplicationRecord
7476
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
7577
validates_with BlacklistedEmailValidator, if: :email_changed?
7678
validates_with EmailMxValidator, if: :validate_email_dns?
79+
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
7780

7881
scope :recent, -> { order(id: :desc) }
7982
scope :admins, -> { where(admin: true) }
@@ -294,7 +297,7 @@ def self.pam_get_user(attributes = {})
294297
end
295298

296299
if resource.blank?
297-
resource = new(email: attributes[:email])
300+
resource = new(email: attributes[:email], agreement: true)
298301
if Devise.check_at_sign && !resource[:email].index('@')
299302
resource[:email] = Rpam2.getenv(resource.find_pam_service, attributes[:email], attributes[:password], 'email', false)
300303
resource[:email] = "#{attributes[:email]}@#{resource.find_pam_suffix}" unless resource[:email]
@@ -307,7 +310,7 @@ def self.ldap_get_user(attributes = {})
307310
resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first })
308311

309312
if resource.blank?
310-
resource = new(email: attributes[:mail].first, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first })
313+
resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first })
311314
resource.ldap_setup(attributes)
312315
end
313316

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
class AppSignUpService < BaseService
4+
def call(app, params)
5+
return unless allowed_registrations?
6+
7+
user_params = params.slice(:email, :password, :agreement)
8+
account_params = params.slice(:username)
9+
user = User.create!(user_params.merge(created_by_application: app, password_confirmation: user_params[:password], account_attributes: account_params))
10+
11+
Doorkeeper::AccessToken.create!(application: app,
12+
resource_owner_id: user.id,
13+
scopes: app.scopes,
14+
expires_in: Doorkeeper.configuration.access_token_expires_in,
15+
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?)
16+
end
17+
18+
private
19+
20+
def allowed_registrations?
21+
Setting.open_registrations && !Rails.configuration.x.single_user_mode
22+
end
23+
end

app/views/user_mailer/confirmation_instructions.html.haml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,12 @@
5555
%tbody
5656
%tr
5757
%td.button-primary
58-
= link_to confirmation_url(@resource, confirmation_token: @token) do
59-
%span= t 'devise.mailer.confirmation_instructions.action'
58+
- if @resource.created_by_application
59+
= link_to confirmation_url(@resource, confirmation_token: @token, redirect_to_app: 'true') do
60+
%span= t 'devise.mailer.confirmation_instructions.action_with_app', app: @resource.created_by_application.name
61+
- else
62+
= link_to confirmation_url(@resource, confirmation_token: @token) do
63+
%span= t 'devise.mailer.confirmation_instructions.action'
6064

6165
%table.email-table{ cellspacing: 0, cellpadding: 0 }
6266
%tbody

app/views/user_mailer/confirmation_instructions.text.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<%= t 'devise.mailer.confirmation_instructions.explanation', host: site_hostname %>
66

7-
=> <%= confirmation_url(@resource, confirmation_token: @token) %>
7+
=> <%= confirmation_url(@resource, confirmation_token: @token, redirect_to_app: @resource.created_by_application ? 'true' : nil) %>
88

99
<%= strip_tags(t('devise.mailer.confirmation_instructions.extra_html', terms_path: about_more_url, policy_path: terms_url)) %>
1010

config/initializers/rack_attack.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ def web_request?
5757
req.authenticated_user_id if req.post? && req.path.start_with?('/api/v1/media')
5858
end
5959

60+
throttle('throttle_api_sign_up', limit: 5, period: 30.minutes) do |req|
61+
req.ip if req.post? && req.path == '/api/v1/accounts'
62+
end
63+
6064
throttle('protected_paths', limit: 25, period: 5.minutes) do |req|
6165
req.ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX
6266
end

config/locales/devise.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ en:
1818
mailer:
1919
confirmation_instructions:
2020
action: Verify email address
21+
action_with_app: Confirm and return to %{app}
2122
explanation: You have created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email.
2223
extra_html: Please also check out <a href="%{terms_path}">the rules of the instance</a> and <a href="%{policy_path}">our terms of service</a>.
2324
subject: 'Mastodon: Confirmation instructions for %{instance}'

0 commit comments

Comments
 (0)