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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/Readme.md +3 -12
- data/ditty.gemspec +4 -1
- data/exe/ditty +4 -0
- data/lib/ditty/cli.rb +44 -0
- data/lib/ditty/controllers/application.rb +3 -4
- data/lib/ditty/controllers/component.rb +38 -14
- data/lib/ditty/controllers/main.rb +115 -34
- data/lib/ditty/controllers/users.rb +7 -10
- data/lib/ditty/db.rb +6 -1
- data/lib/ditty/emails/base.rb +74 -0
- data/lib/ditty/emails/forgot_password.rb +15 -0
- data/lib/ditty/helpers/authentication.rb +2 -2
- data/lib/ditty/helpers/component.rb +7 -4
- data/lib/ditty/helpers/views.rb +5 -2
- data/lib/ditty/listener.rb +41 -7
- data/lib/ditty/models/user.rb +8 -4
- data/lib/ditty/policies/identity_policy.rb +2 -2
- data/lib/ditty/rake_tasks.rb +6 -5
- data/lib/ditty/services/authentication.rb +55 -0
- data/lib/ditty/services/email.rb +18 -20
- data/lib/ditty/services/logger.rb +10 -8
- data/lib/ditty/services/pagination_wrapper.rb +82 -0
- data/lib/ditty/services/settings.rb +45 -0
- data/lib/ditty/version.rb +1 -1
- data/migrate/20180307_password_reset.rb +10 -0
- data/views/audit_logs/index.haml +1 -1
- data/views/emails/base.haml +2 -0
- data/views/emails/forgot_password.haml +26 -0
- data/views/emails/layouts/action.haml +68 -0
- data/views/emails/layouts/alert.haml +88 -0
- data/views/emails/layouts/billing.haml +108 -0
- data/views/identity/forgot.haml +16 -0
- data/views/identity/login.haml +16 -5
- data/views/identity/register.haml +8 -0
- data/views/identity/reset.haml +20 -0
- data/views/layout.haml +2 -0
- metadata +62 -5
- 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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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['
|
22
|
-
env['
|
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.
|
20
|
-
count == 'all'
|
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 =
|
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']) ||
|
91
|
+
return dataset if ['', nil].include?(params['q']) || search_filters == []
|
89
92
|
dataset.where Sequel.|(*search_filters)
|
90
93
|
end
|
91
94
|
end
|
data/lib/ditty/helpers/views.rb
CHANGED
@@ -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
|
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? :
|
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)),
|
data/lib/ditty/listener.rb
CHANGED
@@ -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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
20
|
-
|
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
|
data/lib/ditty/models/user.rb
CHANGED
@@ -15,11 +15,14 @@ module Ditty
|
|
15
15
|
one_to_many :audit_logs
|
16
16
|
|
17
17
|
def role?(check)
|
18
|
-
|
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
|
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
|
52
|
-
|
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
|
data/lib/ditty/rake_tasks.rb
CHANGED
@@ -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
|