mirror of
https://github.com/Fishwaldo/huginn.git
synced 2025-03-15 19:31:26 +00:00
Add admin user management interface
This commit is contained in:
parent
a025923c3c
commit
4722ebfe4e
13 changed files with 323 additions and 30 deletions
76
app/controllers/admin/users_controller.rb
Normal file
76
app/controllers/admin/users_controller.rb
Normal 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
|
|
@ -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
|
||||
|
|
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>
|
48
app/views/admin/users/index.html.erb
Normal file
48
app/views/admin/users/index.html.erb
Normal 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>
|
||||
|
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" %>
|
||||
|
|
|
@ -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]
|
||||
|
|
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
|
82
spec/features/admin_users_spec.rb
Normal file
82
spec/features/admin_users_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue