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> <p class='wizard-step-description'>{{{step.description}}}</p>
{{/if}} {{/if}}
{{#wizard-step-form step=step}} {{#wizard-step-form step=step}}
{{#each step.fields as |field|}} {{#each step.fields as |field|}}
{{wizard-field field=field step=step wizard=wizard}} {{wizard-field field=field step=step wizard=wizard}}

View File

@ -14,6 +14,21 @@ body.wizard {
line-height: 1.4em; line-height: 1.4em;
} }
.finish-installation {
.tada {
width: 300px;
}
.row {
text-align: center;
margin-bottom: 1em;
}
.help-text {
color: #999;
}
}
.discourse-logo { .discourse-logo {
background-image: asset-url('/images/wizard/discourse.png'); background-image: asset-url('/images/wizard/discourse.png');
height: 30px; height: 30px;
@ -182,6 +197,7 @@ body.wizard {
padding: 0.5em; padding: 0.5em;
transition: background-color .3s; transition: background-color .3s;
margin-right: 0.5em; margin-right: 0.5em;
text-decoration: none;
background-color: #fff; background-color: #fff;
color: #333; color: #333;
@ -480,4 +496,4 @@ body.wizard {
.invite-list .new-user { flex-direction: column !important; align-items: inherit !important; } .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 .new-user .invite-email { width: 100% !important; margin-bottom: 5px !important; }
.invite-list .add-user { margin-top: 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 'discourse_hub'
require_dependency 'user_name_suggester' require_dependency 'user_name_suggester'
require_dependency 'rate_limiter' require_dependency 'rate_limiter'
require_dependency 'wizard'
require_dependency 'wizard/builder'
class UsersController < ApplicationController 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 Invite.invalidate_for_email(@user.email) # invite link can't be used to log in anymore
session["password-#{params[:token]}"] = nil session["password-#{params[:token]}"] = nil
logon_after_password_reset logon_after_password_reset
return redirect_to(wizard_path) if Wizard.user_requires_completion?(@user)
end end
end end
end end
@ -507,6 +511,7 @@ class UsersController < ApplicationController
if Guardian.new(@user).can_access_forum? if Guardian.new(@user).can_access_forum?
@user.enqueue_welcome_message('welcome_user') if @user.send_welcome_message @user.enqueue_welcome_message('welcome_user') if @user.send_welcome_message
log_on_user(@user) log_on_user(@user)
return redirect_to(wizard_path) if Wizard.user_requires_completion?(@user)
else else
@needs_approval = true @needs_approval = true
end end

View File

@ -119,6 +119,6 @@ class SiteSerializer < ApplicationSerializer
end end
def include_wizard_required? def include_wizard_required?
Wizard::Builder.new(scope.user).build.requires_completion? Wizard.user_requires_completion?(scope.user)
end end
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 else
emails = GlobalSetting.developer_emails.split(",") emails = GlobalSetting.developer_emails.split(",")
if emails.length > 1 if emails.length > 1
emails = emails[0..-2].join(' , ') << " or #{emails[-1]} " emails = emails[0..-2].join(', ') << " or #{emails[-1]} "
else else
emails = emails[0] emails = emails[0]
end end

View File

@ -3193,6 +3193,17 @@ en:
staff_tag_remove_disallowed: "The tag \"%{tag}\" may only be removed by staff." staff_tag_remove_disallowed: "The tag \"%{tag}\" may only be removed by staff."
rss_by_tag: "Topics tagged %{tag}" 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: wizard:
title: "Discourse Setup" title: "Discourse Setup"
step: step:

View File

@ -37,6 +37,11 @@ Discourse::Application.routes.draw do
end end
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 resources :directory_items
get "site" => "site#site" get "site" => "site#site"
@ -687,6 +692,8 @@ Discourse::Application.routes.draw do
# special case for top # special case for top
root to: "list#top", constraints: HomePageConstraint.new("top"), :as => "top_lists" 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" get "/user-api-key/new" => "user_api_keys#new"
post "/user-api-key" => "user_api_keys#create" post "/user-api-key" => "user_api_keys#create"
post "/user-api-key/revoke" => "user_api_keys#revoke" post "/user-api-key/revoke" => "user_api_keys#revoke"

View File

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

View File

@ -76,13 +76,17 @@ class Wizard
def requires_completion? def requires_completion?
return false unless SiteSetting.wizard_enabled? return false unless SiteSetting.wizard_enabled?
first_admin = User.where(admin: true) first_admin = User.where(admin: true)
.where.not(id: Discourse.system_user.id) .where.not(id: Discourse.system_user.id)
.where.not(auth_token_updated_at: nil) .where.not(auth_token_updated_at: nil)
.order(:auth_token_updated_at) .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
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 expect(session["password-#{token}"]).to be_blank
end 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 it "doesn't invalidate the token when loading the page" do
user = Fabricate(:user, auth_token: SecureRandom.hex(16)) user = Fabricate(:user, auth_token: SecureRandom.hex(16))
email_token = user.email_tokens.create(email: user.email) email_token = user.email_tokens.create(email: user.email)