ditty 0.6.0 → 0.7.0.pre.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -4
  3. data/config.ru +4 -18
  4. data/ditty.gemspec +2 -0
  5. data/lib/ditty/components/app.rb +4 -3
  6. data/lib/ditty/controllers/application.rb +28 -5
  7. data/lib/ditty/controllers/auth.rb +179 -0
  8. data/lib/ditty/controllers/component.rb +1 -3
  9. data/lib/ditty/controllers/main.rb +6 -155
  10. data/lib/ditty/controllers/users.rb +1 -0
  11. data/lib/ditty/helpers/component.rb +50 -22
  12. data/lib/ditty/helpers/response.rb +1 -0
  13. data/lib/ditty/helpers/views.rb +10 -0
  14. data/lib/ditty/listener.rb +1 -1
  15. data/lib/ditty/middleware/accept_extension.rb +31 -0
  16. data/lib/ditty/models/user.rb +1 -5
  17. data/lib/ditty/policies/identity_policy.rb +10 -2
  18. data/lib/ditty/policies/user_policy.rb +8 -1
  19. data/lib/ditty/services/authentication.rb +16 -7
  20. data/lib/ditty/services/logger.rb +4 -3
  21. data/lib/ditty/services/settings.rb +8 -0
  22. data/lib/ditty/version.rb +1 -1
  23. data/views/400.haml +2 -0
  24. data/views/{identity/forgot.haml → auth/forgot_password.haml} +1 -1
  25. data/views/auth/identity.haml +15 -0
  26. data/views/auth/login.haml +18 -0
  27. data/views/auth/register.haml +19 -0
  28. data/views/auth/register_identity.haml +14 -0
  29. data/views/{identity/reset.haml → auth/reset_password.haml} +2 -3
  30. data/views/layout.haml +2 -2
  31. data/views/partials/actions.haml +6 -4
  32. data/views/partials/form_tag.haml +2 -1
  33. data/views/partials/navbar.haml +2 -3
  34. data/views/partials/search.haml +1 -1
  35. data/views/partials/sidebar.haml +3 -3
  36. data/views/roles/display.haml +1 -2
  37. data/views/roles/index.haml +0 -4
  38. data/views/users/display.haml +2 -4
  39. data/views/users/index.haml +11 -10
  40. data/views/users/profile.haml +2 -4
  41. metadata +41 -8
  42. data/views/identity/login.haml +0 -29
  43. data/views/identity/register.haml +0 -29
@@ -131,6 +131,7 @@ module Ditty
131
131
  # Profile
132
132
  get '/profile' do
133
133
  entity = current_user
134
+ halt 404 unless entity
134
135
  authorize entity, :read
135
136
 
136
137
  haml :"#{view_location}/profile", locals: { entity: entity, identity: entity.identity.first, title: 'My Account' }
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'active_support'
4
4
  require 'active_support/inflector'
5
+ require 'active_support/core_ext/object/blank'
5
6
  require 'will_paginate/array'
6
7
 
7
8
  module Ditty
@@ -9,18 +10,38 @@ module Ditty
9
10
  module Component
10
11
  include ActiveSupport::Inflector
11
12
 
13
+ # param :count, Integer, min: 1, default: 10 # Can't do this, since count can be `all`
14
+ def check_count
15
+ return 10 if params[:count].nil?
16
+ count = params[:count].to_i
17
+ return count if count >= 1
18
+ excp = Sinatra::Param::InvalidParameterError.new 'Parameter cannot be less than 1'
19
+ excp.param = :count
20
+ raise excp
21
+ end
22
+
12
23
  def dataset
13
- search(filtered(policy_scope(settings.model_class)))
24
+ ds = policy_scope(settings.model_class)
25
+ ds = ds.where Sequel.|(*search_filters) unless search_filters.blank?
26
+ ds = ds.order ordering unless ordering.blank?
27
+ filtered(ds)
14
28
  end
15
29
 
16
30
  def list
17
- count = params['count'] || 10
18
- page = params['page'] || 1
31
+ param :q, String
32
+ param :page, Integer, min: 1, default: 1
33
+ param :sort, String
34
+ param :order, String, in: %w[asc desc], transform: :downcase, default: 'asc'
35
+ # TODO: Can we dynamically validate the search / filter fields?
36
+
37
+ ds = dataset
38
+ ds = ds.dataset if ds.respond_to?(:dataset)
39
+ return ds if params[:count] == 'all'
40
+ params[:count] = check_count
19
41
 
20
- ds = dataset.respond_to?(:dataset) ? dataset.dataset : dataset
21
- return ds if count == 'all'
22
42
  # Account for difference between sequel paginate and will paginate
23
- ds.is_a?(Array) ? ds.paginate(page: page.to_i, per_page: count.to_i) : ds.paginate(page.to_i, count.to_i)
43
+ return ds.paginate(page: params[:page], per_page: params[:count]) if ds.is_a?(Array)
44
+ ds.paginate(params[:page], params[:count])
24
45
  end
25
46
 
26
47
  def heading(action = nil)
@@ -39,7 +60,7 @@ module Ditty
39
60
  settings.dehumanized || underscore(heading)
40
61
  end
41
62
 
42
- def filters
63
+ def filter_fields
43
64
  self.class.const_defined?('FILTERS') ? self.class::FILTERS : []
44
65
  end
45
66
 
@@ -47,21 +68,32 @@ module Ditty
47
68
  self.class.const_defined?('SEARCHABLE') ? self.class::SEARCHABLE : []
48
69
  end
49
70
 
50
- def filtered(dataset)
71
+ def filtered(ds)
51
72
  filters.each do |filter|
52
- next if [nil, ''].include? params[filter[:name].to_s]
53
- filter[:field] ||= filter[:name]
54
- filter[:modifier] ||= :to_s
55
- dataset = apply_filter(dataset, filter)
73
+ ds = apply_filter(ds, filter)
56
74
  end
57
- dataset
75
+ ds
58
76
  end
59
77
 
60
- def apply_filter(dataset, filter)
61
- value = params[filter[:name].to_s].send(filter[:modifier])
62
- return dataset.where(filter[:field] => value) unless filter[:field].to_s.include? '.'
78
+ def filters
79
+ filter_fields.map do |filter|
80
+ next if params[filter[:name]].blank?
81
+ filter[:field] ||= filter[:name]
82
+ filter[:modifier] ||= :to_s # TODO: Do this with Sinatra Param?
83
+ filter
84
+ end.compact
85
+ end
63
86
 
64
- dataset.where(filter_field(filter) => filter_value(filter))
87
+ def ordering
88
+ return if params[:sort].blank?
89
+ Sequel.send(params[:order].to_sym, params[:sort].to_sym)
90
+ end
91
+
92
+ def apply_filter(ds, filter)
93
+ value = params[filter[:name]].send(filter[:modifier])
94
+ return ds.where(filter[:field] => value) unless filter[:field].to_s.include? '.'
95
+
96
+ ds.where(filter_field(filter) => filter_value(filter))
65
97
  end
66
98
 
67
99
  def filter_field(filter)
@@ -84,13 +116,9 @@ module Ditty
84
116
  end
85
117
 
86
118
  def search_filters
119
+ return [] if params[:q].blank?
87
120
  searchable_fields.map { |f| Sequel.ilike(f.to_sym, "%#{params[:q]}%") }
88
121
  end
89
-
90
- def search(dataset)
91
- return dataset if ['', nil].include?(params['q']) || search_filters == []
92
- dataset.where Sequel.|(*search_filters)
93
- end
94
122
  end
95
123
  end
96
124
  end
@@ -43,6 +43,7 @@ module Ditty
43
43
  format.html do
44
44
  actions = {}
45
45
  actions["#{base_path}/#{entity.id}/edit"] = "Edit #{heading}" if policy(entity).update?
46
+ actions["#{base_path}/new"] = "New #{heading}" if policy(entity).create?
46
47
  title = heading(:read) + (entity.respond_to?(:name) ? ": #{entity.name}" : '')
47
48
  haml :"#{view_location}/display",
48
49
  locals: { entity: entity, title: title, actions: actions },
@@ -104,6 +104,16 @@ module Ditty
104
104
  }
105
105
  haml :'partials/pager', locals: locals
106
106
  end
107
+
108
+ def display(value, type = :string)
109
+ if [true, false].include?(value) || type.to_sym == :boolean
110
+ value ? 'Yes' : 'No'
111
+ elsif value.nil? || type.to_sym == :nil
112
+ '(Empty)'
113
+ else
114
+ value
115
+ end
116
+ end
107
117
  end
108
118
  end
109
119
  end
@@ -17,7 +17,7 @@ module Ditty
17
17
  end
18
18
 
19
19
  def method_missing(method, *args)
20
- return unless args[0].is_a?(Hash) && args[0][:target] && args[0][:target].settings.track_actions
20
+ return unless args[0].is_a?(Hash) && args[0][:target].is_a?(Sinatra::Base) && args[0][:target].settings.track_actions
21
21
 
22
22
  log_action({
23
23
  user: args[0][:target].current_user,
@@ -0,0 +1,31 @@
1
+ module Ditty
2
+ module Middleware
3
+ # Allow requests to be responded to in JSON if the URL has .json at the end.
4
+ # The regex and the content_type can be customized to allow for other formats.
5
+ # Some inspiration from https://gist.github.com/tstachl/6264249
6
+ class AcceptExtension
7
+ attr_reader :env, :regex, :content_type
8
+
9
+ def initialize(app, regex = /\A(.*)\.json(\/?)\Z/, content_type = 'application/json')
10
+ # @mutex = Mutex.new
11
+ @app = app
12
+ @regex = regex
13
+ @content_type = content_type
14
+ end
15
+
16
+ def call(env)
17
+ @env = env
18
+
19
+ request = Rack::Request.new(env)
20
+ if request.path =~ regex
21
+ request.path_info = request.path_info.gsub(regex, '\1\2')
22
+ env = request.env
23
+ env['ACCEPT'] = content_type
24
+ env['CONTENT_TYPE'] = content_type
25
+ end
26
+
27
+ @app.call env
28
+ end
29
+ end
30
+ end
31
+ end
@@ -15,7 +15,7 @@ module Ditty
15
15
  one_to_many :audit_logs
16
16
 
17
17
  def role?(check)
18
- @roles ||= Hash.new do |h,k|
18
+ @roles ||= Hash.new do |h, k|
19
19
  h[k] = !roles_dataset.first(name: k).nil?
20
20
  end
21
21
  @roles[check]
@@ -56,10 +56,6 @@ module Ditty
56
56
  add_role Role.find_or_create(name: 'user')
57
57
  end
58
58
 
59
- def index_prefix
60
- email
61
- end
62
-
63
59
  def username
64
60
  identity_dataset.first.username
65
61
  end
@@ -4,8 +4,16 @@ require 'ditty/policies/application_policy'
4
4
 
5
5
  module Ditty
6
6
  class IdentityPolicy < ApplicationPolicy
7
- def register?
8
- !['1', 1, 'true', true, 'yes'].include? ENV['DITTY_REGISTERING_DISABLED']
7
+ def login?
8
+ true
9
+ end
10
+
11
+ def forgot_password?
12
+ true
13
+ end
14
+
15
+ def reset_password?
16
+ record.new? || (record.reset_requested && record.reset_requested > (Time.now - (24 * 60 * 60)))
9
17
  end
10
18
 
11
19
  def permitted_attributes
@@ -4,6 +4,11 @@ require 'ditty/policies/application_policy'
4
4
 
5
5
  module Ditty
6
6
  class UserPolicy < ApplicationPolicy
7
+ def register?
8
+ # TODO: Check email domain against settings
9
+ !['1', 1, 'true', true, 'yes'].include? ENV['DITTY_REGISTERING_DISABLED']
10
+ end
11
+
7
12
  def create?
8
13
  user && user.super_admin?
9
14
  end
@@ -34,8 +39,10 @@ module Ditty
34
39
  def resolve
35
40
  if user && user.super_admin?
36
41
  scope
37
- else
42
+ elsif user
38
43
  scope.where(id: user.id)
44
+ else
45
+ scope.where(id: -1)
39
46
  end
40
47
  end
41
48
  end
@@ -1,11 +1,13 @@
1
1
  require 'ditty/models/identity'
2
- require 'ditty/controllers/main'
2
+ require 'ditty/controllers/auth'
3
3
  require 'ditty/services/settings'
4
4
  require 'ditty/services/logger'
5
5
 
6
6
  require 'omniauth'
7
7
  OmniAuth.config.logger = Ditty::Services::Logger.instance
8
+ OmniAuth.config.path_prefix = "#{Ditty::Application.map_path}/auth"
8
9
  OmniAuth.config.on_failure = proc { |env|
10
+ next [400, {}, []] if env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
9
11
  OmniAuth::FailureEndpoint.new(env).redirect_to_failure
10
12
  }
11
13
 
@@ -13,13 +15,21 @@ module Ditty
13
15
  module Services
14
16
  module Authentication
15
17
  class << self
18
+ def [](key)
19
+ config[key]
20
+ end
21
+
16
22
  def providers
17
- config.keys
23
+ config.compact.keys
18
24
  end
19
25
 
20
26
  def setup
21
27
  providers.each do |provider|
22
- require "omniauth/#{provider}"
28
+ begin
29
+ require "omniauth/#{provider}"
30
+ rescue LoadError
31
+ require "omniauth-#{provider}"
32
+ end
23
33
  end
24
34
  end
25
35
 
@@ -37,13 +47,12 @@ module Ditty
37
47
  arguments: [
38
48
  {
39
49
  fields: [:username],
40
- callback_path: '/auth/identity/callback',
41
50
  model: Ditty::Identity,
42
- on_login: Ditty::Main,
43
- on_registration: Ditty::Main,
51
+ on_login: Ditty::Auth,
52
+ on_registration: Ditty::Auth,
44
53
  locate_conditions: ->(req) { { username: req['username'] } }
45
54
  }
46
- ],
55
+ ]
47
56
  }
48
57
  }
49
58
  end
@@ -9,6 +9,9 @@ require 'active_support/core_ext/object/blank'
9
9
 
10
10
  module Ditty
11
11
  module Services
12
+ # This is the central logger for Ditty. It can be configured to log to
13
+ # multiple endpoints through Ditty Settings. The default configuration is to
14
+ # send logs to $stdout
12
15
  class Logger
13
16
  include Singleton
14
17
 
@@ -21,9 +24,7 @@ module Ditty
21
24
  klass = values[:class].constantize
22
25
  opts = tr(values[:options]) || nil
23
26
  logger = klass.new(opts)
24
- if values[:level]
25
- logger.level = klass.const_get(values[:level].to_sym)
26
- end
27
+ logger.level = klass.const_get(values[:level].to_sym) if values[:level]
27
28
  @loggers << logger
28
29
  end
29
30
  end
@@ -7,6 +7,14 @@ require 'active_support/core_ext/hash/keys'
7
7
 
8
8
  module Ditty
9
9
  module Services
10
+ # This is the central settings service Ditty. It is used to get the settings
11
+ # for various aspects of Ditty, and can also be used to configure your own
12
+ # application.
13
+ #
14
+ # It has the concept of sections which can either be included in the main
15
+ # settings.yml file, or as separate files in the `config` folder. The values
16
+ # in separate files will be used in preference of those in the `settings.yml`
17
+ # file.
10
18
  module Settings
11
19
  CONFIG_FOLDER = './config'.freeze
12
20
  CONFIG_FILE = "#{CONFIG_FOLDER}/settings.yml".freeze
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ditty
4
- VERSION = '0.6.0'.freeze
4
+ VERSION = '0.7.0-rc1'.freeze
5
5
  end
@@ -0,0 +1,2 @@
1
+ %p.lead
2
+ The input you provided is invalid. Please check it and try again.
@@ -5,7 +5,7 @@
5
5
  .panel-body
6
6
  %p.text-center
7
7
  Enter your email address and we will send you a link to reset your password if you've registered on the system.
8
- %form.form-horizontal{ method: 'post', action: "#{settings.map_path}/auth/identity/forgot" }
8
+ = form_tag("#{settings.map_path}/auth/forgot-password") do
9
9
  .form-group
10
10
  .col-sm-12
11
11
  %input.form-control{ name: 'email', type: 'text', placeholder: 'Enter your email address' }
@@ -0,0 +1,15 @@
1
+ = form_tag("#{settings.map_path}/auth/identity/callback", attributes: { class: '' }) do
2
+ .form-group
3
+ %label.control-label Email
4
+ %input.form-control.border-input{ name: 'username', tabindex: '1' }
5
+ .form-group
6
+ %label.control-label{ style: 'display: block' }
7
+ Password
8
+ %a{ href: "#{settings.map_path}/auth/forgot-password", style: 'float: right', tabindex: '5' }
9
+ Forgot?
10
+ %input.form-control.border-input{ name: 'password', type: 'password', tabindex: '2' }
11
+ %button.btn.btn-primary{ type: 'submit', tabindex: '3' } Log In
12
+ - if policy(::Ditty::User).register?
13
+ .pull-right
14
+ No account yet?
15
+ %a.btn.btn-default{ href: "#{settings.map_path}/auth/register", tabindex: '4' } Register
@@ -0,0 +1,18 @@
1
+ .row
2
+ .col-sm-2
3
+ .col-sm-8
4
+ .panel.panel-default
5
+ .panel-body
6
+ - if Ditty::Services::Authentication.provides? 'identity'
7
+ = haml :'auth/identity'
8
+ .row
9
+ .col-sm-12= "&nbsp;"
10
+ .row
11
+ .col-sm-8.col-sm-push-2
12
+ - Ditty::Services::Authentication.providers.each do |name|
13
+ - provider = Ditty::Services::Authentication[name]
14
+ - next if provider[:login_prompt].nil?
15
+ %a.btn.btn-block.btn-default{ href: "#{settings.map_path}/auth/#{name}" }
16
+ %i.fa{ class: "fa-#{provider[:icon] || 'key'}"}
17
+ = provider[:login_prompt]
18
+ .col-sm-2
@@ -0,0 +1,19 @@
1
+ / TODO: How can we detect a google registration? Extra parameter to the callback? Or a custom callback page?
2
+ .row
3
+ .col-md-2
4
+ .col-md-8
5
+ .panel.panel-default
6
+ .panel-body
7
+ - if Ditty::Services::Authentication.provides? 'identity'
8
+ = haml :'auth/register_identity', locals: { identity: identity }
9
+ .row
10
+ .col-sm-12= "&nbsp;"
11
+ .row
12
+ .col-sm-8.col-sm-push-2
13
+ - Ditty::Services::Authentication.providers.each do |name|
14
+ - provider = Ditty::Services::Authentication[name]
15
+ - next if provider[:register_prompt].nil?
16
+ %a.btn.btn-block.btn-default{ href: "#{settings.map_path}/auth/#{name}" }
17
+ %i.fa{ class: "fa-#{provider[:icon] || 'key'}"}
18
+ = provider[:register_prompt]
19
+ .col-md-2
@@ -0,0 +1,14 @@
1
+ = form_tag("#{settings.map_path}/auth/register/identity") do
2
+ = form_control(:username, identity, label: 'Email', placeholder: 'your@email.com')
3
+ = form_control(:password, identity, label: 'Password', type: :password)
4
+ = form_control(:password_confirmation, identity, label: 'Confirm Password', type: :password)
5
+
6
+ - if identity.errors[:password] && identity.errors[:password].include?('is not strong enough')
7
+ .alert.alert-warning
8
+ %p Make sure your password is at least 8 characters long, and including the following
9
+ %ul
10
+ %li Upper- and lowercase letters
11
+ %li Numbers
12
+ %li Special Characters
13
+
14
+ %button.btn.btn-primary{ type: 'submit' } Register