FEATURE: Configure Admin Account

Adds a "Step 0" to the wizard if the site has no admin accounts where
the user is prompted to finish setting up their admin account from the
list of acceptable email addresses.

Once confirmed, the wizard begins.
This commit is contained in:
Robin Ward 2016-10-18 11:44:25 -04:00
parent 674264726d
commit c03d25f170
17 changed files with 296 additions and 6 deletions

View File

@ -11,7 +11,6 @@
<p class='wizard-step-description'>{{{step.description}}}</p>
{{/if}}
{{#wizard-step-form step=step}}
{{#each step.fields as |field|}}
{{wizard-field field=field step=step wizard=wizard}}

View File

@ -14,6 +14,21 @@ body.wizard {
line-height: 1.4em;
}
.finish-installation {
.tada {
width: 300px;
}
.row {
text-align: center;
margin-bottom: 1em;
}
.help-text {
color: #999;
}
}
.discourse-logo {
background-image: asset-url('/images/wizard/discourse.png');
height: 30px;
@ -182,6 +197,7 @@ body.wizard {
padding: 0.5em;
transition: background-color .3s;
margin-right: 0.5em;
text-decoration: none;
background-color: #fff;
color: #333;
@ -480,4 +496,4 @@ body.wizard {
.invite-list .new-user { flex-direction: column !important; align-items: inherit !important; }
.invite-list .new-user .invite-email { width: 100% !important; margin-bottom: 5px !important; }
.invite-list .add-user { margin-top: 5px !important; }
}
}

View File

@ -0,0 +1,53 @@
class FinishInstallationController < ApplicationController
skip_before_filter :check_xhr, :preload_json, :redirect_to_login_if_required
layout 'finish_installation'
before_filter :ensure_no_admins, except: ['confirm_email']
def index
end
def register
@allowed_emails = find_allowed_emails
@user = User.new
if request.post?
email = params[:email].strip
raise Discourse::InvalidParameters.new unless @allowed_emails.include?(email)
return redirect_confirm(email) if User.where(email: email).exists?
@user.email = email
@user.username = params[:username]
@user.password = params[:password]
@user.password_required!
if @user.save
@email_token = @user.email_tokens.unconfirmed.active.first
Jobs.enqueue(:critical_user_email, type: :signup, user_id: @user.id, email_token: @email_token.token)
return redirect_confirm(@user.email)
end
end
end
def confirm_email
@email = session[:registered_email]
end
protected
def redirect_confirm(email)
session[:registered_email] = email
redirect_to(finish_installation_confirm_email_path)
end
def find_allowed_emails
return [] unless GlobalSetting.respond_to?(:developer_emails) && GlobalSetting.developer_emails.present?
GlobalSetting.developer_emails.split(",").map(&:strip)
end
def ensure_no_admins
raise Discourse::InvalidAccess.new unless SiteSetting.has_login_hint?
end
end

View File

@ -1,6 +1,8 @@
require_dependency 'discourse_hub'
require_dependency 'user_name_suggester'
require_dependency 'rate_limiter'
require_dependency 'wizard'
require_dependency 'wizard/builder'
class UsersController < ApplicationController
@ -411,6 +413,8 @@ class UsersController < ApplicationController
Invite.invalidate_for_email(@user.email) # invite link can't be used to log in anymore
session["password-#{params[:token]}"] = nil
logon_after_password_reset
return redirect_to(wizard_path) if Wizard.user_requires_completion?(@user)
end
end
end
@ -507,6 +511,7 @@ class UsersController < ApplicationController
if Guardian.new(@user).can_access_forum?
@user.enqueue_welcome_message('welcome_user') if @user.send_welcome_message
log_on_user(@user)
return redirect_to(wizard_path) if Wizard.user_requires_completion?(@user)
else
@needs_approval = true
end

View File

@ -119,6 +119,6 @@ class SiteSerializer < ApplicationSerializer
end
def include_wizard_required?
Wizard::Builder.new(scope.user).build.requires_completion?
Wizard.user_requires_completion?(scope.user)
end
end

View File

@ -0,0 +1,3 @@
<h1><%= t 'first_installation.confirm_email.title' %></h1>
<%= raw(t 'first_installation.confirm_email.message', email: @email) %>

View File

@ -0,0 +1,16 @@
<h1><%= t 'first_installation.congratulations' %></h1>
<div class='row'>
<%= image_tag "/images/wizard/tada.svg", class: "tada" %>
</div>
<div class='row help-text'>
<%= t 'first_installation.register.help' %>
</div>
<div class='row'>
<%= link_to(finish_installation_register_path, class: 'wizard-btn primary') do %>
<i class='fa fa-user'></i>
<%= t 'first_installation.register.button' %>
<% end %>
</div>

View File

@ -0,0 +1,57 @@
<h1><%= t 'first_installation.register.title' %></h1>
<%- if @allowed_emails.present? %>
<%= form_tag(finish_installation_register_path) do %>
<div class='wizard-field text-field'>
<label for="email">
<span class="label-value"><%= t 'js.user.email.title' %></span>
</label>
<div class='input-area'>
<%= select_tag :email, options_for_select(@allowed_emails, selected: params[:email]), class: 'combobox' %>
</div>
</div>
<div class='wizard-field text-field <% if @user.errors[:username].present? %>invalid<% end %>'>
<label for="username">
<span class="label-value"><%= t 'js.user.username.title' %></span>
</label>
<div class='field-description'><%= t 'js.user.username.instructions' %></div>
<div class='input-area'>
<%= text_field_tag(:username, params[:username]) %>
</div>
<%- @user.errors[:username].each do |e| %>
<div class='field-error-description'><%= e.to_s %></div>
<%- end %>
</div>
<div class='wizard-field text-field <% if @user.errors[:username].present? %>invalid<% end %>'>
<label for="password">
<span class="label-value"><%= t 'js.user.password.title' %></span>
</label>
<div class='field-description'><%= t 'js.user.password.instructions', count: SiteSetting.min_admin_password_length %></div>
<div class='input-area'>
<%= password_field_tag(:password, params[:password]) %>
</div>
<% @user.errors[:password].each do |e| %>
<div class='field-error-description'><%= e.to_s %></div>
<% end %>
</div>
<%= submit_tag(t('first_installation.register.button'), class: 'wizard-btn primary') %>
<%- end %>
<%- else -%>
<p><%= raw(t 'first_installation.register.no_emails') %></p>
<%- end %>
<script>
(function() {
$('select').select2({ width: '400px' });
})();
</script>

View File

@ -0,0 +1,23 @@
<html>
<head>
<%= stylesheet_link_tag 'wizard' %>
<%= render partial: "common/special_font_face" %>
<%= script 'jquery_include' %>
<%= script 'wizard-vendor' %>
<%= render partial: "layouts/head" %>
<title><%= t 'wizard.title' %></title>
</head>
<body class='wizard'>
<div id='wizard-main'>
<div class='wizard-column'>
<div class='wizard-column-contents finish-installation'>
<%= yield %>
</div>
<div class='wizard-footer'>
<div class='discourse-logo'></div>
</div>
</div>
</div>
</body>
</html>

View File

@ -6,7 +6,7 @@ if User.limit(20).count < 20 && User.where(admin: true).count == 1
else
emails = GlobalSetting.developer_emails.split(",")
if emails.length > 1
emails = emails[0..-2].join(' , ') << " or #{emails[-1]} "
emails = emails[0..-2].join(', ') << " or #{emails[-1]} "
else
emails = emails[0]
end

View File

@ -3193,6 +3193,17 @@ en:
staff_tag_remove_disallowed: "The tag \"%{tag}\" may only be removed by staff."
rss_by_tag: "Topics tagged %{tag}"
first_installation:
congratulations: "Congratulations, you installed Discourse!"
register:
button: "Register"
title: "Register Admin Account"
help: "register a new account to get started"
no_emails: "Unfortunately, no administrator emails were defined during setup, so finalizing the configuration <a href='https://meta.discourse.org/t/how-to-create-an-administrator-account-after-install/14046'>may be difficult</a>."
confirm_email:
title: "Confirm your Email"
message: "<p>We sent an activation mail to <b>%{email}</b>. Please follow the instructions in the email to activate your account.</p><p>If it doesn't arrive, ensure you have set up email correctly for your Discourse and check your spam folder.</p>"
wizard:
title: "Discourse Setup"
step:

View File

@ -37,6 +37,11 @@ Discourse::Application.routes.draw do
end
end
get "finish-installation" => "finish_installation#index"
get "finish-installation/register" => "finish_installation#register"
post "finish-installation/register" => "finish_installation#register"
get "finish-installation/confirm-email" => "finish_installation#confirm_email"
resources :directory_items
get "site" => "site#site"
@ -687,6 +692,8 @@ Discourse::Application.routes.draw do
# special case for top
root to: "list#top", constraints: HomePageConstraint.new("top"), :as => "top_lists"
root to: 'finish_installation#index', constraints: HomePageConstraint.new("finish_installation"), as: 'installation_redirect'
get "/user-api-key/new" => "user_api_keys#new"
post "/user-api-key" => "user_api_keys#create"
post "/user-api-key/revoke" => "user_api_keys#revoke"

View File

@ -4,6 +4,8 @@ class HomePageConstraint
end
def matches?(request)
return @filter == 'finish_installation' if SiteSetting.has_login_hint?
provider = Discourse.current_user_provider.new(request.env)
homepage = provider.current_user ? SiteSetting.homepage : SiteSetting.anonymous_homepage
homepage == @filter

View File

@ -76,13 +76,17 @@ class Wizard
def requires_completion?
return false unless SiteSetting.wizard_enabled?
first_admin = User.where(admin: true)
.where.not(id: Discourse.system_user.id)
.where.not(auth_token_updated_at: nil)
.order(:auth_token_updated_at)
.first
@user.present? && first_admin == @user && !completed? && (Topic.count < 15)
@user.present? && first_admin.first == @user && !completed? && (Topic.count < 15)
end
def self.user_requires_completion?(user)
Wizard::Builder.new(user).build.requires_completion?
end
end

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,85 @@
require 'rails_helper'
describe FinishInstallationController do
describe '.index' do
context "has_login_hint is false" do
before do
SiteSetting.has_login_hint = false
end
it "doesn't allow access" do
get :index
expect(response).not_to be_success
end
end
context "has_login_hint is true" do
before do
SiteSetting.has_login_hint = true
end
it "allows access" do
get :index
expect(response).to be_success
end
end
end
describe '.register' do
context "has_login_hint is false" do
before do
SiteSetting.has_login_hint = false
end
it "doesn't allow access" do
get :register
expect(response).not_to be_success
end
end
context "has_login_hint is true" do
before do
SiteSetting.has_login_hint = true
GlobalSetting.stubs(:developer_emails).returns("robin@example.com")
end
it "allows access" do
get :register
expect(response).to be_success
end
it "raises an error when the email is not in the allowed list" do
expect {
post :register, email: 'notrobin@example.com', username: 'eviltrout', password: 'disismypasswordokay'
}.to raise_error(Discourse::InvalidParameters)
end
it "doesn't redirect when fields are wrong" do
post :register, email: 'robin@example.com', username: '', password: 'disismypasswordokay'
expect(response).not_to be_redirect
end
it "registers the admin when the email is in the list" do
Jobs.expects(:enqueue).with(:critical_user_email, has_entries(type: :signup))
post :register, email: 'robin@example.com', username: 'eviltrout', password: 'disismypasswordokay'
expect(response).to be_redirect
expect(User.where(username: 'eviltrout').exists?).to eq(true)
end
end
end
describe '.confirm_email' do
context "has_login_hint is false" do
before do
SiteSetting.has_login_hint = false
end
it "shows the page" do
get :confirm_email
expect(response).to be_success
end
end
end
end

View File

@ -254,6 +254,14 @@ describe UsersController do
expect(session["password-#{token}"]).to be_blank
end
it "redirects to the wizard if you're the first admin" do
user = Fabricate(:admin, auth_token: SecureRandom.hex(16), auth_token_updated_at: Time.now)
token = user.email_tokens.create(email: user.email).token
get :password_reset, token: token
put :password_reset, token: token, password: 'hg9ow8yhg98oadminlonger'
expect(response).to be_redirect
end
it "doesn't invalidate the token when loading the page" do
user = Fabricate(:user, auth_token: SecureRandom.hex(16))
email_token = user.email_tokens.create(email: user.email)