ditty 0.7.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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