ditty 0.4.1 → 0.6.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/Readme.md +3 -12
  4. data/ditty.gemspec +4 -1
  5. data/exe/ditty +4 -0
  6. data/lib/ditty/cli.rb +44 -0
  7. data/lib/ditty/controllers/application.rb +3 -4
  8. data/lib/ditty/controllers/component.rb +38 -14
  9. data/lib/ditty/controllers/main.rb +115 -34
  10. data/lib/ditty/controllers/users.rb +7 -10
  11. data/lib/ditty/db.rb +6 -1
  12. data/lib/ditty/emails/base.rb +74 -0
  13. data/lib/ditty/emails/forgot_password.rb +15 -0
  14. data/lib/ditty/helpers/authentication.rb +2 -2
  15. data/lib/ditty/helpers/component.rb +7 -4
  16. data/lib/ditty/helpers/views.rb +5 -2
  17. data/lib/ditty/listener.rb +41 -7
  18. data/lib/ditty/models/user.rb +8 -4
  19. data/lib/ditty/policies/identity_policy.rb +2 -2
  20. data/lib/ditty/rake_tasks.rb +6 -5
  21. data/lib/ditty/services/authentication.rb +55 -0
  22. data/lib/ditty/services/email.rb +18 -20
  23. data/lib/ditty/services/logger.rb +10 -8
  24. data/lib/ditty/services/pagination_wrapper.rb +82 -0
  25. data/lib/ditty/services/settings.rb +45 -0
  26. data/lib/ditty/version.rb +1 -1
  27. data/migrate/20180307_password_reset.rb +10 -0
  28. data/views/audit_logs/index.haml +1 -1
  29. data/views/emails/base.haml +2 -0
  30. data/views/emails/forgot_password.haml +26 -0
  31. data/views/emails/layouts/action.haml +68 -0
  32. data/views/emails/layouts/alert.haml +88 -0
  33. data/views/emails/layouts/billing.haml +108 -0
  34. data/views/identity/forgot.haml +16 -0
  35. data/views/identity/login.haml +16 -5
  36. data/views/identity/register.haml +8 -0
  37. data/views/identity/reset.haml +20 -0
  38. data/views/layout.haml +2 -0
  39. metadata +62 -5
  40. data/lib/ditty/helpers/wisper.rb +0 -14
@@ -60,7 +60,7 @@ module Ditty
60
60
  user.check_roles
61
61
  end
62
62
 
63
- log_action("#{dehumanized}_create".to_sym) if settings.track_actions
63
+ broadcast(:component_create, target: self)
64
64
  create_response(user)
65
65
  end
66
66
 
@@ -81,7 +81,7 @@ module Ditty
81
81
  entity.check_roles
82
82
  end
83
83
 
84
- log_action("#{dehumanized}_update".to_sym) if settings.track_actions
84
+ broadcast(:component_update, target: self)
85
85
  update_response(entity)
86
86
  end
87
87
 
@@ -93,13 +93,8 @@ module Ditty
93
93
  identity = entity.identity.first
94
94
  identity_params = params['identity']
95
95
 
96
- unless identity_params['password'] == identity_params['password_confirmation']
97
- flash[:warning] = 'Password didn\'t match'
98
- return redirect back
99
- end
100
-
101
96
  unless current_user.super_admin? || identity.authenticate(identity_params['old_password'])
102
- log_action("#{dehumanized}_update_password_failed".to_sym) if settings.track_actions
97
+ broadcast(:identity_update_password_failed, target: self)
103
98
  flash[:danger] = 'Old Password didn\'t match'
104
99
  return redirect back
105
100
  end
@@ -107,12 +102,14 @@ module Ditty
107
102
  values = permitted_attributes(Identity, :create)
108
103
  identity.set values
109
104
  if identity.valid? && identity.save
110
- log_action("#{dehumanized}_update_password".to_sym) if settings.track_actions
105
+ broadcast(:identity_update_password, target: self)
111
106
  flash[:success] = 'Password Updated'
112
107
  redirect back
113
108
  elsif current_user.super_admin? && current_user.id != id.to_i
109
+ broadcast(:identity_update_password_failed, target: self)
114
110
  haml :"#{view_location}/display", locals: { entity: entity, identity: identity, title: heading }
115
111
  else
112
+ broadcast(:identity_update_password_failed, target: self)
116
113
  haml :"#{view_location}/profile", locals: { entity: entity, identity: identity, title: heading }
117
114
  end
118
115
  end
@@ -127,7 +124,7 @@ module Ditty
127
124
  entity.remove_all_roles
128
125
  entity.destroy
129
126
 
130
- log_action("#{dehumanized}_delete".to_sym) if settings.track_actions
127
+ broadcast(:component_delete, target: self)
131
128
  delete_response(entity)
132
129
  end
133
130
 
data/lib/ditty/db.rb CHANGED
@@ -5,12 +5,17 @@ require 'ditty/services/logger'
5
5
  require 'active_support'
6
6
  require 'active_support/core_ext/object/blank'
7
7
 
8
+ pool_timeout = (ENV['DB_POOL_TIMEOUT'] || 5).to_i
9
+
8
10
  if defined? DB
9
11
  Ditty::Services::Logger.instance.warn 'Database connection already set up'
10
12
  elsif ENV['DATABASE_URL'].blank? == false
11
13
  # Delete DATABASE_URL from the environment, so it isn't accidently
12
14
  # passed to subprocesses. DATABASE_URL may contain passwords.
13
- DB = Sequel.connect(ENV['RACK_ENV'] == 'production' ? ENV.delete('DATABASE_URL') : ENV['DATABASE_URL'])
15
+ DB = Sequel.connect(
16
+ ENV['RACK_ENV'] == 'production' ? ENV.delete('DATABASE_URL') : ENV['DATABASE_URL'],
17
+ pool_timeout: pool_timeout
18
+ )
14
19
 
15
20
  DB.sql_log_level = (ENV['SEQUEL_LOGGING_LEVEL'] || :debug).to_sym
16
21
  DB.loggers << Ditty::Services::Logger.instance
@@ -0,0 +1,74 @@
1
+ require 'haml'
2
+ require 'ditty/components/app'
3
+
4
+ module Ditty
5
+ module Emails
6
+ class Base
7
+ attr_accessor :options, :locals, :mail
8
+
9
+ def initialize(options = {})
10
+ @mail = options[:mail] || Mail.new
11
+ @locals = options[:locals] || {}
12
+ @options = base_options.merge options
13
+ end
14
+
15
+ def deliver!(to = nil, locals = {})
16
+ options[:to] = to unless to.nil?
17
+ @locals.merge!(locals)
18
+ %i[to from subject].each do |param|
19
+ mail.send(param, options[param]) if options[param]
20
+ end
21
+ mail.body content
22
+ mail.deliver!
23
+ end
24
+
25
+ def method_missing(method, *args, &block)
26
+ return super unless respond_to_missing?(method)
27
+ mail.send(method, *args, &block)
28
+ end
29
+
30
+ def respond_to_missing?(method, _include_private = false)
31
+ mail.respond_to? method
32
+ end
33
+
34
+ private
35
+
36
+ def content
37
+ result = Haml::Engine.new(content_haml).render(Object.new, locals)
38
+ return result unless options[:layout]
39
+ Haml::Engine.new(layout_haml).render(Object.new, content: result)
40
+ end
41
+
42
+ def content_haml
43
+ read_template(options[:view])
44
+ end
45
+
46
+ def layout_haml
47
+ read_template("layouts/#{options[:layout]}") if options[:layout]
48
+ end
49
+
50
+ def read_template(template)
51
+ File.read(find_template("emails/#{template}"))
52
+ end
53
+
54
+ def base_options
55
+ { subject: '(No Subject)', from: 'no-reply@ditty.io', view: :base }
56
+ end
57
+
58
+ def find_template(file)
59
+ template = File.expand_path("./views/#{file}.haml")
60
+ return template if File.file? template
61
+ template = File.expand_path("./#{file}.haml", App.view_folder)
62
+ return template if File.file? template
63
+ file
64
+ end
65
+
66
+ class << self
67
+ def deliver!(to = nil, options = {})
68
+ locals = options[:locals] || {}
69
+ new(options).deliver!(to, locals)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ditty/emails/base'
4
+ require 'ditty/services/email'
5
+
6
+ module Ditty
7
+ module Emails
8
+ class ForgotPassword < Base
9
+ def initialize(options = {})
10
+ options = { view: :forgot_password, layout: :action, subject: 'Request to reset password' }.merge(options)
11
+ super(options)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -18,8 +18,8 @@ module Ditty
18
18
  end
19
19
 
20
20
  def current_user_id
21
- return env['omniauth.auth'].uid if env['omniauth.auth']
22
- env['rack.session']['user_id'] if env['rack.session']
21
+ return env['rack.session']['user_id'] if env['rack.session']
22
+ env['omniauth.auth'].uid if env['omniauth.auth']
23
23
  end
24
24
 
25
25
  def authenticate
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'active_support'
4
4
  require 'active_support/inflector'
5
+ require 'will_paginate/array'
5
6
 
6
7
  module Ditty
7
8
  module Helpers
@@ -16,13 +17,15 @@ module Ditty
16
17
  count = params['count'] || 10
17
18
  page = params['page'] || 1
18
19
 
19
- ds = dataset.select
20
- count == 'all' ? ds : ds.paginate(page.to_i, count.to_i)
20
+ ds = dataset.respond_to?(:dataset) ? dataset.dataset : dataset
21
+ return ds if count == 'all'
22
+ # 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)
21
24
  end
22
25
 
23
26
  def heading(action = nil)
24
27
  @headings ||= begin
25
- heading = titleize(demodulize(settings.model_class))
28
+ heading = settings.model_class.to_s.demodulize.titleize
26
29
  h = Hash.new(heading)
27
30
  h[:new] = "New #{heading}"
28
31
  h[:list] = pluralize heading
@@ -85,7 +88,7 @@ module Ditty
85
88
  end
86
89
 
87
90
  def search(dataset)
88
- return dataset if ['', nil].include?(params['q']) || searchable_fields == []
91
+ return dataset if ['', nil].include?(params['q']) || search_filters == []
89
92
  dataset.where Sequel.|(*search_filters)
90
93
  end
91
94
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ditty/services/pagination_wrapper'
4
+
3
5
  module Ditty
4
6
  module Helpers
5
7
  module Views
@@ -85,12 +87,13 @@ module Ditty
85
87
  options[:form_verb] ||= :post
86
88
  options[:attributes] ||= {}
87
89
  options[:attributes] = { 'class': 'form-horizontal' }.merge options[:attributes]
88
- options[:url] = options[:form_verb].to_sym == :get ? url : with_layout(url)
90
+ options[:url] = options[:form_verb].to_sym == :get ? url : with_layout(url)
89
91
  haml :'partials/form_tag', locals: options.merge(block: block)
90
92
  end
91
93
 
92
94
  def pagination(list, base_path, qp = {})
93
- return unless list.respond_to? :pagination_record_count
95
+ return unless list.respond_to?(:pagination_record_count) || list.respond_to?(:total_entries)
96
+ list = Ditty::Services::PaginationWrapper.new(list)
94
97
  locals = {
95
98
  first_link: "#{base_path}?" + query_string(qp.merge(page: 1)),
96
99
  next_link: list.last_page? ? '#' : "#{base_path}?" + query_string(qp.merge(page: list.next_page)),
@@ -1,23 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support'
4
+ require 'active_support/inflector'
3
5
  require 'wisper'
4
6
 
5
7
  module Ditty
6
8
  class Listener
9
+ EVENTS = %i[
10
+ component_list component_create component_read component_update component_delete
11
+ user_register user_login user_logout user_failed_login
12
+ identity_update_password identity_update_password_failed
13
+ ].freeze
14
+
7
15
  def initialize
8
16
  @mutex = Mutex.new
9
17
  end
10
18
 
11
19
  def method_missing(method, *args)
12
- vals = { action: method }
13
- return unless args[0].is_a? Hash
14
- vals[:user] = args[0][:user] if args[0] && args[0].key?(:user)
15
- vals[:details] = args[0][:details] if args[0] && args[0].key?(:details)
16
- @mutex.synchronize { Ditty::AuditLog.create vals }
20
+ return unless args[0].is_a?(Hash) && args[0][:target] && args[0][:target].settings.track_actions
21
+
22
+ log_action({
23
+ user: args[0][:target].current_user,
24
+ action: action_from(args[0][:target], method),
25
+ details: args[0][:details]
26
+ }.merge(args[0][:values] || {}))
27
+ end
28
+
29
+ def respond_to_missing?(method, _include_private = false)
30
+ EVENTS.include? method
31
+ end
32
+
33
+ def user_register(event)
34
+ user = event[:values][:user]
35
+ log_action({
36
+ user: user,
37
+ action: action_from(event[:target], :user_register),
38
+ details: event[:details]
39
+ }.merge(event[:values] || {}))
40
+
41
+ # Create the SA user if none is present
42
+ sa = Role.find_or_create(name: 'super_admin')
43
+ return if User.where(roles: sa).count.positive?
44
+ user.add_role sa
45
+ end
46
+
47
+ def action_from(target, method)
48
+ return method unless method.to_s.start_with? 'component_'
49
+ target.class.to_s.demodulize.underscore + '_' + method.to_s.gsub(/^component_/, '')
17
50
  end
18
51
 
19
- def respond_to_missing?(_method, _include_private = false)
20
- true
52
+ def log_action(values)
53
+ values[:user] ||= values[:target].current_user if values[:target]
54
+ @mutex.synchronize { Ditty::AuditLog.create values }
21
55
  end
22
56
  end
23
57
  end
@@ -15,11 +15,14 @@ module Ditty
15
15
  one_to_many :audit_logs
16
16
 
17
17
  def role?(check)
18
- !roles_dataset.first(name: check).nil?
18
+ @roles ||= Hash.new do |h,k|
19
+ h[k] = !roles_dataset.first(name: k).nil?
20
+ end
21
+ @roles[check]
19
22
  end
20
23
 
21
24
  def method_missing(method_sym, *arguments, &block)
22
- if method_sym.to_s[-1] == '?'
25
+ if respond_to_missing?(method_sym)
23
26
  role?(method_sym[0..-2])
24
27
  else
25
28
  super
@@ -48,8 +51,9 @@ module Ditty
48
51
  end
49
52
 
50
53
  def check_roles
51
- return if role?('anonymous')
52
- add_role Role.find_or_create(name: 'user') unless role?('user')
54
+ return if roles_dataset.first(name: 'anonymous')
55
+ return if roles_dataset.first(name: 'user')
56
+ add_role Role.find_or_create(name: 'user')
53
57
  end
54
58
 
55
59
  def index_prefix
@@ -15,9 +15,9 @@ module Ditty
15
15
  class Scope < ApplicationPolicy::Scope
16
16
  def resolve
17
17
  if user.super_admin?
18
- scope.all
18
+ scope
19
19
  else
20
- []
20
+ scope.where(id: -1)
21
21
  end
22
22
  end
23
23
  end
@@ -32,6 +32,7 @@ module Ditty
32
32
  puts 'Preparing the Ditty public folder'
33
33
  Dir.mkdir 'public' unless File.exist?('public')
34
34
  ::Ditty::Components.public_folder.each do |path|
35
+ puts "Checking #{path}"
35
36
  FileUtils.cp_r "#{path}/.", 'public' unless File.expand_path("#{path}/.").eql? File.expand_path('public')
36
37
  end
37
38
 
@@ -41,7 +42,7 @@ module Ditty
41
42
  FileUtils.cp_r "#{path}/.", 'migrations' unless File.expand_path("#{path}/.").eql? File.expand_path('migrations')
42
43
  end
43
44
  puts 'Migrations added:'
44
- Dir.foreach('migrations').sort.each { |x| puts x if File.file?("migrations/#{x}") }
45
+ Dir.foreach('migrations').sort.each { |x| puts x if File.file?("migrations/#{x}") && x[-3..-1] == '.rb' }
45
46
  end
46
47
 
47
48
  desc 'Migrate Ditty database to latest version'
@@ -57,7 +58,7 @@ module Ditty
57
58
 
58
59
  desc 'Check if the migration is current'
59
60
  task :check do
60
- ::DB.loggers << Logger.new($stdout)
61
+ ::DB.loggers << Logger.new($stdout) if ::DB.loggers.count.zero?
61
62
  puts 'Running Ditty Migrations check'
62
63
  ::Sequel.extension :migration
63
64
  begin
@@ -70,7 +71,7 @@ module Ditty
70
71
 
71
72
  desc 'Migrate Ditty database to latest version'
72
73
  task :up do
73
- ::DB.loggers << Logger.new($stdout)
74
+ ::DB.loggers << Logger.new($stdout) if ::DB.loggers.count.zero?
74
75
  puts 'Running Ditty Migrations up'
75
76
  ::Sequel.extension :migration
76
77
  ::Sequel::Migrator.apply(::DB, folder)
@@ -78,7 +79,7 @@ module Ditty
78
79
 
79
80
  desc 'Remove the whole Ditty database. You WILL lose data'
80
81
  task :down do
81
- ::DB.loggers << Logger.new($stdout)
82
+ ::DB.loggers << Logger.new($stdout) if ::DB.loggers.count.zero?
82
83
  puts 'Running Ditty Migrations down'
83
84
  ::Sequel.extension :migration
84
85
  ::Sequel::Migrator.apply(::DB, folder, 0)
@@ -86,7 +87,7 @@ module Ditty
86
87
 
87
88
  desc 'Reset the Ditty database. You WILL lose data'
88
89
  task :bounce do
89
- ::DB.loggers << Logger.new($stdout)
90
+ ::DB.loggers << Logger.new($stdout) if ::DB.loggers.count.zero?
90
91
  puts 'Running Ditty Migrations bounce'
91
92
  ::Sequel.extension :migration
92
93
  ::Sequel::Migrator.apply(::DB, folder, 0)
@@ -0,0 +1,55 @@
1
+ require 'ditty/models/identity'
2
+ require 'ditty/controllers/main'
3
+ require 'ditty/services/settings'
4
+ require 'ditty/services/logger'
5
+
6
+ require 'omniauth'
7
+ OmniAuth.config.logger = Ditty::Services::Logger.instance
8
+ OmniAuth.config.on_failure = proc { |env|
9
+ OmniAuth::FailureEndpoint.new(env).redirect_to_failure
10
+ }
11
+
12
+ module Ditty
13
+ module Services
14
+ module Authentication
15
+ class << self
16
+ def providers
17
+ config.keys
18
+ end
19
+
20
+ def setup
21
+ providers.each do |provider|
22
+ require "omniauth/#{provider}"
23
+ end
24
+ end
25
+
26
+ def config
27
+ default.merge Ditty::Services::Settings.values(:authentication) || {}
28
+ end
29
+
30
+ def provides?(provider)
31
+ providers.include? provider.to_sym
32
+ end
33
+
34
+ def default
35
+ {
36
+ identity: {
37
+ arguments: [
38
+ {
39
+ fields: [:username],
40
+ callback_path: '/auth/identity/callback',
41
+ model: Ditty::Identity,
42
+ on_login: Ditty::Main,
43
+ on_registration: Ditty::Main,
44
+ locate_conditions: ->(req) { { username: req['username'] } }
45
+ }
46
+ ],
47
+ }
48
+ }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ Ditty::Services::Authentication.setup