Add admin user management interface

This commit is contained in:
Dominik Sander 2016-03-01 12:33:21 +01:00
parent a025923c3c
commit 4722ebfe4e
13 changed files with 323 additions and 30 deletions

View file

@ -0,0 +1,76 @@
class Admin::UsersController < ApplicationController
before_action :authenticate_admin!
before_action :find_user, only: [:edit, :destroy, :update]
helper_method :resource
def index
@users = User.reorder(:created_at).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
private
def find_user
@user = User.find(params[:id])
end
def resource
@user
end
end

View file

@ -16,9 +16,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
@ -44,4 +44,12 @@ class User < ActiveRecord::Base
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

View 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>

View 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>

View file

@ -0,0 +1,48 @@
<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>Inactive 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>state</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">
<%= link_to 'Delete', admin_user_path(user), method: :delete, data: { confirm: 'Are you sure? This can not be undone.' }, class: "btn btn-default" %>
</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>

View 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>

View file

@ -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>

View file

@ -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">

View file

@ -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" %>

View file

@ -66,6 +66,10 @@ Huginn::Application.routes.draw do
end
end
namespace :admin do
resources :users, except: :show
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]

View 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

View file

@ -0,0 +1,82 @@
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
end
end

View file

@ -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