ditty 0.4.1 → 0.6.0

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