diff --git a/Rakefile b/Rakefile
index 6a8c49c..d449c5b 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,6 +1,7 @@
require 'rubygems'
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
+require 'proxes/rake_tasks'
RSpec::Core::RakeTask.new(:spec)
diff --git a/Vagrantfile b/Vagrantfile
index a37c343..fd3cb55 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -2,7 +2,7 @@
# vi: set ft=ruby :
Vagrant.configure(2) do |config|
- config.vm.box = "ubuntu/trusty64"
+ config.vm.box = "ubuntu/xenial64"
config.vm.network :private_network, ip: '172.16.248.110'
@@ -16,7 +16,7 @@ Vagrant.configure(2) do |config|
sudo apt-get install -y screen curl git build-essential libssl-dev
# Ruby
- sudo apt-get install ruby2.0
+ sudo apt-get install -y ruby2.3 ruby2.3-dev
# if [ ! -f /home/vagrant/.rvm/scripts/rvm ]
# then
# gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
@@ -27,7 +27,7 @@ Vagrant.configure(2) do |config|
# Ruby and it's Gems
cd /vagrant
# rvm use $(cat .ruby-version) --install
- gem install bundler --no-rdoc --no-ri
+ sudo gem install bundler --no-rdoc --no-ri
bundle install
# Node
diff --git a/config.ru b/config.ru
index 38cee2f..29c48e7 100644
--- a/config.ru
+++ b/config.ru
@@ -2,48 +2,61 @@
libdir = File.expand_path(File.dirname(__FILE__) + '/lib')
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
+raise 'Unconfigured' unless ENV['ELASTICSEARCH_URL']
+
require 'proxes'
require 'proxes/db'
-raise 'Unconfigured' unless ENV['ELASTICSEARCH_URL']
-
use Rack::Static, urls: ['/assets'], root: 'public'
-
-use Rack::Session::Pool
-# use Rack::Session::Cookie,
-# :key => '_ProxES_session',
-# #:secure=>!TEST_MODE, # Uncomment if only allowing https:// access
-# :secret=>File.read('.session_secret')
+use Rack::MethodOverride
+use Rack::Session::Cookie,
+ :key => '_ProxES_session',
+ #:secure=>!TEST_MODE, # Uncomment if only allowing https:// access
+ :secret=>File.read('.session_secret')
require 'omniauth'
require 'omniauth-identity'
+require 'proxes/controllers/auth_identity'
# OmniAuth.config.test_mode = true
-
use OmniAuth::Builder do
# The identity provider is used by the App.
provider :identity,
fields: [:username],
+ callback_path: '/_proxes/auth/identity/callback',
model: ProxES::Identity,
- on_login: ProxES::Security,
- on_registration: ProxES::Security,
+ on_login: ProxES::AuthIdentity,
+ on_registration: ProxES::AuthIdentity,
locate_conditions: lambda{|req| {username: req['username']} }
end
-
-OmniAuth.config.on_failure = Proc.new { |env|
- OmniAuth::FailureEndpoint.new(env).redirect_to_failure
-}
+OmniAuth.config.on_failure = ProxES::AuthIdentity
require 'warden'
require 'proxes/strategies/jwt_token'
use Warden::Manager do |manager|
manager.default_strategies :jwt_token
manager.scope_defaults :default, action: '_proxes/unauthenticated'
- manager.failure_app = ProxES::Security
+ manager.failure_app = ProxES::App
end
-
Warden::Manager.serialize_into_session { |user| user.id }
Warden::Manager.serialize_from_session { |id| ProxES::User[id] }
+# Management App
+require 'proxes/controllers'
+
+map '/_proxes' do
+ {
+ '/users' => ProxES::Users,
+ '/user-roles' => ProxES::UserRoles,
+ }.each do |route, app|
+ map route do
+ run app
+ end
+ end
+
+ run ProxES::App
+end
+
+
# Proxy all Elasticsearch requests
map '/' do
# Security
@@ -52,8 +65,3 @@ map '/' do
# Forward requests to ES
run Rack::Proxy.new(backend: ENV['ELASTICSEARCH_URL'])
end
-
-# Management App
-map '/_proxes' do
- run ProxES::App
-end
diff --git a/lib/proxes.rb b/lib/proxes.rb
index d2cd10e..53b9ab7 100644
--- a/lib/proxes.rb
+++ b/lib/proxes.rb
@@ -1,5 +1,3 @@
require 'proxes/version'
-require 'proxes/base'
require 'proxes/app'
require 'proxes/security'
-require 'proxes/es_request'
diff --git a/lib/proxes/app.rb b/lib/proxes/app.rb
index 575cc6a..a460cac 100644
--- a/lib/proxes/app.rb
+++ b/lib/proxes/app.rb
@@ -1,28 +1,49 @@
-require 'proxes/base'
-require 'proxes/routes'
+require 'proxes/controllers/application'
module ProxES
# Manage your Elasticsearch cluster, user and user sessions
- class App < ProxES::Base
- plugin :multi_route
+ class App < Application
+ get '/' do
+ authenticate!
+ haml :index
+ end
- def logger
- require 'logger'
- @logger ||= Logger.new($stdout)
+ ['/unauthenticated', '/_proxes/unauthenticated'].each do |path|
+ get path do
+ redirect '/auth/identity'
+ end
end
- def root_url
- @root_url = opts[:root_url] || '/_proxes'
+ post '/auth/identity/new' do
+ identity = Identity.new(params['identity'])
+ if identity.valid? && identity.save
+ flash[:info] = 'Successfully Registered. Please log in'
+ redirect '/auth/identity'
+ else
+ flash.now[:warning] = 'Could not complete the registration. Please try again.'
+ view 'security/register', locals: { identity: identity }
+ end
end
- route do |r|
- r.multi_route
+ post '/auth/identity/callback' do
+ user = User.find_or_create(email: env['omniauth.auth']['info']['email'])
+ user.add_user_role role: 'user' unless user.has_role? 'user'
+ user.add_user_role(role: 'super_admin') if (user.id == 1 && user.has_role?('super_admin') == false)
- r.get do
- authenticate!
+ identity = Identity.find(username: user.email)
+ user.add_identity identity unless identity.user == user
- view 'index'
- end
+ set_user user
+ flash[:success] = 'Logged In'
+ redirect '/_proxes'
+ end
+
+ delete '/auth/identity' do
+ logout
+
+ flash[:info] = 'Logged Out'
+
+ redirect '/_proxes'
end
end
end
diff --git a/lib/proxes/base.rb b/lib/proxes/base.rb
deleted file mode 100644
index 2ea3cc2..0000000
--- a/lib/proxes/base.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-require 'tilt/haml'
-require 'roda'
-require 'rack'
-
-module ProxES
- # Base Roda App
- class Base < Roda
- opts[:root] ||= File.expand_path(File.dirname(__FILE__) + '/../../')
-
- plugin :middleware
-
- use Rack::MethodOverride
- plugin :all_verbs
- plugin :empty_root
-
- plugin :default_headers,
- 'Content-Type'=>'text/html',
- # 'Content-Security-Policy'=>"default-src 'self' https://oss.maxcdn.com/ https://maxcdn.bootstrapcdn.com https://ajax.googleapis.com",
- #'Strict-Transport-Security'=>'max-age=16070400;', # Uncomment if only allowing https:// access
- 'X-Frame-Options'=>'deny',
- 'X-Content-Type-Options'=>'nosniff',
- 'X-XSS-Protection'=>'1; mode=block'
-
- plugin :render, engine: 'haml', views: opts[:views]
- plugin :partials
- plugin :csrf, raise: true, skip_if: lambda { |r| !(r.path =~ %r{/_proxes/.*}) }
- plugin :indifferent_params
- plugin :flash
- plugin :halt
-
- plugin(:not_found) { view 'http_404' }
- plugin(:error_handler) do |e|
- case true
- when e.is_a?(Roda::RodaPlugins::Authentication::NotAuthenticated) || e.is_a?(OmniAuth::Error)
- request.redirect '/auth/identity'
- when e.is_a?(Pundit::NotAuthorizedError)
- request.halt 404
- else
- logger.error e
- raise e unless ENV['RACK_ENV'] == 'production'
- view 'error', locals: { error: e }
- end
- end
-
- plugin :authentication
- plugin :pundit
-
- def logger
- require 'logger'
- @logger ||= Logger.new($stdout)
- end
-
- def root_url
- @root_url = opts[:root_url] || '/_proxes'
- end
- end
-end
diff --git a/lib/proxes/controllers.rb b/lib/proxes/controllers.rb
new file mode 100644
index 0000000..a3802a4
--- /dev/null
+++ b/lib/proxes/controllers.rb
@@ -0,0 +1,2 @@
+require 'proxes/controllers/users'
+require 'proxes/controllers/user_roles'
diff --git a/lib/proxes/controllers/application.rb b/lib/proxes/controllers/application.rb
new file mode 100644
index 0000000..25c80ed
--- /dev/null
+++ b/lib/proxes/controllers/application.rb
@@ -0,0 +1,39 @@
+require 'sinatra/base'
+require 'sinatra/flash'
+require 'proxes/helpers/views'
+require 'proxes/helpers/pundit'
+require 'proxes/helpers/authentication'
+
+module ProxES
+ class Application < Sinatra::Base
+ set :root, ::File.expand_path(::File.dirname(__FILE__) + '/../../../')
+ register Sinatra::Flash
+ helpers ProxES::Helpers::Pundit, ProxES::Helpers::Views, ProxES::Helpers::Authentication
+
+ configure :production do
+ disable :show_exceptions
+ end
+
+ configure :development do
+ set :show_exceptions, :after_handler
+ end
+
+ configure :production, :development do
+ enable :logging
+ end
+
+ not_found do
+ haml :'404', locals: { title: '4 oh 4' }
+ end
+
+ error do
+ error = env['sinatra.error']
+ haml :error, locals: { title: 'Something went wrong', message: error }
+ end
+
+ error ::Pundit::NotAuthorizedError do
+ flash[:warning] = 'Please log in first.'
+ redirect '/auth/identity'
+ end
+ end
+end
diff --git a/lib/proxes/controllers/auth_identity.rb b/lib/proxes/controllers/auth_identity.rb
new file mode 100644
index 0000000..d3cab56
--- /dev/null
+++ b/lib/proxes/controllers/auth_identity.rb
@@ -0,0 +1,20 @@
+require 'proxes/controllers/application'
+
+module ProxES
+ class AuthIdentity < Application
+ get '/auth/identity' do
+ haml :'identity/login', locals: { title: 'Log In' }
+ end
+
+ # Failed Login
+ post '/_proxes/auth/identity/callback' do
+ flash[:warning] = 'Invalid credentials. Please try again.'
+ redirect '/auth/identity'
+ end
+
+ get '/auth/identity/register' do
+ identity = Identity.new
+ haml :'identity/register', locals: { title: 'Register', identity: identity }
+ end
+ end
+end
diff --git a/lib/proxes/controllers/component.rb b/lib/proxes/controllers/component.rb
new file mode 100644
index 0000000..58a6288
--- /dev/null
+++ b/lib/proxes/controllers/component.rb
@@ -0,0 +1,88 @@
+require 'proxes/controllers/application'
+require 'proxes/helpers/component'
+
+module ProxES
+ class Component < Application
+ helpers ProxES::Helpers::Component
+ set base_path: nil
+ set view_location: nil
+
+ # List
+ get '/' do
+ authorize settings.model_class, :list
+
+ actions = {}
+ actions["#{base_path}/new"] = "New #{heading}" if policy(settings.model_class).create?
+
+ haml :"#{view_location}/index", locals: { list: list, title: heading(:list), actions: actions }
+ end
+
+ # Create Form
+ get '/new' do
+ authorize settings.model_class, :create
+
+ entity = settings.model_class.new(permitted_attributes(settings.model_class, :create))
+ haml :"#{view_location}/new", locals: { entity: entity, title: heading(:new) }
+ end
+
+ # Create
+ post '/' do
+ authorize settings.model_class, :create
+
+ entity = settings.model_class.new(permitted_attributes(settings.model_class, :create))
+ if entity.valid? && entity.save
+ flash[:success] = "#{heading} Created"
+ redirect "#{base_path}/#{entity.id}"
+ else
+ haml :"#{view_location}/new", locals: { entity: entity, title: heading(:new) }
+ end
+ end
+
+ # Read
+ get '/:id' do |id|
+ entity = dataset[id.to_i]
+ halt 404 unless entity
+ authorize entity, :read
+
+ actions = {}
+ actions["#{base_path}/#{entity.id}/edit"] = "Edit #{heading}" if policy(entity).update?
+
+ haml :"#{view_location}/display", locals: { entity: entity, title: heading, actions: actions }
+ end
+
+ # Update Form
+ get '/:id/edit' do |id|
+ entity = dataset.find(id: id.to_i)
+ halt 404 unless entity
+ authorize entity, :update
+
+ haml :"#{view_location}/edit", locals: { entity: entity, title: heading(:edit) }
+ end
+
+ # Update
+ put '/:id' do |id|
+ entity = dataset.find(id: id.to_i)
+ halt 404 unless entity
+ authorize entity, :update
+
+ entity.set(permitted_attributes(settings.model_class, :create))
+ if entity.valid? && entity.save
+ flash[:success] = "#{heading} Updated"
+ redirect "#{base_path}/#{entity.id}"
+ else
+ haml :"#{view_location}/edit", locals: { entity: entity, title: heading(:edit) }
+ end
+ end
+
+ delete '/:id' do |id|
+ entity = dataset.find(id: id.to_i)
+ halt 404 unless entity
+ authorize entity, :delete
+
+ entity.destroy
+
+ flash[:success] = "#{heading} Deleted"
+ redirect "#{base_path}"
+ end
+ end
+end
diff --git a/lib/proxes/controllers/user_roles.rb b/lib/proxes/controllers/user_roles.rb
new file mode 100644
index 0000000..21e5803
--- /dev/null
+++ b/lib/proxes/controllers/user_roles.rb
@@ -0,0 +1,10 @@
+require 'proxes/controllers/component'
+require 'proxes/models/user_role'
+
+module ProxES
+ class UserRoles < Component
+ set model_class: ProxES::UserRole
+ set view_location: 'user_roles'
+ set base_path: '/user-roles'
+ end
+end
diff --git a/lib/proxes/controllers/users.rb b/lib/proxes/controllers/users.rb
new file mode 100644
index 0000000..195ad89
--- /dev/null
+++ b/lib/proxes/controllers/users.rb
@@ -0,0 +1,90 @@
+require 'proxes/controllers/component'
+require 'proxes/models/user'
+
+module ProxES
+ class Users < Component
+ set model_class: ProxES::User
+
+ # New
+ get '/new' do
+ authorize settings.model_class, :create
+
+ locals = {
+ title: heading(:new),
+ entity: User.new,
+ identity: Identity.new
+ }
+ haml :"#{view_location}/new", locals: locals, layout_opts: { locals: locals }
+ end
+
+ # Create
+ post '/' do
+ authorize settings.model_class, :create
+
+ locals = { title: heading(:new) }
+
+ user_params = permitted_attributes(User, :create)
+ identity_params = permitted_attributes(Identity, :create)
+ user_params['email'] = identity_params['username']
+ roles = user_params.delete('user_roles')
+ user = locals[:user] = User.new(user_params)
+ identity = locals[:identity] = Identity.new(identity_params)
+ if identity.valid? && user.valid?
+ DB.transaction(:isolation => :serializable) do
+ identity.save
+ user.save
+ user.add_identity identity
+
+ if roles
+ user.remove_all_user_roles
+ roles.each { |role| user.add_user_role(role: role) }
+ end
+ end
+
+ flash[:success] = 'User created'
+ redirect "/_proxes/users/#{user.id}"
+ else
+ flash.now[:danger] = 'Could not create the user'
+ locals[:entity] = user
+ locals[:identity] = identity
+ haml :"#{view_location}/new", locals: locals
+ end
+ end
+
+ # Update
+ put '/:id' do |id|
+ entity = dataset.find(id: id.to_i)
+ halt 404 unless entity
+ authorize entity, :update
+
+ values = permitted_attributes(settings.model_class, :update)
+ roles = values.delete('user_roles')
+ entity.set values
+ if entity.valid? && entity.save
+ if roles
+ entity.remove_all_user_roles
+ roles.each { |role| entity.add_user_role(role: role) }
+ end
+
+ flash[:success] = "#{heading} Updated"
+ redirect "/_proxes/users/#{entity.id}"
+ else
+ haml :"#{view_location}/edit", locals: { entity: entity, title: heading(:edit) }
+ end
+ end
+
+ # Delete
+ delete '/:id' do |id|
+ entity = dataset.find(id: id.to_i)
+ halt 404 unless entity
+ authorize entity, :delete
+
+ entity.remove_all_identity
+ entity.remove_all_user_roles
+ entity.destroy
+
+ flash[:success] = "#{heading} Deleted"
+ redirect "/_proxes/users"
+ end
+ end
+end
diff --git a/lib/proxes/db.rb b/lib/proxes/db.rb
index 12dc0af..a70f5d7 100644
--- a/lib/proxes/db.rb
+++ b/lib/proxes/db.rb
@@ -7,6 +7,8 @@
DB.loggers << Logger.new($stdout)
+DB.extension(:pagination)
+
Sequel::Model.plugin :auto_validations
Sequel::Model.plugin :prepared_statements
Sequel::Model.plugin :prepared_statements_associations
diff --git a/lib/proxes/helpers/authentication.rb b/lib/proxes/helpers/authentication.rb
new file mode 100644
index 0000000..7c0c3a4
--- /dev/null
+++ b/lib/proxes/helpers/authentication.rb
@@ -0,0 +1,31 @@
+module ProxES::Helpers
+ module Authentication
+ def current_user
+ env['warden'] ? env['warden'].user : nil
+ end
+
+ def authenticate
+ env['warden'] && env['warden'].authenticate
+ end
+
+ def authenticated?
+ env['warden'] && env['warden'].authenticated?
+ end
+
+ def authenticate!
+ raise NotAuthenticated unless env['warden']
+ env['warden'].authenticate!
+ end
+
+ def logout
+ env['warden'] && env['warden'].logout
+ end
+
+ def set_user(user)
+ env['warden'].set_user(user)
+ end
+ end
+
+ class NotAuthenticated < StandardError
+ end
+end
diff --git a/lib/proxes/helpers/component.rb b/lib/proxes/helpers/component.rb
new file mode 100644
index 0000000..009d1bb
--- /dev/null
+++ b/lib/proxes/helpers/component.rb
@@ -0,0 +1,39 @@
+require 'active_support'
+require 'active_support/inflector'
+
+module ProxES::Helpers
+ module Component
+ def dataset
+ policy_scope(settings.model_class)
+ end
+
+ def list
+ params['count'] = params['count'] ? params['count'].to_i : 10
+ params['page'] = params['page'] ? params['page'].to_i : 1
+
+ dataset.select.paginate(params['page'], params['count'])
+ end
+
+ def heading(action = nil)
+ heading = ActiveSupport::Inflector.demodulize settings.model_class
+ case action
+ when :list
+ ActiveSupport::Inflector.pluralize heading
+ when :new
+ "New #{heading}"
+ when :edit
+ "Edit #{heading}"
+ else
+ heading
+ end
+ end
+
+ def base_path
+ settings.base_path || "/#{heading(:list).downcase}"
+ end
+
+ def view_location
+ settings.view_location || "#{heading(:list).downcase}"
+ end
+ end
+end
diff --git a/lib/proxes/helpers/pundit.rb b/lib/proxes/helpers/pundit.rb
new file mode 100644
index 0000000..923bc08
--- /dev/null
+++ b/lib/proxes/helpers/pundit.rb
@@ -0,0 +1,42 @@
+require 'pundit'
+require 'proxes/es_request'
+
+module ProxES
+ module Helpers
+ module Pundit
+ include ::Pundit
+
+ def authorize(record, query = nil)
+ if record.is_a?(::ProxES::ESRequest)
+ if record.action.nil? && record.index
+ query = '_index?'
+ else
+ query = record.action ? record.action.to_s + '?' : '_root?'
+ end
+ else
+ raise ArgumentError, 'Pundit cannot determine the query' if query.nil?
+ end
+ query = :"#{query}?" unless query[-1] == '?'
+ super
+ end
+
+ def permitted_attributes(record, action)
+ param_key = PolicyFinder.new(record).param_key
+ policy = policy(record)
+ method_name = if policy.respond_to?("permitted_attributes_for_#{action}")
+ "permitted_attributes_for_#{action}"
+ else
+ 'permitted_attributes'
+ end
+
+ request.params.fetch(param_key, {}).select do |key, value|
+ policy.public_send(method_name).include? key.to_sym
+ end
+ end
+
+ def pundit_user
+ current_user
+ end
+ end
+ end
+end
diff --git a/lib/proxes/helpers/views.rb b/lib/proxes/helpers/views.rb
new file mode 100644
index 0000000..280c66f
--- /dev/null
+++ b/lib/proxes/helpers/views.rb
@@ -0,0 +1,37 @@
+module ProxES::Helpers
+ module Views
+ def form_control(name, model, opts = {})
+ id = opts.delete(:id) || name
+ type = opts.delete(:type) || 'text'
+ label = opts.delete(:label) || name.to_s.titlecase
+ klass = opts.delete(:class) || 'form-control' unless type == 'file'
+ group = opts.delete(:group) || model.class.name.split('::').last.downcase
+
+ attributes = opts.merge(id: id, name: "#{group}[#{name}]", type: type, class: klass)
+ locals = { model: model, label: label, attributes: attributes, name: name, group: group }
+ haml :'partials/form_control', locals: locals
+ end
+
+ def flash_messages(key = :flash)
+ return "" if flash(key).empty?
+ id = (key == :flash ? "flash" : "flash_#{key}")
+ messages = flash(key).collect {|message| "
#{message[1]}
\n"}
+ "\n" + messages.join + "
"
+ end
+
+ def delete_form(entity, label = 'Delete')
+ locals = { delete_label: label, entity: entity }
+ haml :'partials/delete_form', locals: locals
+ end
+
+ def pagination(list, base_path, count: params[:count])
+ locals = {
+ next_link: list.last_page? ? '#' : "#{base_path}?page=#{list.next_page}&count=#{list.page_size}",
+ prev_link: list.first_page? ? '#' : "#{base_path}?page=#{list.prev_page}&count=#{list.page_size}",
+ base_path: base_path,
+ list: list,
+ }
+ haml :'partials/pager', locals: locals
+ end
+ end
+end
diff --git a/lib/proxes/models/user.rb b/lib/proxes/models/user.rb
index ddb848a..56f9435 100644
--- a/lib/proxes/models/user.rb
+++ b/lib/proxes/models/user.rb
@@ -8,9 +8,18 @@
module ProxES
class User < Sequel::Model
one_to_many :identity
+ one_to_many :user_roles
def has_role?(check)
- check.to_sym == role.to_sym
+ user_roles.map(&:role).map(&:to_sym).include? check.to_sym
+ end
+
+ def admin?
+ (user_roles.map(&:role) & ['admin', 'super_admin']).any?
+ end
+
+ def admin?
+ has_role?(:admin) || has_role?(:super_admin)
end
def method_missing(method_sym, *arguments, &block)
@@ -30,9 +39,6 @@ def validate
validates_presence :email
validates_unique :email unless email.blank?
validates_format /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :email unless email.blank?
-
- validates_presence :role
- validates_includes ['super_admin', 'admin', 'owner', 'user'], :role unless role.blank?
end
def index_prefix
diff --git a/lib/proxes/models/user_role.rb b/lib/proxes/models/user_role.rb
new file mode 100644
index 0000000..564669d
--- /dev/null
+++ b/lib/proxes/models/user_role.rb
@@ -0,0 +1,19 @@
+require 'sequel'
+
+module ProxES
+ class UserRole < Sequel::Model
+ many_to_one :user
+
+ subset(:admins, role: ['admin', 'super_admin'])
+ subset(:authorisers, role: 'authoriser')
+
+ def validate
+ validates_presence :user_id
+ validates_includes self.class.role_names, :role
+ end
+
+ def self.role_names
+ ['super_admin', 'admin', 'user']
+ end
+ end
+end
diff --git a/lib/proxes/policies/application_policy.rb b/lib/proxes/policies/application_policy.rb
index 6da7673..06a281c 100644
--- a/lib/proxes/policies/application_policy.rb
+++ b/lib/proxes/policies/application_policy.rb
@@ -1,17 +1,19 @@
-class ApplicationPolicy
- attr_reader :user, :record
+module ProxES
+ class ApplicationPolicy
+ attr_reader :user, :record
- def initialize(user, record)
- @user = user
- @record = record
- end
+ def initialize(user, record)
+ @user = user
+ @record = record
+ end
- class Scope
- attr_reader :user, :scope
+ class Scope
+ attr_reader :user, :scope
- def initialize(user, scope)
- @user = user
- @scope = scope
+ def initialize(user, scope)
+ @user = user
+ @scope = scope
+ end
end
end
end
diff --git a/lib/proxes/policies/token_policy.rb b/lib/proxes/policies/token_policy.rb
new file mode 100644
index 0000000..58d1da0
--- /dev/null
+++ b/lib/proxes/policies/token_policy.rb
@@ -0,0 +1,45 @@
+require_relative 'application_policy'
+
+module ProxES
+ class TokenPolicy < ApplicationPolicy
+ def create?
+ user.admin?
+ end
+
+ def list?
+ create?
+ end
+
+ def read?
+ record.id == user.id || user.admin?
+ end
+
+ def update?
+ read?
+ end
+
+ def delete?
+ create?
+ end
+
+ def register?
+ true
+ end
+
+ def permitted_attributes
+ attribs = [:email, :name, :surname]
+ attribs << :role if user.admin?
+ attribs
+ end
+
+ class Scope < ApplicationPolicy::Scope
+ def resolve
+ if user.admin?
+ scope.all
+ else
+ []
+ end
+ end
+ end
+ end
+end
diff --git a/lib/proxes/policies/user_policy.rb b/lib/proxes/policies/user_policy.rb
index 39e1f8f..f03fe6e 100644
--- a/lib/proxes/policies/user_policy.rb
+++ b/lib/proxes/policies/user_policy.rb
@@ -1,21 +1,22 @@
-require_relative 'application_policy'
+require 'proxes/policies/application_policy'
+
module ProxES
class UserPolicy < ApplicationPolicy
def create?
- user.admin?
+ user && user.admin?
end
def list?
create?
end
- def get?
- record.id == user.id || user.admin?
+ def read?
+ user && (record.id == user.id || user.admin?)
end
def update?
- get?
+ read?
end
def delete?
@@ -28,16 +29,16 @@ def register?
def permitted_attributes
attribs = [:email, :name, :surname]
- attribs << :role if user.admin?
+ attribs << :user_roles if user.admin?
attribs
end
class Scope < ApplicationPolicy::Scope
def resolve
- if user.admin?
- scope.all
+ if user && user.admin?
+ scope
else
- []
+ scope.where(id: -1)
end
end
end
diff --git a/lib/proxes/security.rb b/lib/proxes/security.rb
index 44fbde7..3924663 100644
--- a/lib/proxes/security.rb
+++ b/lib/proxes/security.rb
@@ -1,108 +1,52 @@
-require 'proxes/base'
+require 'rack-proxy'
require 'proxes/es_request'
require 'proxes/policies/es_policy'
+require 'proxes/helpers/pundit'
+require 'proxes/helpers/authentication'
module ProxES
- # Provide OmniAuth::Identity roots and Pundit checks
- class Security < ProxES::Base
- route do |r|
- # Warden
- r.on '_proxes' do
- r.get 'unauthenticated' do
- r.redirect '/auth/identity'
- end
- end
-
- # Omniauth Identity paths
- r.on 'auth' do
- r.get 'failure' do
- message = case params['message']
- when 'invalid_credentials'
- 'Invalid credentials. Please try again.'
- else
- params['message']
- end
-
- flash[:warning] = message
- r.redirect '/auth/identity'
- end
+ class Security
+ attr_reader :env
- r.on 'identity' do
- identity = Identity.new
+ include ProxES::Helpers::Authentication
+ include ProxES::Helpers::Pundit
- r.delete do
- session.delete('user_id')
- flash[:info] = 'Logged Out'
- redirect '/'
- end
-
- r.get 'register' do
- view 'security/register', locals: { identity: identity }
- end
-
- r.post 'callback' do
- user = User.find_or_create(email: env['omniauth.auth']['info']['email']){|u| u.role = 'user' }
- identity = Identity.find(username: user.email)
- user.add_identity identity unless identity.user
+ def initialize(app)
+ @app = app
+ end
- flash[:success] = 'Logged In'
- env['warden'].set_user(user)
- r.redirect root_url
- end
+ def call(env)
+ @env = env
- r.post 'new' do
- authorize Identity, :register
- identity = Identity.new(permitted_attributes(Identity, :register))
- if identity.valid? && identity.save
- flash[:info] = 'Successfully Registered. Please log in'
- r.redirect '/auth/identity'
- else
- flash.now[:warning] = 'Could not complete the registration. Please try again.'
- view 'security/register', locals: { identity: identity }
- end
- end
+ request = ProxES::ESRequest.new(env)
- r.get do
- view 'security/login'
- end
- end
+ unless ENV['RACK_ENV'] == 'production'
+ puts '================================================================================'
+ puts '= ' + "Request: #{request.fullpath}".ljust(76) + '='
+ puts '= ' + "Endpoint: #{request.endpoint}".ljust(76) + '='
+ puts '= ' + "Index: #{request.index}".ljust(76) + '='
+ puts '= ' + "Type: #{request.type}".ljust(76) + '='
+ puts '= ' + "Action: #{request.action}".ljust(76) + '='
+ puts '================================================================================'
end
- # Everything Else
- r.on proc{true} do
- authenticate!
-
- request = ProxES::ESRequest.new(env)
-
- unless ENV['RACK_ENV'] == 'production'
- puts '================================================================================'
- puts '= ' + "Request: #{request.fullpath}".ljust(76) + '='
- puts '= ' + "Endpoint: #{request.endpoint}".ljust(76) + '='
- puts '= ' + "Index: #{request.index}".ljust(76) + '='
- puts '= ' + "Type: #{request.type}".ljust(76) + '='
- puts '= ' + "Action: #{request.action}".ljust(76) + '='
- puts '================================================================================'
- end
-
- if request.has_indices?
- policy_scope request
- else
- authorize request
- end
-
- unless ENV['RACK_ENV'] == 'production'
- puts '================================================================================'
- puts '= ' + "Request: #{request.fullpath}".ljust(76) + '='
- puts '= ' + "Endpoint: #{request.endpoint}".ljust(76) + '='
- puts '= ' + "Index: #{request.index}".ljust(76) + '='
- puts '= ' + "Type: #{request.type}".ljust(76) + '='
- puts '= ' + "Action: #{request.action}".ljust(76) + '='
- puts '================================================================================'
- end
+ if request.has_indices?
+ policy_scope request
+ else
+ authorize request
+ end
- # Throw so that we move on to the next middleware
- throw :next, true
+ unless ENV['RACK_ENV'] == 'production'
+ puts '================================================================================'
+ puts '= ' + "Request: #{request.fullpath}".ljust(76) + '='
+ puts '= ' + "Endpoint: #{request.endpoint}".ljust(76) + '='
+ puts '= ' + "Index: #{request.index}".ljust(76) + '='
+ puts '= ' + "Type: #{request.type}".ljust(76) + '='
+ puts '= ' + "Action: #{request.action}".ljust(76) + '='
+ puts '================================================================================'
end
+
+ @app.call env
end
end
end
diff --git a/lib/roda/plugins/authentication.rb b/lib/roda/plugins/authentication.rb
deleted file mode 100644
index ffc42af..0000000
--- a/lib/roda/plugins/authentication.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-class Roda
- module RodaPlugins
- # Provide helper methods to access Warden
- module Authentication
- module InstanceMethods
-
- def current_user
- env['warden'] ? env['warden'].user : nil
- end
-
- def authenticate
- env['warden'] && env['warden'].authenticate
- end
-
- def authenticated?
- env['warden'] && env['warden'].authenticated?
- end
-
- def authenticate!
- raise NotAuthenticated unless env['warden']
- env['warden'].authenticate!
- end
-
- def logout
- env['warden'] && env['warden'].logout
- end
- end
-
- class NotAuthenticated < StandardError
- end
- end
-
- register_plugin(:authentication, Authentication)
- end
-end
diff --git a/lib/roda/plugins/pundit.rb b/lib/roda/plugins/pundit.rb
deleted file mode 100644
index b4f7175..0000000
--- a/lib/roda/plugins/pundit.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require 'pundit'
-require 'proxes/es_request'
-
-class Roda
- module RodaPlugins
- module Pundit
- def self.configure(app, opts = {})
- policies_path = File.expand_path(opts[:policies]||'policies', app.opts[:root])
- Dir[File.join(policies_path, '/**/*.rb')].each { |file| require file }
- end
-
- module InstanceMethods
- include ::Pundit
-
- def authorize(record, query = nil)
- if record.is_a?(::ProxES::ESRequest)
- if record.action.nil? && record.index
- query = '_index?'
- else
- query = record.action ? record.action.to_s + '?' : '_root?'
- end
- else
- raise ArgumentError, 'Pundit cannot determine the query' if query.nil?
- end
- query = query.to_s + '?' unless query[-1] == '?'
- super(record, query)
- end
-
- def pundit_user
- current_user
- end
-
- def permitted_attributes(record, action)
- param_key = PolicyFinder.new(record).param_key
- policy = policy(record)
- method_name = if policy.respond_to?("permitted_attributes_for_#{action}")
- "permitted_attributes_for_#{action}"
- else
- 'permitted_attributes'
- end
-
- request.params.fetch(param_key, {}).select do |key, value|
- policy.public_send(method_name).include? key.to_sym
- end
- end
- end
- end
-
- register_plugin(:pundit, Pundit)
- end
-end
diff --git a/migrate/001_tables.rb b/migrate/001_tables.rb
index d8c1f04..a8ffa12 100644
--- a/migrate/001_tables.rb
+++ b/migrate/001_tables.rb
@@ -5,7 +5,6 @@
String :name
String :surname
String :email
- String :role
DateTime :created_at
DateTime :updated_at
end
@@ -18,5 +17,12 @@
DateTime :created_at
DateTime :updated_at
end
+
+ create_table :user_roles do
+ primary_key :id
+ foreign_key :user_id, :users
+ String :role
+ unique [:user_id, :role]
+ end
end
end
diff --git a/proxes.gemspec b/proxes.gemspec
index 1fd742b..4c6b1ce 100644
--- a/proxes.gemspec
+++ b/proxes.gemspec
@@ -22,14 +22,20 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'bundler', '~> 1.12'
spec.add_development_dependency 'rake', '~> 10.0'
spec.add_development_dependency 'rspec', '~> 3.0'
+ spec.add_development_dependency 'racksh'
+ spec.add_development_dependency 'rack-test'
+ spec.add_development_dependency 'pry-byebug'
+ spec.add_development_dependency 'rubocop'
spec.add_development_dependency 'guard'
spec.add_development_dependency 'guard-rake'
spec.add_development_dependency 'guard-rspec'
spec.add_development_dependency 'guard-shotgun'
+ spec.add_development_dependency 'database_cleaner'
+ spec.add_development_dependency 'factory_girl'
- spec.add_dependency 'rack_csrf'
spec.add_dependency 'rack-proxy'
- spec.add_dependency 'roda'
+ spec.add_dependency 'sinatra'
+ spec.add_dependency 'sinatra-flash'
spec.add_dependency 'elasticsearch'
spec.add_dependency 'logger'
spec.add_dependency 'pundit'
diff --git a/spec/crud_spec.rb b/spec/crud_spec.rb
new file mode 100644
index 0000000..a0d40ec
--- /dev/null
+++ b/spec/crud_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+require 'proxes/controllers'
+require 'support/crud_shared_examples'
+
+{
+ '/users' => ProxES::Users,
+}.each do |route, controller|
+ describe controller do
+ def app
+ described_class
+ end
+
+ context 'as super_admin_user' do
+ let(:user) { create(:super_admin_user) }
+ let(:model) { create(app.model_class.name.to_sym) }
+
+ before(:each) do
+ # Log in
+ warden = double(Warden::Proxy)
+ allow(warden).to receive(:user).and_return(user)
+ env 'warden', warden
+ end
+
+ it_behaves_like 'a CRUD Controller', route
+ end
+
+ context 'as user' do
+ let(:user) { create(:user) }
+ let(:model) { create(app.model_class.name.to_sym) }
+
+ before(:each) do
+ # Log in
+ warden = double(Warden::Proxy)
+ allow(warden).to receive(:user).and_return(user)
+ env 'warden', warden
+ end
+
+ it_behaves_like 'a CRUD Controller', route
+ end
+ end
+end
diff --git a/spec/factories.rb b/spec/factories.rb
new file mode 100644
index 0000000..53ff269
--- /dev/null
+++ b/spec/factories.rb
@@ -0,0 +1,34 @@
+require 'proxes/models/user'
+require 'proxes/models/user_role'
+
+FactoryGirl.define do
+ to_create { |i| i.save }
+
+ sequence(:email) { |n| "person-#{n}@example.com" }
+ sequence(:name) { |n| "Name-#{n}" }
+
+ factory :user, class: ProxES::User, aliases: [:'ProxES::User'] do
+ email
+
+ after(:create) do |user, evaluator|
+ user.add_user_role(role: 'user')
+ end
+
+ factory :admin_user do
+ after(:create) do |user, evaluator|
+ user.add_user_role(role: 'admin')
+ end
+ end
+
+ factory :super_admin_user do
+ after(:create) do |user, evaluator|
+ user.add_user_role(role: 'super_admin')
+ end
+ end
+ end
+
+ factory :user_role, class: ProxES::UserRole, aliases: [:'ProxES::UserRole'] do
+ role
+ user
+ end
+end
diff --git a/spec/proxes/security_spec.rb b/spec/proxes/security_spec.rb
new file mode 100644
index 0000000..96bc6e3
--- /dev/null
+++ b/spec/proxes/security_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+require 'proxes/security'
+require 'rack'
+require 'pundit/rspec'
+
+describe ProxES::Security do
+ def app
+ ProxES::Security.new(proc{[200,{},['Hello, world.']]})
+ end
+
+ context '#call' do
+ fit 'rejects anonymous requests' do
+ expect { get('/') }.to raise_error(ProxES::Helpers::NotAuthenticated)
+ end
+
+ context 'logged in' do
+ let(:user) { create(:user) }
+
+ before(:each) do
+ # Log in
+ warden = double(Warden::Proxy)
+ allow(warden).to receive(:user).and_return(user)
+ allow(warden).to receive(:authenticate!)
+ env 'warden', warden
+ end
+
+ it 'authorizes calls that return data' do
+ expect(get("/#{user.email}/_search")).to be_ok
+ expect{ get('/notmyindex/_search') }.to raise_error(Pundit::NotAuthorizedError)
+ end
+
+ it 'authorizes calls that do actions' do
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 459cfab..a1ac977 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,2 +1,35 @@
-$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
+ENV['RACK_ENV'] = 'test'
+
require 'proxes'
+require 'proxes/db'
+require 'rspec'
+require 'rack/test'
+require 'warden'
+require 'factory_girl'
+require 'database_cleaner'
+
+RSpec.configure do |config|
+ config.include Rack::Test::Methods
+ config.include Warden::Test::Helpers
+ config.include FactoryGirl::Syntax::Methods
+
+ config.alias_example_to :fit, focus: true
+ config.filter_run focus: true
+ config.run_all_when_everything_filtered = true
+
+ config.before(:suite) do
+ DatabaseCleaner.strategy = :transaction
+
+ FactoryGirl.find_definitions
+ end
+
+ config.around(:each) do |example|
+ DatabaseCleaner.cleaning do
+ example.run
+ end
+ end
+
+ config.after(:each) do
+ Warden.test_reset!
+ end
+end
diff --git a/spec/support/crud_shared_examples.rb b/spec/support/crud_shared_examples.rb
new file mode 100644
index 0000000..40ee8c7
--- /dev/null
+++ b/spec/support/crud_shared_examples.rb
@@ -0,0 +1,44 @@
+shared_examples 'a CRUD Controller' do |route|
+ context 'GET' do
+ it "#{route}" do
+ model # Ensure that there's at least one item in the list
+ get '/'
+
+ if Pundit.policy(user, app.model_class).list?
+ expect(last_response).to be_ok
+ else
+ expect(last_response).to_not be_ok
+ end
+ end
+
+ it "#{route}/new" do
+ get '/'
+
+ if Pundit.policy(user, app.model_class).list?
+ expect(last_response).to be_ok
+ else
+ expect(last_response).to_not be_ok
+ end
+ end
+
+ it "#{route}/id" do
+ get "/#{model.id}"
+
+ if Pundit.policy(user, app.model_class).list?
+ expect(last_response).to be_ok
+ else
+ expect(last_response).to_not be_ok
+ end
+ end
+
+ it "#{route}/id/edit" do
+ get "/#{model.id}"
+
+ if Pundit.policy(user, app.model_class).list?
+ expect(last_response).to be_ok
+ else
+ expect(last_response).to_not be_ok
+ end
+ end
+ end
+end
diff --git a/views/http_404.haml b/views/404.haml
similarity index 100%
rename from views/http_404.haml
rename to views/404.haml
diff --git a/views/_getting_started.haml b/views/getting_started.haml
similarity index 85%
rename from views/_getting_started.haml
rename to views/getting_started.haml
index bcc361f..121cfca 100644
--- a/views/_getting_started.haml
+++ b/views/getting_started.haml
@@ -14,8 +14,7 @@
%pre
curl -X GET 'http://proxes.co.za/#{ current_user.index_prefix}/_search' -H 'Authorization: YourTokenHere'
- %form.text-center.lead{ method: 'post', action: root_url + '/tokens' }
+ %form.text-center.lead{ method: 'post', action: '/tokens' }
No Token yet?
- = csrf_tag
%button.btn.btn-primary{ type: 'submit' } Get One
diff --git a/views/security/login.haml b/views/identity/login.haml
similarity index 86%
rename from views/security/login.haml
rename to views/identity/login.haml
index 6c45fa0..58856ea 100644
--- a/views/security/login.haml
+++ b/views/identity/login.haml
@@ -5,8 +5,7 @@
.panel-heading
ProxES Login
.panel-body
- %form{ method: 'post', action: '/auth/identity/callback' }
- = csrf_tag
+ %form{ method: 'post', action: '/_proxes/auth/identity/callback' }
.form-group
%label.control-label Username
%input.form-control.border-input{ name: 'username' }
diff --git a/views/identity/register.haml b/views/identity/register.haml
new file mode 100644
index 0000000..249a606
--- /dev/null
+++ b/views/identity/register.haml
@@ -0,0 +1,17 @@
+.row
+ .col-sm-12
+ %h1 ProxES Registration
+.row
+ .col-sm-2
+ .col-sm-8
+ .panel.panel-default
+ .panel-heading
+ ProxES Registration
+ .panel-body
+ %form.form-horizontal{ method: 'post', action: '/_proxes/auth/identity/new' }
+ = form_control(:username, identity, label: 'Email', placeholder: 'Your email address')
+ = form_control(:password, identity, label: 'Password', type: :password)
+ = form_control(:password_confirmation, identity, label: 'Confirm Password', type: :password)
+
+ %button.btn.btn-primary{ type: 'submit' } Register
+ .col-sm-2
diff --git a/views/index.haml b/views/index.haml
index 8a0c3d9..6f3b1b6 100644
--- a/views/index.haml
+++ b/views/index.haml
@@ -1,6 +1,6 @@
-= partial('getting_started')
+= haml :getting_started
#indexlist
-%script{ src: root_url + '/assets/js/vendors.js', type: 'text/javascript' }
-%script{ src: root_url + '/assets/js/bundle.js', type: 'text/javascript' }
+%script{ src: '/assets/js/vendors.js', type: 'text/javascript' }
+%script{ src: '/assets/js/bundle.js', type: 'text/javascript' }
diff --git a/views/layout.haml b/views/layout.haml
index b131a90..343ee61 100644
--- a/views/layout.haml
+++ b/views/layout.haml
@@ -4,7 +4,7 @@
%meta{ charset: 'utf-8' }
%meta{ 'http-equiv': "X-UA-Compatible", content: "IE=edge,chrome=1" }
%meta{ name: "viewport", content: "width=device-width, initial-scale=1" }
- %link{ rel: "apple-touch-icon", sizes: "76x76", href: root_url + '/assets/img/apple-icon.png' }
+ %link{ rel: "apple-touch-icon", sizes: "76x76", href: '/assets/img/apple-icon.png' }
%title
ProxES
@@ -24,11 +24,11 @@
%body
#wrapper
- = partial('navbar', locals: { title: (defined?(title) ? title : 'ProxES') })
+ = haml :'partials/navbar', locals: { title: (defined?(title) ? title : 'ProxES') }
#page-wrapper
.row
.col-md-12
- = partial('notifications')
+ = haml :'partials/notifications'
= yield
%footer.footer.text-muted.text-center
%hr
diff --git a/views/partials/delete_form.haml b/views/partials/delete_form.haml
new file mode 100644
index 0000000..6e1ec8e
--- /dev/null
+++ b/views/partials/delete_form.haml
@@ -0,0 +1,4 @@
+
diff --git a/views/partials/form_control.haml b/views/partials/form_control.haml
new file mode 100644
index 0000000..aa763e0
--- /dev/null
+++ b/views/partials/form_control.haml
@@ -0,0 +1,17 @@
+%div{ class: "form-group#{model.errors[name] ? ' has-error' : ''}" }
+ %label.col-sm-3.control-label{ for: attributes[:id] }= label
+ .col-sm-9
+ - type = attributes.delete(:type)
+ - if type == 'select'
+ - options = attributes.delete(:options)
+ %select{attributes}
+ %option{ value: ""} -- Select One --
+ - options.each do |k,v| k ||= v; v ||= k;
+ %option{ value: k, selected: (k.to_s == model[name].to_s)} = v
+ - elsif type == 'textarea'
+ %textarea{attributes}
+ = model[name]
+ - else
+ %input{attributes, type: type, value: model[name]}
+ - if model.errors[name]
+ %p.help-block.text-danger= model.errors[name].join(', ')
diff --git a/views/_navbar.haml b/views/partials/navbar.haml
similarity index 86%
rename from views/_navbar.haml
rename to views/partials/navbar.haml
index b016b87..604c48a 100644
--- a/views/_navbar.haml
+++ b/views/partials/navbar.haml
@@ -10,16 +10,15 @@
%span.icon-bar.bar2
%span.icon-bar.bar3
-if current_user
- %form.nav.navbar-top-links.navbar-form.navbar-right{ action: '/auth/identity', method: 'post' }
- = csrf_tag
+ %form.nav.navbar-top-links.navbar-form.navbar-right{ action: '/_proxes/auth/identity', method: 'post' }
%input{ name: '_method', value: 'DELETE', type: 'hidden' }
%button.btn.btn-default{ type: 'submit' }
/ %i.ti-panel
Logout
- .navbar-default.sidebar{ role: 'navigation' }
- = partial('sidebar')
- else
%ul.nav.navbar-top-links.navbar-right
%li
%a.btn.btn-link{ href: '/auth/identity' }
Log In
+ .navbar-default.sidebar{ role: 'navigation' }
+ = haml :'partials/sidebar'
diff --git a/views/_notifications.haml b/views/partials/notifications.haml
similarity index 100%
rename from views/_notifications.haml
rename to views/partials/notifications.haml
diff --git a/views/partials/pager.haml b/views/partials/pager.haml
new file mode 100644
index 0000000..bc4ccd0
--- /dev/null
+++ b/views/partials/pager.haml
@@ -0,0 +1,19 @@
+
diff --git a/views/_sidebar.haml b/views/partials/sidebar.haml
similarity index 67%
rename from views/_sidebar.haml
rename to views/partials/sidebar.haml
index 6cda9f7..75ac95b 100644
--- a/views/_sidebar.haml
+++ b/views/partials/sidebar.haml
@@ -1,20 +1,20 @@
%ul.nav.nav-pills.nav-stacked
- if defined?(current_user) && current_user
%li
- %a{ href: root_url + '/' }
+ %a{ href: '/_proxes' }
%i.fa.fa-dashboard.fa-fw
Dashboard
- if current_user.admin?
%li
- %a{ href: root_url + '/users' }
- %i.fa.fa-user
+ %a{ href: '/_proxes/users' }
+ %i.fa.fa-user.fa-fw
Users
- else
%li.active
%a{ href: '/auth/identity' }
- %i.fa.fa-user
+ %i.fa.fa-user.fa-fw
Log In
%li
%a{ href: '/auth/identity/register' }
- %i.fa.fa-pencil-square-o
+ %i.fa.fa-pencil-square-o.fa-fw
Register
diff --git a/views/security/register.haml b/views/security/register.haml
deleted file mode 100644
index d301d26..0000000
--- a/views/security/register.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-.row
- .col-sm-2
- .col-sm-8
- .panel.panel-default
- .panel-heading
- ProxES Registration
- .panel-body
- %form{ method: 'post', action: '/auth/identity/new' }
- = csrf_tag
- .form-group{ class: ('has-error' if identity.errors[:username]) }
- %label.control-label Email
- %input.form-control.border-input{ name: 'identity[username]', id: :'identity_username', type: 'text', autofocus: true, value: identity.username, placeholder: 'Your email address' }
- -if identity.errors[:username]
- %p.help-block.text-danger
- = identity.errors[:username].join(', ')
-
- .form-group{ class: ('has-error' if identity.errors[:password]) }
- %label.control-label Password
- %input.form-control.border-input{ name: 'identity[password]', id: 'identity_password', type: 'password', placeholder: 'Your password' }
- -if identity.errors[:password]
- %p.help-block.text-danger
- = identity.errors[:password].join(', ')
-
- .form-group{ class: ('has-error' if identity.errors[:password_confirmation]) }
- %label.control-label Confirm Password
- %input.form-control.border-input{ name: 'identity[password_confirmation]', id: 'identity_password_confirmation', type: 'password', placeholder: 'Confirm your password' }
- -if identity.errors[:password_confirmation]
- %p.help-block.text-danger
- = identity.errors[:password_confirmation].join(', ')
-
- %button.btn.btn-primary{ type: 'submit' } Register
- .col-sm-2
diff --git a/views/sidebar.haml b/views/sidebar.haml
new file mode 100644
index 0000000..51f19d0
--- /dev/null
+++ b/views/sidebar.haml
@@ -0,0 +1,20 @@
+%ul.nav.nav-pills.nav-stacked
+ - if current_user
+ %li
+ %a{ href: '/_proxes' }
+ %i.fa.fa-dashboard.fa-fw
+ Dashboard
+ - if current_user.admin?
+ %li
+ %a{ href: '/_proxes/users' }
+ %i.fa.fa-user.fa-fw
+ Users
+ - else
+ %li.active
+ %a{ href: '/auth/identity' }
+ %i.fa.fa-user.fa-fw
+ Log In
+ %li
+ %a{ href: '/auth/identity/register' }
+ %i.fa.fa-pencil-square-o.fa-fw
+ Register
diff --git a/views/users/_identity.haml b/views/users/_identity.haml
deleted file mode 100644
index 150ad2d..0000000
--- a/views/users/_identity.haml
+++ /dev/null
@@ -1,23 +0,0 @@
-.form-group{ class: ('has-error' if identity.errors[:username]) }
- %label.control-label Email
- - if identity.id
- %input.form-control.border-input{ disabled: true, id: :'identity_username', type: 'text', value: identity.username }
- - else
- %input.form-control.border-input{ name: 'identity[username]', id: :'identity_username', type: 'text', autofocus: true, value: identity.username, placeholder: 'Your email address' }
- -if identity.errors[:username]
- %p.help-block.text-danger
- = identity.errors[:username].join(', ')
-
-.form-group{ class: ('has-error' if identity.errors[:password]) }
- %label.control-label Password
- %input.form-control.border-input{ name: 'identity[password]', id: 'identity_password', type: 'password', placeholder: 'Your password' }
- -if identity.errors[:password]
- %p.help-block.text-danger
- = identity.errors[:password].join(', ')
-
-.form-group{ class: ('has-error' if identity.errors[:password_confirmation]) }
- %label.control-label Confirm Password
- %input.form-control.border-input{ name: 'identity[password_confirmation]', id: 'identity_password_confirmation', type: 'password', placeholder: 'Confirm your password' }
- -if identity.errors[:password_confirmation]
- %p.help-block.text-danger
- = identity.errors[:password_confirmation].join(', ')
diff --git a/views/users/_user.haml b/views/users/_user.haml
deleted file mode 100644
index b892e26..0000000
--- a/views/users/_user.haml
+++ /dev/null
@@ -1,23 +0,0 @@
-.form-group{ class: ('has-error' if user.errors[:name]) }
- %label.control-label Name
- %input.form-control.border-input{ name: 'user[name]', id: :'user_name', type: 'text', autofocus: true, value: user.name, placeholder: 'Your name' }
- -if user.errors[:name]
- %p.help-block.text-danger
- = user.errors[:name].join(', ')
-
-.form-group{ class: ('has-error' if user.errors[:surname]) }
- %label.control-label Surname
- %input.form-control.border-input{ name: 'user[surname]', id: :'user_surname', type: 'text', autofocus: true, value: user.surname, placeholder: 'Your surname' }
- -if user.errors[:surname]
- %p.help-block.text-danger
- = user.errors[:surname].join(', ')
-
-.form-group{ class: ('has-error' if user.errors[:role]) }
- %label.control-label Role
- %select.form-control.border-input{ name: 'user[role]', id: 'user_role' }
- %option{ value: 'user', selected: (user.role == 'user') } User
- %option{ value: 'owner', selected: (user.role == 'owner') } Owner
- %option{ value: 'admin', selected: (user.role == 'admin') } Admin
- -if user.errors[:role]
- %p.help-block.text-danger
- = user.errors[:role].join(', ')
diff --git a/views/users/display.haml b/views/users/display.haml
new file mode 100644
index 0000000..f54f590
--- /dev/null
+++ b/views/users/display.haml
@@ -0,0 +1,32 @@
+.row
+ .col-md-2
+ .col-md-8
+ .panel.panel-default
+ .panel-body
+ .author
+ %img.pull-right.thumbnail{ src: entity.gravatar }
+ %h4.title= entity.email
+
+ %hr
+ %p.description
+ %label Name:
+ = entity.name
+ %p.description
+ %label Surname:
+ = entity.surname
+ %p.description
+ %label Roles:
+ = entity.user_roles.map(&:role).map(&:titlecase).join(', ')
+ %p.description
+ %label Signed up:
+ = entity.created_at.strftime('%Y-%m-%d %H:%M:%S')
+
+ .row
+ .col-md-6
+ %a.btn.btn-default{ href: "/_proxes/users/#{entity.id}/edit" } Edit
+ .col-md-6.text-right
+ - if policy(entity).delete?
+ %form{ method: 'post', action: "/_proxes/users/#{entity.id}" }
+ %input{ name: '_method', value: 'DELETE', type: 'hidden' }
+ %button.btn.btn-warning{ type: 'submit' } Delete
+ .col-md-2
diff --git a/views/users/edit.haml b/views/users/edit.haml
index f38d3d2..db10eca 100644
--- a/views/users/edit.haml
+++ b/views/users/edit.haml
@@ -3,10 +3,9 @@
.col-md-8
.panel.panel-default
.panel-body
- %form{ method: 'post', action: root_url + "/users/#{user.id}" }
+ %form.form-horizontal{ method: 'post', action: "/_proxes/users/#{entity.id}" }
%input{ name: '_method', value: 'PUT', type: 'hidden' }
- = csrf_tag
- = partial('users/user', locals: { user: user })
+ = haml :'users/user', locals: { user: entity }
%button.btn.btn-primary{ type: 'submit' }
Update User
.col-md-2
diff --git a/views/users/identity.haml b/views/users/identity.haml
new file mode 100644
index 0000000..aa887a9
--- /dev/null
+++ b/views/users/identity.haml
@@ -0,0 +1,3 @@
+= form_control(:username, identity, label: 'Email', placeholder: 'Your email address')
+= form_control(:password, identity, type: 'password', placeholder: 'Your password')
+= form_control(:password_confirmation, identity, type: 'password', label: 'Confirm Password', placeholder: 'Confirm your password')
diff --git a/views/users/index.haml b/views/users/index.haml
new file mode 100644
index 0000000..7d1ba17
--- /dev/null
+++ b/views/users/index.haml
@@ -0,0 +1,22 @@
+.row
+ .col-md-12
+ .panel.panel-default
+ .panel-heading
+ = title
+ %table.table.table-striped
+ %thead
+ %tr
+ %th Email
+ %th Name
+ %th Surname
+ %th Roles
+ %tbody
+ -list.each do |entity|
+ %tr
+ %td
+ %a{ href: "/_proxes/users/#{entity.id}" }= entity.email
+ %td= entity.name
+ %td= entity.surname
+ %td= entity.user_roles.map(&:role).map(&:titlecase).join(', ')
+ .panel-body.text-right
+ %a.btn.btn-primary{ href: '/_proxes/users/new' } New User
\ No newline at end of file
diff --git a/views/users/list.haml b/views/users/list.haml
deleted file mode 100644
index 479a772..0000000
--- a/views/users/list.haml
+++ /dev/null
@@ -1,22 +0,0 @@
-.row
- .col-md-12
- .panel.panel-default
- .panel-heading
- = title
- %table.table.table-striped
- %thead
- %tr
- %th Email
- %th Name
- %th Surname
- %th Roles
- %tbody
- -users.each do |user|
- %tr
- %td
- %a{ href: root_url + "/users/#{user.id}" }= user.email
- %td= user.name
- %td= user.surname
- %td= user.role
- .panel-body.text-right
- %a.btn.btn-primary{ href: root_url + '/users/new' } New User
\ No newline at end of file
diff --git a/views/users/new.haml b/views/users/new.haml
index 197ce08..baa9aa9 100644
--- a/views/users/new.haml
+++ b/views/users/new.haml
@@ -3,10 +3,9 @@
.col-md-8
.panel.panel-default
.panel-body
- %form{ method: 'post', action: root_url + '/users' }
- = csrf_tag
- = partial('users/identity', locals: { identity: identity })
- = partial('users/user', locals: { user: user })
+ %form.form-horizontal{ method: 'post', action: '/_proxes/users' }
+ = haml :'users/identity', locals: { identity: identity }
+ = haml :'users/user', locals: { user: entity }
%button.btn.btn-primary.btn{ type: 'submit' }
Create User
.col-md-2
diff --git a/views/users/show.haml b/views/users/show.haml
deleted file mode 100644
index 708de93..0000000
--- a/views/users/show.haml
+++ /dev/null
@@ -1,23 +0,0 @@
-.row
- .col-md-2
- .col-md-8
- .panel.panel-default
- .panel-body
- .author
- %img.pull-right.thumbnail{ src: user.gravatar }
- %h4.title= user.email
-
- %hr
- %p.description
- %label Name:
- = user.name
- %p.description
- %label Surname:
- = user.surname
- %p.description
- %label Signed up:
- = user.created_at.strftime('%Y-%m-%d %H:%M:%S')
-
- %p.text-right
- %a.btn.btn-default{ href: root_url + "/users/#{user.id}/edit" } Edit
- .col-md-2
diff --git a/views/users/user.haml b/views/users/user.haml
new file mode 100644
index 0000000..1290f31
--- /dev/null
+++ b/views/users/user.haml
@@ -0,0 +1,11 @@
+= form_control(:name, user)
+= form_control(:surname, user)
+.form-group{ class: ('has-error' if user.errors[:user_roles]) }
+ %label.control-label.col-sm-3 Role
+ .col-sm-9
+ %select.form-control.border-input{ name: 'user[user_roles][]', id: 'user_user_roles', multiple: true }
+ - ProxES::UserRole.role_names.each do |name|
+ %option{ value: name, selected: user.has_role?(name) }= name.titlecase
+ -if user.errors[:role]
+ %p.help-block.text-danger
+ = user.errors[:role].join(', ')