ditty 0.7.2 → 0.8.0

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -7
  3. data/.travis.yml +5 -5
  4. data/Gemfile.ci +2 -0
  5. data/Rakefile +4 -3
  6. data/Readme.md +24 -2
  7. data/ditty.gemspec +4 -3
  8. data/lib/ditty.rb +24 -0
  9. data/lib/ditty/cli.rb +6 -2
  10. data/lib/ditty/components/app.rb +10 -1
  11. data/lib/ditty/controllers/application.rb +72 -10
  12. data/lib/ditty/controllers/audit_logs.rb +1 -5
  13. data/lib/ditty/controllers/auth.rb +37 -17
  14. data/lib/ditty/controllers/component.rb +15 -5
  15. data/lib/ditty/controllers/main.rb +1 -5
  16. data/lib/ditty/controllers/roles.rb +2 -5
  17. data/lib/ditty/controllers/user_login_traits.rb +18 -0
  18. data/lib/ditty/controllers/users.rb +4 -9
  19. data/lib/ditty/db.rb +3 -1
  20. data/lib/ditty/emails/base.rb +13 -4
  21. data/lib/ditty/helpers/authentication.rb +6 -5
  22. data/lib/ditty/helpers/component.rb +9 -1
  23. data/lib/ditty/helpers/response.rb +24 -3
  24. data/lib/ditty/helpers/views.rb +20 -0
  25. data/lib/ditty/listener.rb +38 -10
  26. data/lib/ditty/middleware/accept_extension.rb +2 -0
  27. data/lib/ditty/middleware/error_catchall.rb +2 -0
  28. data/lib/ditty/models/audit_log.rb +1 -0
  29. data/lib/ditty/models/base.rb +4 -0
  30. data/lib/ditty/models/identity.rb +3 -0
  31. data/lib/ditty/models/role.rb +1 -0
  32. data/lib/ditty/models/user.rb +9 -1
  33. data/lib/ditty/models/user_login_trait.rb +17 -0
  34. data/lib/ditty/policies/audit_log_policy.rb +6 -6
  35. data/lib/ditty/policies/role_policy.rb +2 -2
  36. data/lib/ditty/policies/user_login_trait_policy.rb +45 -0
  37. data/lib/ditty/policies/user_policy.rb +2 -2
  38. data/lib/ditty/rubocop.rb +3 -0
  39. data/lib/ditty/seed.rb +2 -0
  40. data/lib/ditty/services/authentication.rb +7 -2
  41. data/lib/ditty/services/email.rb +8 -2
  42. data/lib/ditty/services/logger.rb +11 -0
  43. data/lib/ditty/services/pagination_wrapper.rb +2 -0
  44. data/lib/ditty/services/settings.rb +14 -3
  45. data/lib/ditty/tasks/ditty.rake +109 -0
  46. data/lib/ditty/tasks/omniauth-ldap.rake +43 -0
  47. data/lib/ditty/version.rb +1 -1
  48. data/lib/rubocop/cop/ditty/call_services_directly.rb +42 -0
  49. data/migrate/20181209_add_user_login_traits.rb +16 -0
  50. data/migrate/20181209_extend_audit_log.rb +12 -0
  51. data/views/403.haml +2 -0
  52. data/views/audit_logs/index.haml +11 -6
  53. data/views/auth/ldap.haml +17 -0
  54. data/views/emails/forgot_password.haml +1 -1
  55. data/views/emails/layouts/action.haml +10 -6
  56. data/views/emails/layouts/alert.haml +2 -1
  57. data/views/emails/layouts/billing.haml +2 -1
  58. data/views/error.haml +8 -3
  59. data/views/partials/form_control.haml +24 -20
  60. data/views/partials/navbar.haml +11 -12
  61. data/views/partials/sidebar.haml +1 -1
  62. data/views/roles/index.haml +2 -0
  63. data/views/user_login_traits/display.haml +32 -0
  64. data/views/user_login_traits/edit.haml +10 -0
  65. data/views/user_login_traits/form.haml +5 -0
  66. data/views/user_login_traits/index.haml +30 -0
  67. data/views/user_login_traits/new.haml +10 -0
  68. data/views/users/display.haml +1 -1
  69. data/views/users/login_traits.haml +27 -0
  70. data/views/users/profile.haml +2 -0
  71. metadata +50 -21
  72. data/lib/ditty/rake_tasks.rb +0 -102
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e259c6108ab65c11c7e5c33bb445406f3c5e172762369ac3fd9acc4055410d72
4
- data.tar.gz: 0af669878d11300cdf8e0853bfde158f70510ccf4ae390b11910c73fa1856347
3
+ metadata.gz: 2b391296636a3f248171d3d71ca4c11466b8cce56f8814c271f2352e241da297
4
+ data.tar.gz: 8b136a384187ea59d6d8caa8f46f3dcc7a4a72939ba924ddff0eb8a332828aa1
5
5
  SHA512:
6
- metadata.gz: 315f96dba2cc2c6ed416bf23f0f326deccf55bfe27e72c94b51f3001c84c5b8bab0571700c47d981bb23e0b61ab7a62c971653603e1ae2b6df9e0eb926756068
7
- data.tar.gz: f426c1883a87e955fb5e9dd680f482287438fd101db1ebed3c3a515bdf8cf7d93c5cfc7ccba5b23ce37f70e8a7ccedc54bcc01c592eb19599090a111802e05d6
6
+ metadata.gz: 8102d97c1b3516e4f922600055a22b5bff5293dd69fbcb92a5cd233ad0e9f2e52fc6f091e05aebcee9e1dd610e76b0049db29b1eccfd54ff09f794325156cf68
7
+ data.tar.gz: c1ad5e59438e58fc6dbc34d5d2a95b1c5da4a0ccb88481fc15b9dd5942ece2d7525adcf52d3d520f9fd2c91dff65829d24a27ad8fb60d66889ac6f9b983c2fba
@@ -1,12 +1,9 @@
1
+ require: rubocop-rspec
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 2.3
1
5
  Metrics/LineLength:
2
6
  Max: 120
3
-
4
- Style/NumericPredicate:
5
- Enabled: false
6
-
7
7
  Layout/LeadingCommentSpace:
8
8
  Exclude:
9
9
  - 'config.ru'
10
-
11
- AllCops:
12
- TargetRubyVersion: 2.2
@@ -1,10 +1,10 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.5.1
5
- - 2.4.0
6
- - 2.3.3
7
- - 2.2.6
4
+ - 2.6.0
5
+ - 2.5.3
6
+ - 2.4.5
7
+ - 2.3.8
8
8
  gemfile: Gemfile.ci
9
9
  env:
10
10
  global:
@@ -21,7 +21,7 @@ before_script:
21
21
  - bundle exec rake ditty:prep
22
22
  script:
23
23
  - bundle exec rake
24
- - bundle exec rubocop --fail-level W lib views
24
+ - bundle exec rubocop --fail-level W lib views specs
25
25
  after_script:
26
26
  - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
27
27
  after_success:
data/Gemfile.ci CHANGED
@@ -3,7 +3,9 @@ source 'https://rubygems.org'
3
3
 
4
4
  gemspec
5
5
 
6
+ gem 'faker'
6
7
  gem 'rubocop'
8
+ gem 'rubocop-rspec'
7
9
  gem 'simplecov', '~> 0.13.0'
8
10
  gem 'sqlite3'
9
11
 
data/Rakefile CHANGED
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rake'
4
- require 'bundler/gem_tasks'
5
- require 'ditty/rake_tasks'
6
-
7
4
  require 'ditty'
8
5
  require 'ditty/components/app'
9
6
 
7
+ Ditty.component :app
8
+
9
+ Ditty::Components.tasks
10
+ require 'bundler/gem_tasks' if File.exist? 'ditty.gemspec'
10
11
  begin
11
12
  require 'rspec/core/rake_task'
12
13
  RSpec::Core::RakeTask.new(:spec)
data/Readme.md CHANGED
@@ -1,6 +1,7 @@
1
1
  [![Build Status](https://travis-ci.org/EagerELK/ditty.svg?branch=master)](https://travis-ci.org/EagerELK/ditty)
2
2
  [![Code Climate](https://codeclimate.com/github/EagerELK/ditty/badges/gpa.svg)](https://codeclimate.com/github/EagerELK/ditty)
3
3
  [![Test Coverage](https://codeclimate.com/github/EagerELK/ditty/badges/coverage.svg)](https://codeclimate.com/github/EagerELK/ditty/coverage)
4
+ [![Inline docs](http://inch-ci.org/github/EagerELK/ditty.svg?branch=master)](http://inch-ci.org/github/EagerELK/ditty)
4
5
 
5
6
  # Ditty
6
7
 
@@ -33,12 +34,33 @@ gem install ditty
33
34
 
34
35
  1. Add the components to your rack config file. See the included [`config.ru`](https://github.com/EagerELK/ditty/blob/master/config.ru) file for an example setup
35
36
  2. Set the DB connection as the `DATABASE_URL` ENV variable: `DATABASE_URL=sqlite://development.db`
37
+ 3. Prepare the Ditty folder: `bundle exec ditty prep`
36
38
  3. Run the Ditty migrations: `bundle exec ditty migrate`
37
39
  4. Run the Ditty server: `bundle exec ditty server`
38
40
 
39
- ## Components
41
+ ### Components
40
42
 
41
- The application can now be further extended by creating components.
43
+ The application can now be further extended by creating [components](https://github.com/EagerELK/ditty/wiki/Creating-a-Component).
44
+
45
+ ### Rubocop Cops
46
+
47
+ Ditty provides a number of [Rubocop](https://github.com/rubocop-hq/rubocop) cops
48
+ to ensure that the Ditty framework is used correctly. Enable this by adding the
49
+ following to your `.rubocop.yml` file:
50
+
51
+ ```yaml
52
+ require: ditty/rubocop
53
+ ```
54
+
55
+ You can run Ditty specific cops as follows:
56
+
57
+ ```bash
58
+ bundle exec rubocop --only Ditty
59
+ ```
60
+
61
+ Adding the `-a` flag to the invocation will automatically fix some of the issues
62
+ for you, but, as always, ensure you have a working copy of your code before
63
+ running this.
42
64
 
43
65
  ## Development
44
66
 
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = ['ditty']
21
21
  spec.require_paths = ['lib']
22
22
 
23
- spec.add_development_dependency 'bundler', '~> 1.12'
23
+ spec.add_development_dependency 'bundler', '>= 1'
24
24
  spec.add_development_dependency 'database_cleaner'
25
25
  spec.add_development_dependency 'factory_bot'
26
26
  spec.add_development_dependency 'rack-test'
@@ -30,10 +30,11 @@ Gem::Specification.new do |spec|
30
30
 
31
31
  spec.add_dependency 'activesupport', '>= 3'
32
32
  spec.add_dependency 'bcrypt', '~> 3.1'
33
+ spec.add_dependency 'browser', '~> 2.5'
33
34
  spec.add_dependency 'haml', '~> 5.0'
34
35
  spec.add_dependency 'logger', '~> 1.0'
35
- spec.add_dependency 'oga', '>= 2.14'
36
36
  spec.add_dependency 'mail', '>= 1.7'
37
+ spec.add_dependency 'oga', '>= 2.14'
37
38
  spec.add_dependency 'omniauth', '~> 1.0'
38
39
  spec.add_dependency 'omniauth-identity', '~> 1.0'
39
40
  spec.add_dependency 'pundit', '~> 1.0'
@@ -45,8 +46,8 @@ Gem::Specification.new do |spec|
45
46
  spec.add_dependency 'sinatra-contrib', '~> 2.0'
46
47
  spec.add_dependency 'sinatra-flash', '~> 0.3'
47
48
  spec.add_dependency 'sinatra-param', '~> 1.5'
48
- spec.add_dependency 'tilt', '>= 2'
49
49
  spec.add_dependency 'thor', '>= 0.20'
50
+ spec.add_dependency 'tilt', '>= 2'
50
51
  spec.add_dependency 'will_paginate', '>= 3.1'
51
52
  spec.add_dependency 'wisper', '~> 2.0'
52
53
  end
@@ -6,6 +6,8 @@ require 'ditty/services/logger'
6
6
  module Ditty
7
7
  class ComponentError < StandardError; end
8
8
 
9
+ class TemplateNotFoundError < StandardError; end
10
+
9
11
  # A thread safe cache class, offering only #[] and #[]= methods,
10
12
  # each protected by a mutex.
11
13
  # Ripped off from Roda - https://github.com/jeremyevans/roda
@@ -30,6 +32,10 @@ module Ditty
30
32
  @mutex.synchronize { @hash.map(&block) }
31
33
  end
32
34
 
35
+ def each(&block)
36
+ @mutex.synchronize { @hash.each(&block) }
37
+ end
38
+
33
39
  def inject(memo, &block)
34
40
  @mutex.synchronize { @hash.inject(memo, &block) }
35
41
  end
@@ -37,6 +43,10 @@ module Ditty
37
43
  def each_with_object(memo, &block)
38
44
  @mutex.synchronize { @hash.each_with_object(memo, &block) }
39
45
  end
46
+
47
+ def key?(key)
48
+ @hash.key? key
49
+ end
40
50
  end
41
51
 
42
52
  # Ripped off from Roda - https://github.com/jeremyevans/roda
@@ -57,6 +67,10 @@ module Ditty
57
67
  component
58
68
  end
59
69
 
70
+ def self.component?(name)
71
+ @components.key? name
72
+ end
73
+
60
74
  # Register the given component with Component, so that it can be loaded using #component
61
75
  # with a symbol. Should be used by component files. Example:
62
76
  #
@@ -111,6 +125,15 @@ module Ditty
111
125
  end
112
126
  end
113
127
 
128
+ def self.tasks
129
+ require 'rake'
130
+ require 'rake/tasklib'
131
+ require 'ditty/db' unless defined? DB
132
+ components.each do |_name, comp|
133
+ comp.tasks if comp.respond_to?(:tasks)
134
+ end
135
+ end
136
+
114
137
  module Base
115
138
  module ClassMethods
116
139
  # Load a new component into the current class. A component can be a module
@@ -121,6 +144,7 @@ module Ditty
121
144
  # Component.component :csrf
122
145
  def component(component, *args, &block)
123
146
  raise ComponentError, 'Cannot add a component to a frozen Component class' if frozen?
147
+
124
148
  component = Components.load_component(component) if component.is_a?(Symbol)
125
149
  include(component::InstanceMethods) if defined?(component::InstanceMethods)
126
150
  extend(component::ClassMethods) if defined?(component::ClassMethods)
@@ -1,8 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # https://nandovieira.com/creating-generators-and-executables-with-thor
2
4
  require 'dotenv/load'
3
5
  require 'thor'
4
- require 'ditty'
5
- require 'ditty/rake_tasks'
6
6
  require 'rack'
7
7
  require 'rake'
8
8
 
@@ -12,6 +12,7 @@ module Ditty
12
12
 
13
13
  desc 'server', 'Start the Ditty server'
14
14
  require './application' if File.exist?('application.rb')
15
+ Ditty::Components.tasks
15
16
  def server
16
17
  # Ensure the token files are present
17
18
  Rake::Task['ditty:generate_tokens'].invoke
@@ -39,6 +40,9 @@ module Ditty
39
40
  # Run the migrations
40
41
  Rake::Task['ditty:migrate:up'].invoke
41
42
  puts 'Ditty Migrations Executed'
43
+
44
+ Rake::Task['ditty:dump_schema'].invoke
45
+ puts 'Ditty DB Schema Dumped'
42
46
  end
43
47
  end
44
48
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'ditty'
4
+ require 'ditty/services/settings'
4
5
 
5
6
  module Ditty
6
7
  class App
@@ -12,6 +13,7 @@ module Ditty
12
13
  require 'ditty/models/role'
13
14
  require 'ditty/models/identity'
14
15
  require 'ditty/models/audit_log'
16
+ require 'ditty/models/user_login_trait'
15
17
  end
16
18
 
17
19
  def self.configure(_container)
@@ -34,7 +36,8 @@ module Ditty
34
36
  '/auth' => ::Ditty::Auth,
35
37
  '/users' => ::Ditty::Users,
36
38
  '/roles' => ::Ditty::Roles,
37
- '/audit-logs' => ::Ditty::AuditLogs
39
+ '/audit-logs' => ::Ditty::AuditLogs,
40
+ '/login-traits' => ::Ditty::UserLoginTraits
38
41
  }
39
42
  end
40
43
 
@@ -65,6 +68,12 @@ module Ditty
65
68
  ::Ditty::Role.find_or_create(name: 'user')
66
69
  end
67
70
  end
71
+
72
+ def self.tasks
73
+ Kernel.load 'ditty/tasks/ditty.rake'
74
+ auth_settings = Ditty::Services::Settings[:authentication] || {}
75
+ Kernel.load 'ditty/tasks/omniauth-ldap.rake' if auth_settings.key?(:ldap)
76
+ end
68
77
  end
69
78
  end
70
79
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'browser/browser'
3
4
  require 'wisper'
4
5
  require 'oga'
5
6
  require 'sinatra/base'
@@ -22,6 +23,7 @@ module Ditty
22
23
  set :root, ENV['APP_ROOT'] || ::File.expand_path(::File.dirname(__FILE__) + '/../../../')
23
24
  set :map_path, nil
24
25
  set :view_location, nil
26
+ set :view_folder, nil
25
27
  set :model_class, nil
26
28
  set :raise_sinatra_param_exceptions, true
27
29
  set track_actions: false
@@ -39,6 +41,10 @@ module Ditty
39
41
  use Rack::NestedParams
40
42
 
41
43
  helpers do
44
+ def logger
45
+ Ditty::Services::Logger.instance
46
+ end
47
+
42
48
  def base_path
43
49
  settings.base_path || "#{settings.map_path}/#{dasherize(view_location)}"
44
50
  end
@@ -46,8 +52,33 @@ module Ditty
46
52
  def view_location
47
53
  return settings.view_location if settings.view_location
48
54
  return underscore(pluralize(demodulize(settings.model_class))) if settings.model_class
55
+
49
56
  underscore(demodulize(self.class))
50
57
  end
58
+
59
+ def browser
60
+ Browser.new(request.user_agent, accept_language: request.env['HTTP_ACCEPT_LANGUAGE'])
61
+ end
62
+
63
+ def config(name, default = '')
64
+ Ditty::Services::Settings[name] || default
65
+ end
66
+ end
67
+
68
+ def view_folders
69
+ folders = ['./views']
70
+ folders << settings.view_folder if settings.view_folder
71
+ folders << Ditty::App.view_folder
72
+ end
73
+
74
+ def find_template(views, name, engine, &block)
75
+ # Backwards compatability
76
+ return super(views, name, engine, &block) if settings.view_folder.nil? && self.class.name.split('::').first != 'Ditty'
77
+
78
+ view_folders.each do |folder|
79
+ super(folder, name, engine, &block) # Root
80
+ end
81
+ raise Ditty::TemplateNotFoundError, "Could not find template `#{name}`"
51
82
  end
52
83
 
53
84
  configure :production do
@@ -77,14 +108,28 @@ module Ditty
77
108
  end
78
109
 
79
110
  error Helpers::NotAuthenticated, ::Pundit::NotAuthorizedError do
80
- respond_to do |format|
81
- status 401
82
- format.html do
83
- flash[:warning] = 'Please log in first.'
84
- redirect with_layout("#{settings.map_path}/auth/login")
111
+ # TODO: Check if this is logged / tracked
112
+ if authenticated?
113
+ respond_to do |format|
114
+ status 403
115
+ format.html do
116
+ flash.now[:danger] = 'Cannot perform that action at the moment.'
117
+ haml :'403', locals: { title: 'Forbidden' }, layout: layout
118
+ end
119
+ format.json do
120
+ json code: 403, errors: ['Forbidden']
121
+ end
85
122
  end
86
- format.json do
87
- json code: 401, errors: ['Not Authenticated']
123
+ else
124
+ respond_to do |format|
125
+ format.html do
126
+ flash[:warning] = 'Please log in first.'
127
+ redirect with_layout("#{settings.map_path}/auth/login")
128
+ end
129
+ format.json do
130
+ status 401
131
+ json code: 401, errors: ['Not Authenticated']
132
+ end
88
133
  end
89
134
  end
90
135
  end
@@ -120,7 +165,7 @@ module Ditty
120
165
  error ::Sequel::ForeignKeyConstraintViolation do
121
166
  error = env['sinatra.error']
122
167
  broadcast(:application_error, error)
123
- ::Ditty::Services::Logger.instance.error error
168
+ logger.error error
124
169
  respond_to do |format|
125
170
  status 400
126
171
  format.html do
@@ -132,10 +177,23 @@ module Ditty
132
177
  end
133
178
  end
134
179
 
180
+ error Ditty::TemplateNotFoundError do
181
+ # TODO: Display a better error message
182
+ error = env['sinatra.error']
183
+ broadcast(:application_error, error)
184
+ logger.error error
185
+ respond_to do |format|
186
+ status 500
187
+ format.html do
188
+ haml :error, locals: { title: 'Template not found', error: error }, layout: layout
189
+ end
190
+ end
191
+ end
192
+
135
193
  error do
136
194
  error = env['sinatra.error']
137
195
  broadcast(:application_error, error)
138
- ::Ditty::Services::Logger.instance.error error
196
+ logger.error error
139
197
  respond_to do |format|
140
198
  status 500
141
199
  format.html do
@@ -148,10 +206,13 @@ module Ditty
148
206
  end
149
207
 
150
208
  before(/.*/) do
151
- ::Ditty::Services::Logger.instance.debug "Running with #{self.class} - #{request.path_info}"
209
+ logger.info "Running with #{self.class} - #{request.path_info}"
152
210
  if request.path =~ /.*\.json\Z/
153
211
  content_type :json
154
212
  request.path_info = request.path_info.gsub(/.json$/, '')
213
+ elsif request.path =~ /.*\.csv\Z/
214
+ content_type :csv
215
+ request.path_info = request.path_info.gsub(/.csv$/, '')
155
216
  elsif request.env['ACCEPT']
156
217
  content_type request.env['ACCEPT']
157
218
  else
@@ -161,6 +222,7 @@ module Ditty
161
222
 
162
223
  after do
163
224
  return if params[:layout].nil?
225
+
164
226
  response.body = response.body.map do |resp|
165
227
  document = Oga.parse_html(resp)
166
228
  document.css('a').each do |elm|
@@ -8,6 +8,7 @@ module Ditty
8
8
  class AuditLogs < Ditty::Component
9
9
  set model_class: AuditLog
10
10
 
11
+ SEARCHABLE = %i[details platform device browser ip_address].freeze
11
12
  FILTERS = [
12
13
  { name: :user, field: 'user.email' },
13
14
  { name: :action }
@@ -23,11 +24,6 @@ module Ditty
23
24
  end
24
25
  end
25
26
 
26
- def find_template(views, name, engine, &block)
27
- super(views, name, engine, &block) # Root
28
- super(::Ditty::App.view_folder, name, engine, &block) # Ditty
29
- end
30
-
31
27
  def list
32
28
  super.order(:created_at).reverse
33
29
  end