ditty 0.2.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 +7 -0
- data/.gitignore +14 -0
- data/.pryrc +6 -0
- data/.rspec +2 -0
- data/.rubocop.yml +15 -0
- data/.travis.yml +15 -0
- data/Gemfile.ci +19 -0
- data/License.txt +7 -0
- data/Rakefile +10 -0
- data/Readme.md +67 -0
- data/config.ru +33 -0
- data/ditty.gemspec +46 -0
- data/lib/ditty/components/app.rb +78 -0
- data/lib/ditty/controllers/application.rb +79 -0
- data/lib/ditty/controllers/audit_logs.rb +44 -0
- data/lib/ditty/controllers/component.rb +161 -0
- data/lib/ditty/controllers/main.rb +86 -0
- data/lib/ditty/controllers/roles.rb +16 -0
- data/lib/ditty/controllers/users.rb +183 -0
- data/lib/ditty/db.rb +12 -0
- data/lib/ditty/helpers/authentication.rb +58 -0
- data/lib/ditty/helpers/component.rb +63 -0
- data/lib/ditty/helpers/pundit.rb +34 -0
- data/lib/ditty/helpers/views.rb +50 -0
- data/lib/ditty/helpers/wisper.rb +14 -0
- data/lib/ditty/listener.rb +23 -0
- data/lib/ditty/models/audit_log.rb +14 -0
- data/lib/ditty/models/base.rb +7 -0
- data/lib/ditty/models/identity.rb +70 -0
- data/lib/ditty/models/role.rb +16 -0
- data/lib/ditty/models/user.rb +63 -0
- data/lib/ditty/policies/application_policy.rb +21 -0
- data/lib/ditty/policies/audit_log_policy.rb +41 -0
- data/lib/ditty/policies/identity_policy.rb +25 -0
- data/lib/ditty/policies/role_policy.rb +41 -0
- data/lib/ditty/policies/user_policy.rb +47 -0
- data/lib/ditty/rake_tasks.rb +85 -0
- data/lib/ditty/seed.rb +1 -0
- data/lib/ditty/services/logger.rb +48 -0
- data/lib/ditty/version.rb +5 -0
- data/lib/ditty.rb +142 -0
- data/migrate/20170207_base_tables.rb +40 -0
- data/migrate/20170208_audit_log.rb +12 -0
- data/migrate/20170416_audit_log_details.rb +9 -0
- data/public/browserconfig.xml +9 -0
- data/public/images/apple-icon.png +0 -0
- data/public/images/favicon-16x16.png +0 -0
- data/public/images/favicon-32x32.png +0 -0
- data/public/images/launcher-icon-1x.png +0 -0
- data/public/images/launcher-icon-2x.png +0 -0
- data/public/images/launcher-icon-4x.png +0 -0
- data/public/images/mstile-150x150.png +0 -0
- data/public/images/safari-pinned-tab.svg +43 -0
- data/public/manifest.json +25 -0
- data/views/404.haml +7 -0
- data/views/audit_logs/index.haml +30 -0
- data/views/error.haml +4 -0
- data/views/identity/login.haml +19 -0
- data/views/identity/register.haml +14 -0
- data/views/index.haml +1 -0
- data/views/layout.haml +55 -0
- data/views/partials/delete_form.haml +4 -0
- data/views/partials/footer.haml +5 -0
- data/views/partials/form_control.haml +20 -0
- data/views/partials/navbar.haml +24 -0
- data/views/partials/notifications.haml +24 -0
- data/views/partials/pager.haml +14 -0
- data/views/partials/sidebar.haml +35 -0
- data/views/roles/display.haml +18 -0
- data/views/roles/edit.haml +11 -0
- data/views/roles/form.haml +1 -0
- data/views/roles/index.haml +22 -0
- data/views/roles/new.haml +10 -0
- data/views/users/display.haml +50 -0
- data/views/users/edit.haml +11 -0
- data/views/users/identity.haml +3 -0
- data/views/users/index.haml +23 -0
- data/views/users/new.haml +11 -0
- data/views/users/profile.haml +39 -0
- data/views/users/user.haml +3 -0
- metadata +431 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ditty/controllers/application'
|
4
|
+
|
5
|
+
module Ditty
|
6
|
+
class Main < Application
|
7
|
+
def find_template(views, name, engine, &block)
|
8
|
+
super(views, name, engine, &block) # Root
|
9
|
+
super(::Ditty::App.view_folder, name, engine, &block) # Basic Plugin
|
10
|
+
end
|
11
|
+
|
12
|
+
# Home Page
|
13
|
+
get '/' do
|
14
|
+
authenticate!
|
15
|
+
haml :index, locals: { title: 'Home' }
|
16
|
+
end
|
17
|
+
|
18
|
+
# OmniAuth Identity Stuff
|
19
|
+
# Log in Page
|
20
|
+
get '/auth/identity' do
|
21
|
+
haml :'identity/login', locals: { title: 'Log In' }
|
22
|
+
end
|
23
|
+
|
24
|
+
get '/auth/failure' do
|
25
|
+
broadcast(:identity_failed_login)
|
26
|
+
flash[:warning] = 'Invalid credentials. Please try again.'
|
27
|
+
redirect "#{settings.map_path}/auth/identity"
|
28
|
+
end
|
29
|
+
|
30
|
+
post '/auth/identity/callback' do
|
31
|
+
if env['omniauth.auth']
|
32
|
+
# Successful Login
|
33
|
+
user = User.find(email: env['omniauth.auth']['info']['email'])
|
34
|
+
self.current_user = user
|
35
|
+
log_action(:identity_login, user: user)
|
36
|
+
flash[:success] = 'Logged In'
|
37
|
+
redirect "#{settings.map_path}/"
|
38
|
+
else
|
39
|
+
# Failed Login
|
40
|
+
broadcast(:identity_failed_login)
|
41
|
+
flash[:warning] = 'Invalid credentials. Please try again.'
|
42
|
+
redirect "#{settings.map_path}/auth/identity"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Register Page
|
47
|
+
get '/auth/identity/register' do
|
48
|
+
identity = Identity.new
|
49
|
+
haml :'identity/register', locals: { title: 'Register', identity: identity }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Register Action
|
53
|
+
post '/auth/identity/new' do
|
54
|
+
identity = Identity.new(params['identity'])
|
55
|
+
if identity.valid? && identity.save
|
56
|
+
user = User.find_or_create(email: identity.username)
|
57
|
+
user.add_identity identity
|
58
|
+
|
59
|
+
# Create the SA user if none is present
|
60
|
+
sa = Role.find_or_create(name: 'super_admin')
|
61
|
+
user.add_role sa if User.where(roles: sa).count == 0
|
62
|
+
|
63
|
+
log_action(:identity_register, user: user)
|
64
|
+
flash[:info] = 'Successfully Registered. Please log in'
|
65
|
+
redirect "#{settings.map_path}/auth/identity"
|
66
|
+
else
|
67
|
+
flash.now[:warning] = 'Could not complete the registration. Please try again.'
|
68
|
+
haml :'identity/register', locals: { identity: identity }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Logout Action
|
73
|
+
delete '/auth/identity' do
|
74
|
+
log_action(:identity_logout)
|
75
|
+
logout
|
76
|
+
flash[:info] = 'Logged Out'
|
77
|
+
|
78
|
+
redirect "#{settings.map_path}/"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Unauthenticated
|
82
|
+
get '/unauthenticated' do
|
83
|
+
redirect "#{settings.map_path}/auth/identity"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ditty/controllers/component'
|
4
|
+
require 'ditty/models/role'
|
5
|
+
require 'ditty/policies/role_policy'
|
6
|
+
|
7
|
+
module Ditty
|
8
|
+
class Roles < Ditty::Component
|
9
|
+
set model_class: Role
|
10
|
+
|
11
|
+
def find_template(views, name, engine, &block)
|
12
|
+
super(views, name, engine, &block) # Root
|
13
|
+
super(::Ditty::App.view_folder, name, engine, &block) # Ditty
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ditty/controllers/component'
|
4
|
+
require 'ditty/models/user'
|
5
|
+
require 'ditty/policies/user_policy'
|
6
|
+
require 'ditty/models/identity'
|
7
|
+
require 'ditty/policies/identity_policy'
|
8
|
+
|
9
|
+
module Ditty
|
10
|
+
class Users < Ditty::Component
|
11
|
+
set model_class: User
|
12
|
+
set track_actions: true
|
13
|
+
|
14
|
+
def find_template(views, name, engine, &block)
|
15
|
+
super(views, name, engine, &block) # Root
|
16
|
+
super(::Ditty::App.view_folder, name, engine, &block) # Ditty
|
17
|
+
end
|
18
|
+
|
19
|
+
# New
|
20
|
+
get '/new' do
|
21
|
+
authorize settings.model_class, :create
|
22
|
+
|
23
|
+
locals = {
|
24
|
+
title: heading(:new),
|
25
|
+
entity: User.new,
|
26
|
+
identity: Identity.new
|
27
|
+
}
|
28
|
+
haml :"#{view_location}/new", locals: locals
|
29
|
+
end
|
30
|
+
|
31
|
+
# Create
|
32
|
+
post '/' do
|
33
|
+
authorize settings.model_class, :create
|
34
|
+
|
35
|
+
locals = { title: heading(:new) }
|
36
|
+
|
37
|
+
user_params = permitted_attributes(User, :create)
|
38
|
+
identity_params = permitted_attributes(Identity, :create)
|
39
|
+
user_params['email'] = identity_params['username']
|
40
|
+
roles = user_params.delete('role_id')
|
41
|
+
|
42
|
+
user = locals[:user] = User.new(user_params)
|
43
|
+
identity = locals[:identity] = Identity.new(identity_params)
|
44
|
+
|
45
|
+
if identity.valid? && user.valid?
|
46
|
+
DB.transaction(isolation: :serializable) do
|
47
|
+
identity.save
|
48
|
+
user.save
|
49
|
+
user.add_identity identity
|
50
|
+
if roles
|
51
|
+
roles.each do |role_id|
|
52
|
+
user.add_role(role_id) unless user.roles.map(&:id).include? role_id.to_i
|
53
|
+
end
|
54
|
+
end
|
55
|
+
user.check_roles
|
56
|
+
end
|
57
|
+
|
58
|
+
log_action("#{dehumanized}_create".to_sym) if settings.track_actions
|
59
|
+
respond_to do |format|
|
60
|
+
format.html do
|
61
|
+
flash[:success] = 'User created'
|
62
|
+
redirect "/users/#{user.id}"
|
63
|
+
end
|
64
|
+
format.json do
|
65
|
+
headers 'Content-Type' => 'application/json'
|
66
|
+
redirect "/users/#{user.id}", 201
|
67
|
+
end
|
68
|
+
end
|
69
|
+
else
|
70
|
+
respond_to do |format|
|
71
|
+
format.html do
|
72
|
+
flash.now[:danger] = 'Could not create the user'
|
73
|
+
locals[:entity] = user
|
74
|
+
locals[:identity] = identity
|
75
|
+
haml :"#{view_location}/new", locals: locals
|
76
|
+
end
|
77
|
+
format.json do
|
78
|
+
headers \
|
79
|
+
'Content-Type' => 'application/json',
|
80
|
+
'Content-Location' => "#{view_location}/new"
|
81
|
+
body ''
|
82
|
+
status 402
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Update
|
89
|
+
put '/:id' do |id|
|
90
|
+
entity = dataset[id.to_i]
|
91
|
+
halt 404 unless entity
|
92
|
+
authorize entity, :update
|
93
|
+
|
94
|
+
values = permitted_attributes(settings.model_class, :update)
|
95
|
+
roles = values.delete('role_id')
|
96
|
+
entity.set values
|
97
|
+
if entity.valid? && entity.save
|
98
|
+
entity.remove_all_roles
|
99
|
+
roles.each { |role_id| entity.add_role(role_id) } if roles
|
100
|
+
entity.check_roles
|
101
|
+
log_action("#{dehumanized}_update".to_sym) if settings.track_actions
|
102
|
+
respond_to do |format|
|
103
|
+
format.html do
|
104
|
+
flash[:success] = "#{heading} Updated"
|
105
|
+
redirect "/users/#{entity.id}"
|
106
|
+
end
|
107
|
+
format.json do
|
108
|
+
content_type 'application/json'
|
109
|
+
headers 'Location' => "/users/#{entity.id}"
|
110
|
+
body entity.to_hash.to_json
|
111
|
+
status 200
|
112
|
+
end
|
113
|
+
end
|
114
|
+
else
|
115
|
+
haml :"#{view_location}/edit", locals: { entity: entity, title: heading(:edit) }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
put '/:id/identity' do |id|
|
120
|
+
entity = dataset[id.to_i]
|
121
|
+
halt 404 unless entity
|
122
|
+
authorize entity, :update
|
123
|
+
|
124
|
+
identity = entity.identity.first
|
125
|
+
identity_params = params['identity']
|
126
|
+
|
127
|
+
unless identity_params['password'] == identity_params['password_confirmation']
|
128
|
+
flash[:warning] = 'Password didn\'t match'
|
129
|
+
return redirect back
|
130
|
+
end
|
131
|
+
|
132
|
+
unless current_user.super_admin? || identity.authenticate(identity_params['old_password'])
|
133
|
+
log_action("#{dehumanized}_update_password_failed".to_sym) if settings.track_actions
|
134
|
+
flash[:danger] = 'Old Password didn\'t match'
|
135
|
+
return redirect back
|
136
|
+
end
|
137
|
+
|
138
|
+
values = permitted_attributes(Identity, :create)
|
139
|
+
identity.set values
|
140
|
+
if identity.valid? && identity.save
|
141
|
+
log_action("#{dehumanized}_update_password".to_sym) if settings.track_actions
|
142
|
+
flash[:success] = 'Password Updated'
|
143
|
+
redirect "#{base_path}/#{entity.id}"
|
144
|
+
elsif current_user.super_admin?
|
145
|
+
haml :"#{view_location}/display", locals: { entity: entity, identity: identity, title: heading }
|
146
|
+
else
|
147
|
+
haml :"#{view_location}/profile", locals: { entity: entity, identity: identity, title: heading }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Delete
|
152
|
+
delete '/:id', provides: %i[html json] do |id|
|
153
|
+
entity = dataset[id.to_i]
|
154
|
+
halt 404 unless entity
|
155
|
+
authorize entity, :delete
|
156
|
+
|
157
|
+
entity.remove_all_identity
|
158
|
+
entity.remove_all_roles
|
159
|
+
entity.destroy
|
160
|
+
|
161
|
+
log_action("#{dehumanized}_delete".to_sym) if settings.track_actions
|
162
|
+
respond_to do |format|
|
163
|
+
format.html do
|
164
|
+
flash[:success] = "#{heading} Deleted"
|
165
|
+
redirect '/users'
|
166
|
+
end
|
167
|
+
format.json do
|
168
|
+
content_type 'application/json'
|
169
|
+
headers 'Location' => '/users'
|
170
|
+
status 204
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Profile
|
176
|
+
get '/profile' do
|
177
|
+
entity = current_user
|
178
|
+
authorize entity, :read
|
179
|
+
|
180
|
+
haml :"#{view_location}/profile", locals: { entity: entity, identity: entity.identity.first, title: 'My Account' }
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
data/lib/ditty/db.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sequel'
|
4
|
+
require 'ditty/services/logger'
|
5
|
+
|
6
|
+
# Delete DATABASE_URL from the environment, so it isn't accidently
|
7
|
+
# passed to subprocesses. DATABASE_URL may contain passwords.
|
8
|
+
DB = Sequel.connect(ENV['RACK_ENV'] == 'production' ? ENV.delete('DATABASE_URL') : ENV['DATABASE_URL'])
|
9
|
+
|
10
|
+
log_level = (ENV['SEQUEL_LOGGING_LEVEL'] || :debug).to_sym
|
11
|
+
DB.sql_log_level = log_level
|
12
|
+
DB.loggers << Ditty::Services::Logger.instance
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ditty
|
4
|
+
module Helpers
|
5
|
+
module Authentication
|
6
|
+
def current_user
|
7
|
+
if env['rack.session'].nil? || env['rack.session']['user_id'].nil?
|
8
|
+
self.current_user = anonymous_user
|
9
|
+
end
|
10
|
+
@users ||= Hash.new { |h, k| h[k] = User[k] }
|
11
|
+
@users[env['rack.session']['user_id']]
|
12
|
+
end
|
13
|
+
|
14
|
+
def current_user=(user)
|
15
|
+
env['rack.session'] = {} if env['rack.session'].nil?
|
16
|
+
env['rack.session']['user_id'] = user.id if user
|
17
|
+
end
|
18
|
+
|
19
|
+
def authenticate
|
20
|
+
authenticated?
|
21
|
+
end
|
22
|
+
|
23
|
+
def authenticated?
|
24
|
+
current_user && !current_user.role?('anonymous')
|
25
|
+
end
|
26
|
+
|
27
|
+
def authenticate!
|
28
|
+
raise NotAuthenticated unless authenticated?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def logout
|
33
|
+
env['rack.session'].delete('user_id')
|
34
|
+
end
|
35
|
+
|
36
|
+
def check_basic(request)
|
37
|
+
auth = Rack::Auth::Basic::Request.new(request.env)
|
38
|
+
return false unless auth.provided? && auth.basic?
|
39
|
+
|
40
|
+
identity = ::Ditty::Identity.find(username: auth.credentials[0])
|
41
|
+
identity ||= ::Ditty::Identity.find(username: CGI.unescape(auth.credentials[0]))
|
42
|
+
return false unless identity
|
43
|
+
self.current_user = identity.user if identity.authenticate(auth.credentials[1])
|
44
|
+
end
|
45
|
+
|
46
|
+
def anonymous_user
|
47
|
+
return @anonymous_user if defined? @anonymous_user
|
48
|
+
@anonymous_user ||= begin
|
49
|
+
role = ::Ditty::Role.where(name: 'anonymous').first
|
50
|
+
::Ditty::User.where(roles: role).first unless role.nil?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class NotAuthenticated < StandardError
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/inflector'
|
5
|
+
|
6
|
+
module Ditty
|
7
|
+
module Helpers
|
8
|
+
module Component
|
9
|
+
include ActiveSupport::Inflector
|
10
|
+
|
11
|
+
def dataset
|
12
|
+
filtered(policy_scope(settings.model_class))
|
13
|
+
end
|
14
|
+
|
15
|
+
def list
|
16
|
+
params['count'] ||= 10
|
17
|
+
params['page'] ||= 1
|
18
|
+
|
19
|
+
dataset.select.paginate(params['page'].to_i, params['count'].to_i)
|
20
|
+
end
|
21
|
+
|
22
|
+
def heading(action = nil)
|
23
|
+
@headings ||= begin
|
24
|
+
heading = titleize(demodulize(settings.model_class))
|
25
|
+
h = Hash.new(heading)
|
26
|
+
h[:new] = "New #{heading}"
|
27
|
+
h[:list] = pluralize heading
|
28
|
+
h[:edit] = "Edit #{heading}"
|
29
|
+
h
|
30
|
+
end
|
31
|
+
@headings[action]
|
32
|
+
end
|
33
|
+
|
34
|
+
def dehumanized
|
35
|
+
settings.dehumanized || underscore(heading)
|
36
|
+
end
|
37
|
+
|
38
|
+
def base_path
|
39
|
+
settings.base_path || "#{settings.map_path}/#{dasherize(view_location)}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def filters
|
43
|
+
self.class.const_defined?('FILTERS') ? self.class::FILTERS : []
|
44
|
+
end
|
45
|
+
|
46
|
+
def filtered(dataset)
|
47
|
+
filters.each do |filter|
|
48
|
+
next unless params[filter[:name].to_s]
|
49
|
+
filter[:field] ||= filter[:name]
|
50
|
+
dataset = apply_filter(dataset, filter)
|
51
|
+
end
|
52
|
+
dataset
|
53
|
+
end
|
54
|
+
|
55
|
+
def apply_filter(dataset, filter)
|
56
|
+
value = params[filter[:name].to_s]
|
57
|
+
return dataset if value == '' || value.nil?
|
58
|
+
value = value.send(filter[:modifier]) if filter[:modifier]
|
59
|
+
dataset.where(filter[:field].to_sym => value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pundit'
|
4
|
+
|
5
|
+
module Ditty
|
6
|
+
module Helpers
|
7
|
+
module Pundit
|
8
|
+
include ::Pundit
|
9
|
+
|
10
|
+
def authorize(record, query)
|
11
|
+
query = :"#{query}?" unless query[-1] == '?'
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def permitted_attributes(record, action)
|
16
|
+
param_key = PolicyFinder.new(record).param_key
|
17
|
+
policy = policy(record)
|
18
|
+
method_name = if policy.respond_to?("permitted_attributes_for_#{action}")
|
19
|
+
"permitted_attributes_for_#{action}"
|
20
|
+
else
|
21
|
+
'permitted_attributes'
|
22
|
+
end
|
23
|
+
|
24
|
+
request.params.fetch(param_key, {}).select do |key, _value|
|
25
|
+
policy.public_send(method_name).include? key.to_sym
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def pundit_user
|
30
|
+
current_user
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ditty
|
4
|
+
module Helpers
|
5
|
+
module Views
|
6
|
+
def form_control(name, model, opts = {})
|
7
|
+
label = opts.delete(:label) || name.to_s.titlecase
|
8
|
+
klass = opts.delete(:class) || 'form-control' unless opts[:type] == 'file'
|
9
|
+
group = opts.delete(:group) || model.class.to_s.demodulize.underscore
|
10
|
+
field = opts.delete(:field) || name
|
11
|
+
default = opts.delete(:default) || nil
|
12
|
+
|
13
|
+
attributes = { type: 'text', id: name, name: "#{group}[#{name}]", class: klass }.merge(opts)
|
14
|
+
haml :'partials/form_control', locals: {
|
15
|
+
model: model,
|
16
|
+
label: label,
|
17
|
+
attributes: attributes,
|
18
|
+
name: name,
|
19
|
+
group: group,
|
20
|
+
field: field,
|
21
|
+
default: default
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def flash_messages(key = :flash)
|
26
|
+
return '' if flash(key).empty?
|
27
|
+
id = (key == :flash ? 'flash' : "flash_#{key}")
|
28
|
+
messages = flash(key).collect do |message|
|
29
|
+
" <div class='alert alert-#{message[0]} alert-dismissable' role='alert'>#{message[1]}</div>\n"
|
30
|
+
end
|
31
|
+
"<div id='#{id}'>\n" + messages.join + '</div>'
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete_form(entity, label = 'Delete')
|
35
|
+
locals = { delete_label: label, entity: entity }
|
36
|
+
haml :'partials/delete_form', locals: locals
|
37
|
+
end
|
38
|
+
|
39
|
+
def pagination(list, base_path)
|
40
|
+
locals = {
|
41
|
+
next_link: list.last_page? ? '#' : "#{base_path}?page=#{list.next_page}&count=#{list.page_size}",
|
42
|
+
prev_link: list.first_page? ? '#' : "#{base_path}?page=#{list.prev_page}&count=#{list.page_size}",
|
43
|
+
base_path: base_path,
|
44
|
+
list: list
|
45
|
+
}
|
46
|
+
haml :'partials/pager', locals: locals
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'wisper'
|
2
|
+
|
3
|
+
module Ditty
|
4
|
+
class Listener
|
5
|
+
def initialize
|
6
|
+
@mutex = Mutex.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def method_missing(method, *args)
|
10
|
+
vals = { action: method }
|
11
|
+
return unless args[0].is_a? Hash
|
12
|
+
vals[:user] = args[0][:user] if args[0] && args[0].key?(:user)
|
13
|
+
vals[:details] = args[0][:details] if args[0] && args[0].key?(:details)
|
14
|
+
@mutex.synchronize { AuditLog.create vals }
|
15
|
+
end
|
16
|
+
|
17
|
+
def respond_to_missing?(_method, _include_private = false)
|
18
|
+
true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Wisper.subscribe(Ditty::Listener.new)
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bcrypt'
|
4
|
+
require 'ditty/models/base'
|
5
|
+
require 'omniauth-identity'
|
6
|
+
require 'active_support'
|
7
|
+
require 'active_support/core_ext/object/blank'
|
8
|
+
|
9
|
+
module Ditty
|
10
|
+
class Identity < Sequel::Model
|
11
|
+
include ::Ditty::Base
|
12
|
+
many_to_one :user
|
13
|
+
|
14
|
+
attr_accessor :password, :password_confirmation
|
15
|
+
|
16
|
+
# OmniAuth Related
|
17
|
+
include OmniAuth::Identity::Model
|
18
|
+
|
19
|
+
def self.locate(conditions)
|
20
|
+
where(conditions).first
|
21
|
+
end
|
22
|
+
|
23
|
+
def authenticate(unencrypted)
|
24
|
+
self if ::BCrypt::Password.new(crypted_password) == unencrypted
|
25
|
+
end
|
26
|
+
|
27
|
+
def persisted?
|
28
|
+
!new? && @destroyed != true
|
29
|
+
end
|
30
|
+
|
31
|
+
# Return whatever we want to pass to the omniauth hash here
|
32
|
+
def info
|
33
|
+
{
|
34
|
+
email: username
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
# Validation
|
39
|
+
def validate
|
40
|
+
validates_presence :username
|
41
|
+
unless username.blank?
|
42
|
+
validates_unique :username
|
43
|
+
validates_format(/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :username)
|
44
|
+
end
|
45
|
+
|
46
|
+
if password_required
|
47
|
+
validates_presence :password
|
48
|
+
validates_presence :password_confirmation
|
49
|
+
validates_min_length 8, :password
|
50
|
+
end
|
51
|
+
|
52
|
+
errors.add(:password_confirmation, 'must match password') if !password.blank? && password != password_confirmation
|
53
|
+
end
|
54
|
+
|
55
|
+
# Callbacks
|
56
|
+
def before_save
|
57
|
+
encrypt_password unless password == '' || password.nil?
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def encrypt_password
|
63
|
+
self.crypted_password = ::BCrypt::Password.create(password)
|
64
|
+
end
|
65
|
+
|
66
|
+
def password_required
|
67
|
+
crypted_password.blank? || !password.blank?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ditty/models/base'
|
4
|
+
|
5
|
+
module Ditty
|
6
|
+
class Role < Sequel::Model
|
7
|
+
include ::Ditty::Base
|
8
|
+
|
9
|
+
many_to_many :users
|
10
|
+
|
11
|
+
def validate
|
12
|
+
validates_presence [:name]
|
13
|
+
validates_unique [:name]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|