mirror of
https://github.com/Fishwaldo/huginn.git
synced 2025-03-15 19:31:26 +00:00
Merge pull request #1330 from kreuzwerker/feature/user-admin-interface
Admin user management
This commit is contained in:
commit
bf7c2feba4
25 changed files with 617 additions and 49 deletions
36
.env.example
36
.env.example
|
@ -40,9 +40,9 @@ DATABASE_PASSWORD=""
|
|||
# Should Rails force all requests to use SSL?
|
||||
FORCE_SSL=false
|
||||
|
||||
############################
|
||||
# Allowing Signups #
|
||||
############################
|
||||
################################################
|
||||
# User authentication and registration #
|
||||
################################################
|
||||
|
||||
# This invitation code will be required for users to signup with your Huginn installation.
|
||||
# You can see its use in user.rb. PLEASE CHANGE THIS!
|
||||
|
@ -51,6 +51,36 @@ INVITATION_CODE=try-huginn
|
|||
# If you don't want to require new users to have an invitation code in order to sign up, set this to true.
|
||||
SKIP_INVITATION_CODE=false
|
||||
|
||||
# If you'd like to require new users to confirm their email address after sign up, set this to true.
|
||||
REQUIRE_CONFIRMED_EMAIL=false
|
||||
|
||||
# If REQUIRE_CONFIRMED_EMAIL is true, set this to the duration in which a user needs to confirm their email address.
|
||||
ALLOW_UNCONFIRMED_ACCESS_FOR=2.days
|
||||
|
||||
# Duration for which the above confirmation token is valid
|
||||
CONFIRM_WITHIN=3.days
|
||||
|
||||
# Minimum password length
|
||||
MIN_PASSWORD_LENGTH=8
|
||||
|
||||
# Duration for which the reset password token is valid
|
||||
RESET_PASSWORD_WITHIN=6.hours
|
||||
|
||||
# Set to 'failed_attempts' to lock user accounts for the UNLOCK_AFTER period they fail MAX_FAILED_LOGIN_ATTEMPTS login attempts. Set to 'none' to allow unlimited failed login attempts.
|
||||
LOCK_STRATEGY=failed_attempts
|
||||
|
||||
# After how many failed login attempts the account is locked when LOCK_STRATEGY is set to failed_attempts.
|
||||
MAX_FAILED_LOGIN_ATTEMPTS=10
|
||||
|
||||
# Can be set to 'email', 'time', 'both' or 'none'. 'none' requires manual unlocking of your users!
|
||||
UNLOCK_STRATEGY=both
|
||||
|
||||
# Duration after which the user is unlocked when UNLOCK_STRATEGY is 'both' or 'time' and LOCK_STRATEGY is 'failed_attempts'
|
||||
UNLOCK_AFTER=1.hour
|
||||
|
||||
# Duration for which the user will be remembered without asking for credentials again.
|
||||
REMEMBER_FOR=4.weeks
|
||||
|
||||
#############################
|
||||
# Email Configuration #
|
||||
#############################
|
||||
|
|
94
app/controllers/admin/users_controller.rb
Normal file
94
app/controllers/admin/users_controller.rb
Normal file
|
@ -0,0 +1,94 @@
|
|||
class Admin::UsersController < ApplicationController
|
||||
before_action :authenticate_admin!
|
||||
|
||||
before_action :find_user, only: [:edit, :destroy, :update, :deactivate, :activate]
|
||||
|
||||
helper_method :resource
|
||||
|
||||
def index
|
||||
@users = User.reorder('created_at DESC').page(params[:page])
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json { render json: @users }
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
@user = User.new
|
||||
end
|
||||
|
||||
def create
|
||||
admin = params[:user].delete(:admin)
|
||||
@user = User.new(params[:user])
|
||||
@user.requires_no_invitation_code!
|
||||
@user.admin = admin
|
||||
|
||||
respond_to do |format|
|
||||
if @user.save
|
||||
format.html { redirect_to admin_users_path, notice: "User '#{@user.username}' was successfully created." }
|
||||
format.json { render json: @user, status: :ok, location: admin_users_path(@user) }
|
||||
else
|
||||
format.html { render action: 'new' }
|
||||
format.json { render json: @user.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
admin = params[:user].delete(:admin)
|
||||
params[:user].except!(:password, :password_confirmation) if params[:user][:password].blank?
|
||||
@user.assign_attributes(params[:user])
|
||||
@user.admin = admin
|
||||
|
||||
respond_to do |format|
|
||||
if @user.save
|
||||
format.html { redirect_to admin_users_path, notice: "User '#{@user.username}' was successfully updated." }
|
||||
format.json { render json: @user, status: :ok, location: admin_users_path(@user) }
|
||||
else
|
||||
format.html { render action: 'edit' }
|
||||
format.json { render json: @user.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@user.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to admin_users_path, notice: "User '#{@user.username}' was deleted." }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def deactivate
|
||||
@user.deactivate!
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to admin_users_path, notice: "User '#{@user.username}' was deactivated." }
|
||||
format.json { render json: @user, status: :ok, location: admin_users_path(@user) }
|
||||
end
|
||||
end
|
||||
|
||||
def activate
|
||||
@user.activate!
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to admin_users_path, notice: "User '#{@user.username}' was activated." }
|
||||
format.json { render json: @user, status: :ok, location: admin_users_path(@user) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_user
|
||||
@user = User.find(params[:id])
|
||||
end
|
||||
|
||||
def resource
|
||||
@user
|
||||
end
|
||||
end
|
13
app/helpers/users_helper.rb
Normal file
13
app/helpers/users_helper.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
module UsersHelper
|
||||
def user_account_state(user)
|
||||
if !user.active?
|
||||
content_tag :span, 'inactive', class: 'label label-danger'
|
||||
elsif user.access_locked?
|
||||
content_tag :span, 'locked', class: 'label label-danger'
|
||||
elsif ENV['REQUIRE_CONFIRMED_EMAIL'] == 'true' && !user.confirmed?
|
||||
content_tag :span, 'unconfirmed', class: 'label label-warning'
|
||||
else
|
||||
content_tag :span, 'active', class: 'label label-success'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -61,8 +61,8 @@ class Agent < ActiveRecord::Base
|
|||
has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :agent
|
||||
has_many :scenarios, :through => :scenario_memberships, :inverse_of => :agents
|
||||
|
||||
scope :active, -> { where(disabled: false) }
|
||||
scope :inactive, -> { where(disabled: true) }
|
||||
scope :active, -> { where(disabled: false, deactivated: false) }
|
||||
scope :inactive, -> { where(['disabled = ? OR deactivated = ?', true, true]) }
|
||||
|
||||
scope :of_type, lambda { |type|
|
||||
type = case type
|
||||
|
@ -381,7 +381,7 @@ class Agent < ActiveRecord::Base
|
|||
joins("JOIN links ON (links.receiver_id = agents.id)").
|
||||
joins("JOIN agents AS sources ON (links.source_id = sources.id)").
|
||||
joins("JOIN events ON (events.agent_id = sources.id AND events.id > links.event_id_at_creation)").
|
||||
where("NOT agents.disabled AND (agents.last_checked_event_id IS NULL OR events.id > agents.last_checked_event_id)")
|
||||
where("NOT agents.disabled AND NOT agents.deactivated AND (agents.last_checked_event_id IS NULL OR events.id > agents.last_checked_event_id)")
|
||||
if options[:only_receivers].present?
|
||||
scope = scope.where("agents.id in (?)", options[:only_receivers])
|
||||
end
|
||||
|
@ -432,7 +432,7 @@ class Agent < ActiveRecord::Base
|
|||
# per type of agent, so you can override this to define custom bulk check behavior for your custom Agent type.
|
||||
def bulk_check(schedule)
|
||||
raise "Call #bulk_check on the appropriate subclass of Agent" if self == Agent
|
||||
where("agents.schedule = ? and disabled = false", schedule).pluck("agents.id").each do |agent_id|
|
||||
where("NOT disabled AND NOT deactivated AND schedule = ?", schedule).pluck("agents.id").each do |agent_id|
|
||||
async_check(agent_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
# Huginn is designed to be a multi-User system. Users have many Agents (and Events created by those Agents).
|
||||
class User < ActiveRecord::Base
|
||||
devise :database_authenticatable, :registerable,
|
||||
:recoverable, :rememberable, :trackable, :validatable, :lockable,
|
||||
:omniauthable
|
||||
DEVISE_MODULES = [:database_authenticatable, :registerable,
|
||||
:recoverable, :rememberable, :trackable,
|
||||
:validatable, :lockable, :omniauthable,
|
||||
(ENV['REQUIRE_CONFIRMED_EMAIL'] == 'true' ? :confirmable : nil)].compact
|
||||
devise *DEVISE_MODULES
|
||||
|
||||
INVITATION_CODES = [ENV['INVITATION_CODE'] || 'try-huginn']
|
||||
|
||||
|
@ -16,9 +18,9 @@ class User < ActiveRecord::Base
|
|||
attr_accessible *(ACCESSIBLE_ATTRIBUTES + [:admin]), :as => :admin
|
||||
|
||||
validates_presence_of :username
|
||||
validates_uniqueness_of :username
|
||||
validates :username, uniqueness: { case_sensitive: false }
|
||||
validates_format_of :username, :with => /\A[a-zA-Z0-9_-]{3,15}\Z/, :message => "can only contain letters, numbers, underscores, and dashes, and must be between 3 and 15 characters in length."
|
||||
validates_inclusion_of :invitation_code, :on => :create, :in => INVITATION_CODES, :message => "is not valid", if: ->{ User.using_invitation_code? }
|
||||
validates_inclusion_of :invitation_code, :on => :create, :in => INVITATION_CODES, :message => "is not valid", if: -> { !requires_no_invitation_code? && User.using_invitation_code? }
|
||||
|
||||
has_many :user_credentials, :dependent => :destroy, :inverse_of => :user
|
||||
has_many :events, -> { order("events.created_at desc") }, :dependent => :delete_all, :inverse_of => :user
|
||||
|
@ -41,7 +43,41 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def active?
|
||||
!deactivated_at
|
||||
end
|
||||
|
||||
def deactivate!
|
||||
User.transaction do
|
||||
agents.update_all(deactivated: true)
|
||||
update_attribute(:deactivated_at, Time.now)
|
||||
end
|
||||
end
|
||||
|
||||
def activate!
|
||||
User.transaction do
|
||||
agents.update_all(deactivated: false)
|
||||
update_attribute(:deactivated_at, nil)
|
||||
end
|
||||
end
|
||||
|
||||
def active_for_authentication?
|
||||
super && active?
|
||||
end
|
||||
|
||||
def inactive_message
|
||||
active? ? super : :deactivated_account
|
||||
end
|
||||
|
||||
def self.using_invitation_code?
|
||||
ENV['SKIP_INVITATION_CODE'] != 'true'
|
||||
end
|
||||
|
||||
def requires_no_invitation_code!
|
||||
@requires_no_invitation_code = true
|
||||
end
|
||||
|
||||
def requires_no_invitation_code?
|
||||
!!@requires_no_invitation_code
|
||||
end
|
||||
end
|
||||
|
|
26
app/views/admin/users/_form.html.erb
Normal file
26
app/views/admin/users/_form.html.erb
Normal file
|
@ -0,0 +1,26 @@
|
|||
<%= form_for([:admin, @user], html: { class: 'form-horizontal' }) do |f| %>
|
||||
<%= devise_error_messages! %>
|
||||
<%= render partial: '/devise/registrations/common_registration_fields', locals: { f: f } %>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-4 col-md-10">
|
||||
<%= f.label :admin do %>
|
||||
<%= f.check_box :admin %> Admin
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-4 col-md-10">
|
||||
<%= f.submit class: "btn btn-primary" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, admin_users_path, class: "btn btn-default" %>
|
||||
</div>
|
||||
</div>
|
9
app/views/admin/users/edit.html.erb
Normal file
9
app/views/admin/users/edit.html.erb
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div class='container'>
|
||||
<div class='row'>
|
||||
<div class='col-md-12'>
|
||||
<h2>Edit User</h2>
|
||||
|
||||
<%= render partial: 'form' %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
55
app/views/admin/users/index.html.erb
Normal file
55
app/views/admin/users/index.html.erb
Normal file
|
@ -0,0 +1,55 @@
|
|||
<div class='container'>
|
||||
<div class='row'>
|
||||
<div class='col-md-12'>
|
||||
<div class="page-header">
|
||||
<h2>
|
||||
Users
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class='table-responsive'>
|
||||
<table class='table table-striped events'>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>State</th>
|
||||
<th>Active agents</th>
|
||||
<th>Deactivated agents</th>
|
||||
<th>Registered since</th>
|
||||
<th>Options</th>
|
||||
</tr>
|
||||
|
||||
<% @users.each do |user| %>
|
||||
<tr>
|
||||
<td><%= link_to user.username, edit_admin_user_path(user) %></td>
|
||||
<td><%= user.email %></td>
|
||||
<td><%= user_account_state(user) %></td>
|
||||
<td><%= user.agents.active.count %></td>
|
||||
<td><%= user.agents.inactive.count %></td>
|
||||
<td title='<%= user.created_at %>'><%= time_ago_in_words user.created_at %> ago</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-xs">
|
||||
<% if user != current_user %>
|
||||
<% if user.active? %>
|
||||
<%= link_to 'Deactivate', deactivate_admin_user_path(user), method: :put, class: "btn btn-default" %>
|
||||
<% else %>
|
||||
<%= link_to 'Activate', activate_admin_user_path(user), method: :put, class: "btn btn-default" %>
|
||||
<% end %>
|
||||
<%= link_to 'Delete', admin_user_path(user), method: :delete, data: { confirm: 'Are you sure? This can not be undone.' }, class: "btn btn-default" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<%= paginate @users, theme: 'twitter-bootstrap-3' %>
|
||||
|
||||
<div class="btn-group">
|
||||
<%= link_to icon_tag('glyphicon-plus') + ' New User', new_admin_user_path, class: "btn btn-default" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
9
app/views/admin/users/new.html.erb
Normal file
9
app/views/admin/users/new.html.erb
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div class='container'>
|
||||
<div class='row'>
|
||||
<div class='col-md-12'>
|
||||
<h2>Create new User</h2>
|
||||
|
||||
<%= render partial: 'form' %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,28 @@
|
|||
<div class="form-group">
|
||||
<%= f.label :email, class: 'col-md-4 control-label' %>
|
||||
<div class="col-md-6">
|
||||
<%= f.email_field :email, autofocus: true, class: 'form-control' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :username, class: 'col-md-4 control-label' %>
|
||||
<div class="col-md-6">
|
||||
<%= f.text_field :username, class: 'form-control' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :password, class: 'col-md-4 control-label' %>
|
||||
<div class="col-md-6">
|
||||
<%= f.password_field :password, autocomplete: "off", class: 'form-control' %>
|
||||
<% if @validatable %><span class="help-inline"><%= @minimum_password_length %> characters minimum.</span><% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :password_confirmation, class: 'col-md-4 control-label' %>
|
||||
<div class="col-md-6">
|
||||
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %>
|
||||
</div>
|
||||
</div>
|
|
@ -41,34 +41,7 @@ bin/setup_heroku
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :email, class: 'col-md-4 control-label' %>
|
||||
<div class="col-md-6">
|
||||
<%= f.email_field :email, autofocus: true, class: 'form-control' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :username, class: 'col-md-4 control-label' %>
|
||||
<div class="col-md-6">
|
||||
<%= f.text_field :username, class: 'form-control' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :password, class: 'col-md-4 control-label' %>
|
||||
<div class="col-md-6">
|
||||
<%= f.password_field :password, autocomplete: "off", class: 'form-control' %>
|
||||
<% if @validatable %><span class="help-inline"><%= @minimum_password_length %> characters minimum.</span><% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :password_confirmation, class: 'col-md-4 control-label' %>
|
||||
<div class="col-md-6">
|
||||
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %>
|
||||
</div>
|
||||
</div>
|
||||
<%= render partial: 'common_registration_fields', locals: { f: f } %>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-4 col-md-10">
|
||||
|
|
|
@ -74,6 +74,9 @@
|
|||
<li>
|
||||
<%= link_to 'Job Management', jobs_path, :tabindex => '-1' %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to 'User Management', admin_users_path, tabindex: '-1' %>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<%= link_to 'About', 'https://github.com/cantino/huginn', :tabindex => "-1" %>
|
||||
|
|
|
@ -103,7 +103,7 @@ Devise.setup do |config|
|
|||
# able to access the website for two days without confirming their account,
|
||||
# access will be blocked just in the third day. Default is 0.days, meaning
|
||||
# the user cannot access the website without confirming their account.
|
||||
# config.allow_unconfirmed_access_for = 2.days
|
||||
config.allow_unconfirmed_access_for = Utils.parse_duration(ENV['ALLOW_UNCONFIRMED_ACCESS_FOR']).presence || 2.days
|
||||
|
||||
# A period that the user is allowed to confirm their account before their
|
||||
# token becomes invalid. For example, if set to 3.days, the user can confirm
|
||||
|
@ -111,7 +111,7 @@ Devise.setup do |config|
|
|||
# their account can't be confirmed with the token any more.
|
||||
# Default is nil, meaning there is no restriction on how long a user can take
|
||||
# before confirming their account.
|
||||
# config.confirm_within = 3.days
|
||||
config.confirm_within = Utils.parse_duration(ENV['CONFIRM_WITHIN']).presence || 3.days
|
||||
|
||||
# If true, requires any email changes to be confirmed (exactly the same way as
|
||||
# initial account confirmation) to be applied. Requires additional unconfirmed_email
|
||||
|
@ -124,7 +124,7 @@ Devise.setup do |config|
|
|||
|
||||
# ==> Configuration for :rememberable
|
||||
# The time the user will be remembered without asking for credentials again.
|
||||
config.remember_for = 4.weeks
|
||||
config.remember_for = Utils.parse_duration(ENV['REMEMBER_FOR']).presence || 4.weeks
|
||||
|
||||
# Invalidates all the remember me tokens when the user signs out.
|
||||
config.expire_all_remember_me_on_sign_out = true
|
||||
|
@ -142,7 +142,7 @@ Devise.setup do |config|
|
|||
|
||||
# ==> Configuration for :validatable
|
||||
# Range for password length.
|
||||
config.password_length = 8..128
|
||||
config.password_length = (Utils.if_present(ENV['MIN_PASSWORD_LENGTH'], :to_i) || 8)..128
|
||||
|
||||
# Email regex used to validate email formats. It simply asserts that
|
||||
# one (and only one) @ exists in the given string. This is mainly
|
||||
|
@ -158,7 +158,7 @@ Devise.setup do |config|
|
|||
# Defines which strategy will be used to lock an account.
|
||||
# :failed_attempts = Locks an account after a number of failed attempts to sign in.
|
||||
# :none = No lock strategy. You should handle locking by yourself.
|
||||
config.lock_strategy = :failed_attempts
|
||||
config.lock_strategy = Utils.if_present(ENV['LOCK_STRATEGY'], :to_sym) || :failed_attempts
|
||||
|
||||
# Defines which key will be used when locking and unlocking an account
|
||||
config.unlock_keys = [ :email ]
|
||||
|
@ -168,14 +168,14 @@ Devise.setup do |config|
|
|||
# :time = Re-enables login after a certain amount of time (see :unlock_in below)
|
||||
# :both = Enables both strategies
|
||||
# :none = No unlock strategy. You should handle unlocking by yourself.
|
||||
config.unlock_strategy = :both
|
||||
config.unlock_strategy = Utils.if_present(ENV['UNLOCK_STRATEGY'], :to_sym) || :both
|
||||
|
||||
# Number of authentication tries before locking an account if lock_strategy
|
||||
# is failed attempts.
|
||||
config.maximum_attempts = 10
|
||||
config.maximum_attempts = Utils.if_present(ENV['MAX_FAILED_LOGIN_ATTEMPTS'], :to_i) || 10
|
||||
|
||||
# Time interval to unlock the account if :time is enabled as unlock_strategy.
|
||||
config.unlock_in = 1.hour
|
||||
config.unlock_in = Utils.parse_duration(ENV['UNLOCK_AFTER']).presence || 1.hour
|
||||
|
||||
# Warn on the last attempt before the account is locked.
|
||||
# config.last_attempt_warning = true
|
||||
|
@ -188,7 +188,7 @@ Devise.setup do |config|
|
|||
# Time interval you can reset your password with a reset password key.
|
||||
# Don't put a too small interval or your users won't have the time to
|
||||
# change their passwords.
|
||||
config.reset_password_within = 6.hours
|
||||
config.reset_password_within = Utils.parse_duration(ENV['RESET_PASSWORD_WITHIN']).presence || 6.hours
|
||||
|
||||
# ==> Configuration for :encryptable
|
||||
# Allow you to use another encryption algorithm besides bcrypt (default). You can use
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
|
||||
|
||||
en:
|
||||
devise:
|
||||
failure:
|
||||
deactivated_account: "Your account has been deactivated by an administrator."
|
||||
datetime:
|
||||
distance_in_words:
|
||||
half_a_minute: "half a minute"
|
||||
|
|
|
@ -66,6 +66,15 @@ Huginn::Application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
namespace :admin do
|
||||
resources :users, except: :show do
|
||||
member do
|
||||
put :deactivate
|
||||
put :activate
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
get "/worker_status" => "worker_status#show"
|
||||
|
||||
match "/users/:user_id/web_requests/:agent_id/:secret" => "web_requests#handle_request", :as => :web_requests, :via => [:get, :post, :put, :delete]
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
class AddConfirmableAttributesToUsers < ActiveRecord::Migration
|
||||
def change
|
||||
change_table(:users) do |t|
|
||||
## Confirmable
|
||||
t.string :confirmation_token
|
||||
t.datetime :confirmed_at
|
||||
t.datetime :confirmation_sent_at
|
||||
t.string :unconfirmed_email # Only if using reconfirmable
|
||||
end
|
||||
|
||||
add_index :users, :confirmation_token, unique: true
|
||||
|
||||
if ENV['REQUIRE_CONFIRMED_EMAIL'] != 'true' && ActiveRecord::Base.connection.column_exists?(:users, :confirmed_at)
|
||||
User.update_all('confirmed_at = NOW()')
|
||||
end
|
||||
end
|
||||
end
|
7
db/migrate/20160302095413_add_deactivated_at_to_users.rb
Normal file
7
db/migrate/20160302095413_add_deactivated_at_to_users.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
class AddDeactivatedAtToUsers < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :users, :deactivated_at, :datetime
|
||||
|
||||
add_index :users, :deactivated_at
|
||||
end
|
||||
end
|
6
db/migrate/20160307084729_add_deactivated_to_agents.rb
Normal file
6
db/migrate/20160307084729_add_deactivated_to_agents.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
class AddDeactivatedToAgents < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :agents, :deactivated, :boolean, default: false
|
||||
add_index :agents, [:disabled, :deactivated]
|
||||
end
|
||||
end
|
21
db/migrate/20160307085545_warn_about_duplicate_usernames.rb
Normal file
21
db/migrate/20160307085545_warn_about_duplicate_usernames.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
class WarnAboutDuplicateUsernames < ActiveRecord::Migration
|
||||
def up
|
||||
names = User.group('LOWER(username)').having('count(*) > 1').pluck('LOWER(username)')
|
||||
if names.length > 0
|
||||
puts "-----------------------------------------------------"
|
||||
puts "--------------------- WARNiNG -----------------------"
|
||||
puts "-------- Found users with duplicate usernames -------"
|
||||
puts "-----------------------------------------------------"
|
||||
puts "For the users to log in using their username they have to change it to a unique name"
|
||||
names.each do |name|
|
||||
puts
|
||||
puts "'#{name}' is used multiple times:"
|
||||
User.where(['LOWER(username) = ?', name]).each do |u|
|
||||
puts "#{u.id}\t#{u.email}"
|
||||
end
|
||||
end
|
||||
puts
|
||||
puts
|
||||
end
|
||||
end
|
||||
end
|
21
lib/utils.rb
21
lib/utils.rb
|
@ -130,4 +130,25 @@ module Utils
|
|||
def self.sort_tuples!(array, orders = [])
|
||||
TupleSorter.sort!(array, orders)
|
||||
end
|
||||
|
||||
def self.parse_duration(string)
|
||||
return nil if string.blank?
|
||||
case string.strip
|
||||
when /\A(\d+)\.(\w+)\z/
|
||||
$1.to_i.send($2.to_s)
|
||||
when /\A(\d+)\z/
|
||||
$1.to_i
|
||||
else
|
||||
STDERR.puts "WARNING: Invalid duration format: '#{string.strip}'"
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def self.if_present(string, method)
|
||||
if string.present?
|
||||
string.send(method)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,3 +11,4 @@ WUNDERLIST_OAUTH_KEY=wunderoauthkey
|
|||
EVERNOTE_OAUTH_KEY=evernoteoauthkey
|
||||
EVERNOTE_OAUTH_SECRET=evernoteoauthsecret
|
||||
FAILED_JOBS_TO_KEEP=2
|
||||
REQUIRE_CONFIRMED_EMAIL=false
|
||||
|
|
107
spec/features/admin_users_spec.rb
Normal file
107
spec/features/admin_users_spec.rb
Normal file
|
@ -0,0 +1,107 @@
|
|||
require 'capybara_helper'
|
||||
|
||||
describe Admin::UsersController do
|
||||
it "requires to be signed in as an admin" do
|
||||
login_as(users(:bob))
|
||||
visit admin_users_path
|
||||
expect(page).to have_text('Admin access required to view that page.')
|
||||
end
|
||||
|
||||
context "as an admin" do
|
||||
before :each do
|
||||
login_as(users(:jane))
|
||||
end
|
||||
|
||||
it "lists all users" do
|
||||
visit admin_users_path
|
||||
expect(page).to have_text('bob')
|
||||
expect(page).to have_text('jane')
|
||||
end
|
||||
|
||||
it "allows to delete a user" do
|
||||
visit admin_users_path
|
||||
find(:css, "a[href='/admin/users/#{users(:bob).id}']").click
|
||||
expect(page).to have_text("User 'bob' was deleted.")
|
||||
expect(page).not_to have_text('bob@example.com')
|
||||
end
|
||||
|
||||
context "creating new users" do
|
||||
it "follow the 'new user' link" do
|
||||
visit admin_users_path
|
||||
click_on('New User')
|
||||
expect(page).to have_text('Create new User')
|
||||
end
|
||||
|
||||
it "creates a new user" do
|
||||
visit new_admin_user_path
|
||||
fill_in 'Email', with: 'test@test.com'
|
||||
fill_in 'Username', with: 'usertest'
|
||||
fill_in 'Password', with: '12345678'
|
||||
fill_in 'Password confirmation', with: '12345678'
|
||||
click_on 'Create User'
|
||||
expect(page).to have_text("User 'usertest' was successfully created.")
|
||||
expect(page).to have_text('test@test.com')
|
||||
end
|
||||
|
||||
it "requires the passwords to match" do
|
||||
visit new_admin_user_path
|
||||
fill_in 'Email', with: 'test@test.com'
|
||||
fill_in 'Username', with: 'usertest'
|
||||
fill_in 'Password', with: '12345678'
|
||||
fill_in 'Password confirmation', with: 'no_match'
|
||||
click_on 'Create User'
|
||||
expect(page).to have_text("Password confirmation doesn't match")
|
||||
end
|
||||
end
|
||||
|
||||
context "updating existing users" do
|
||||
it "follows the edit link" do
|
||||
visit admin_users_path
|
||||
click_on('bob')
|
||||
expect(page).to have_text('Edit User')
|
||||
end
|
||||
|
||||
it "updates an existing user" do
|
||||
visit edit_admin_user_path(users(:bob))
|
||||
check 'Admin'
|
||||
click_on 'Update User'
|
||||
expect(page).to have_text("User 'bob' was successfully updated.")
|
||||
visit edit_admin_user_path(users(:bob))
|
||||
expect(page).to have_checked_field('Admin')
|
||||
end
|
||||
|
||||
it "requires the passwords to match when changing them" do
|
||||
visit edit_admin_user_path(users(:bob))
|
||||
fill_in 'Password', with: '12345678'
|
||||
fill_in 'Password confirmation', with: 'no_match'
|
||||
click_on 'Update User'
|
||||
expect(page).to have_text("Password confirmation doesn't match")
|
||||
end
|
||||
end
|
||||
|
||||
context "(de)activating users" do
|
||||
it "does not show deactivation buttons for the current user" do
|
||||
visit admin_users_path
|
||||
expect(page).not_to have_css("a[href='/admin/users/#{users(:jane).id}/deactivate']")
|
||||
end
|
||||
|
||||
it "deactivates an existing user" do
|
||||
visit admin_users_path
|
||||
expect(page).not_to have_text('inactive')
|
||||
find(:css, "a[href='/admin/users/#{users(:bob).id}/deactivate']").click
|
||||
expect(page).to have_text('inactive')
|
||||
users(:bob).reload
|
||||
expect(users(:bob)).not_to be_active
|
||||
end
|
||||
|
||||
it "deactivates an existing user" do
|
||||
users(:bob).deactivate!
|
||||
visit admin_users_path
|
||||
find(:css, "a[href='/admin/users/#{users(:bob).id}/activate']").click
|
||||
expect(page).not_to have_text('inactive')
|
||||
users(:bob).reload
|
||||
expect(users(:bob)).to be_active
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -172,4 +172,33 @@ describe Utils do
|
|||
expect(tuples).to eq expected
|
||||
end
|
||||
end
|
||||
|
||||
context "#parse_duration" do
|
||||
it "works with correct arguments" do
|
||||
expect(Utils.parse_duration('2.days')).to eq(2.days)
|
||||
expect(Utils.parse_duration('2.seconds')).to eq(2)
|
||||
expect(Utils.parse_duration('2')).to eq(2)
|
||||
end
|
||||
|
||||
it "returns nil when passed nil" do
|
||||
expect(Utils.parse_duration(nil)).to be_nil
|
||||
end
|
||||
|
||||
it "warns and returns nil when not parseable" do
|
||||
mock(STDERR).puts("WARNING: Invalid duration format: 'bogus'")
|
||||
expect(Utils.parse_duration('bogus')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "#if_present" do
|
||||
it "returns nil when passed nil" do
|
||||
expect(Utils.if_present(nil, :to_i)).to be_nil
|
||||
end
|
||||
|
||||
it "calls the specified method when the argument is present" do
|
||||
argument = mock()
|
||||
mock(argument).to_i { 1 }
|
||||
expect(Utils.if_present(argument, :to_i)).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,34 @@ require 'rails_helper'
|
|||
describe Agent do
|
||||
it_behaves_like WorkingHelpers
|
||||
|
||||
describe '.active/inactive' do
|
||||
let(:agent) { agents(:jane_website_agent) }
|
||||
|
||||
it 'is active per default' do
|
||||
expect(Agent.active).to include(agent)
|
||||
expect(Agent.inactive).not_to include(agent)
|
||||
end
|
||||
|
||||
it 'is not active when disabled' do
|
||||
agent.update_attribute(:disabled, true)
|
||||
expect(Agent.active).not_to include(agent)
|
||||
expect(Agent.inactive).to include(agent)
|
||||
end
|
||||
|
||||
it 'is not active when deactivated' do
|
||||
agent.update_attribute(:deactivated, true)
|
||||
expect(Agent.active).not_to include(agent)
|
||||
expect(Agent.inactive).to include(agent)
|
||||
end
|
||||
|
||||
it 'is not active when disabled and deactivated' do
|
||||
agent.update_attribute(:disabled, true)
|
||||
agent.update_attribute(:deactivated, true)
|
||||
expect(Agent.active).not_to include(agent)
|
||||
expect(Agent.inactive).to include(agent)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".bulk_check" do
|
||||
before do
|
||||
@weather_agent_count = Agents::WeatherAgent.where(:schedule => "midnight", :disabled => false).count
|
||||
|
@ -18,6 +46,12 @@ describe Agent do
|
|||
mock(Agents::WeatherAgent).async_check(anything).times(@weather_agent_count - 1)
|
||||
Agents::WeatherAgent.bulk_check("midnight")
|
||||
end
|
||||
|
||||
it "should skip agents of deactivated accounts" do
|
||||
agents(:bob_weather_agent).user.deactivate!
|
||||
mock(Agents::WeatherAgent).async_check(anything).times(@weather_agent_count - 1)
|
||||
Agents::WeatherAgent.bulk_check("midnight")
|
||||
end
|
||||
end
|
||||
|
||||
describe ".run_schedule" do
|
||||
|
@ -335,6 +369,13 @@ describe Agent do
|
|||
Agent.receive! # and we receive it
|
||||
}.to change { agents(:bob_rain_notifier_agent).reload.last_checked_event_id }
|
||||
end
|
||||
|
||||
it "should not run agents of deactivated accounts" do
|
||||
agents(:bob_weather_agent).user.deactivate!
|
||||
Agent.async_check(agents(:bob_weather_agent).id)
|
||||
mock(Agent).async_receive(agents(:bob_rain_notifier_agent).id, anything).times(0)
|
||||
Agent.receive!
|
||||
end
|
||||
end
|
||||
|
||||
describe ".async_receive" do
|
||||
|
|
|
@ -19,6 +19,12 @@ describe User do
|
|||
should_not allow_value(v).for(:invitation_code)
|
||||
end
|
||||
end
|
||||
|
||||
it "requires no authentication code when requires_no_invitation_code! is called" do
|
||||
u = User.new(username: 'test', email: 'test@test.com', password: '12345678', password_confirmation: '12345678')
|
||||
u.requires_no_invitation_code!
|
||||
expect(u).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context "when configured not to use invitation codes" do
|
||||
|
@ -34,4 +40,28 @@ describe User do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context '#deactivate!' do
|
||||
it "deactivates the user and all her agents" do
|
||||
agent = agents(:jane_website_agent)
|
||||
users(:jane).deactivate!
|
||||
agent.reload
|
||||
expect(agent.deactivated).to be_truthy
|
||||
expect(users(:jane).deactivated_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context '#activate!' do
|
||||
before do
|
||||
users(:bob).deactivate!
|
||||
end
|
||||
|
||||
it 'activates the user and all his agents' do
|
||||
agent = agents(:bob_website_agent)
|
||||
users(:bob).activate!
|
||||
agent.reload
|
||||
expect(agent.deactivated).to be_falsy
|
||||
expect(users(:bob).deactivated_at).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue