ditty 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.pryrc +6 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +15 -0
  6. data/.travis.yml +15 -0
  7. data/Gemfile.ci +19 -0
  8. data/License.txt +7 -0
  9. data/Rakefile +10 -0
  10. data/Readme.md +67 -0
  11. data/config.ru +33 -0
  12. data/ditty.gemspec +46 -0
  13. data/lib/ditty/components/app.rb +78 -0
  14. data/lib/ditty/controllers/application.rb +79 -0
  15. data/lib/ditty/controllers/audit_logs.rb +44 -0
  16. data/lib/ditty/controllers/component.rb +161 -0
  17. data/lib/ditty/controllers/main.rb +86 -0
  18. data/lib/ditty/controllers/roles.rb +16 -0
  19. data/lib/ditty/controllers/users.rb +183 -0
  20. data/lib/ditty/db.rb +12 -0
  21. data/lib/ditty/helpers/authentication.rb +58 -0
  22. data/lib/ditty/helpers/component.rb +63 -0
  23. data/lib/ditty/helpers/pundit.rb +34 -0
  24. data/lib/ditty/helpers/views.rb +50 -0
  25. data/lib/ditty/helpers/wisper.rb +14 -0
  26. data/lib/ditty/listener.rb +23 -0
  27. data/lib/ditty/models/audit_log.rb +14 -0
  28. data/lib/ditty/models/base.rb +7 -0
  29. data/lib/ditty/models/identity.rb +70 -0
  30. data/lib/ditty/models/role.rb +16 -0
  31. data/lib/ditty/models/user.rb +63 -0
  32. data/lib/ditty/policies/application_policy.rb +21 -0
  33. data/lib/ditty/policies/audit_log_policy.rb +41 -0
  34. data/lib/ditty/policies/identity_policy.rb +25 -0
  35. data/lib/ditty/policies/role_policy.rb +41 -0
  36. data/lib/ditty/policies/user_policy.rb +47 -0
  37. data/lib/ditty/rake_tasks.rb +85 -0
  38. data/lib/ditty/seed.rb +1 -0
  39. data/lib/ditty/services/logger.rb +48 -0
  40. data/lib/ditty/version.rb +5 -0
  41. data/lib/ditty.rb +142 -0
  42. data/migrate/20170207_base_tables.rb +40 -0
  43. data/migrate/20170208_audit_log.rb +12 -0
  44. data/migrate/20170416_audit_log_details.rb +9 -0
  45. data/public/browserconfig.xml +9 -0
  46. data/public/images/apple-icon.png +0 -0
  47. data/public/images/favicon-16x16.png +0 -0
  48. data/public/images/favicon-32x32.png +0 -0
  49. data/public/images/launcher-icon-1x.png +0 -0
  50. data/public/images/launcher-icon-2x.png +0 -0
  51. data/public/images/launcher-icon-4x.png +0 -0
  52. data/public/images/mstile-150x150.png +0 -0
  53. data/public/images/safari-pinned-tab.svg +43 -0
  54. data/public/manifest.json +25 -0
  55. data/views/404.haml +7 -0
  56. data/views/audit_logs/index.haml +30 -0
  57. data/views/error.haml +4 -0
  58. data/views/identity/login.haml +19 -0
  59. data/views/identity/register.haml +14 -0
  60. data/views/index.haml +1 -0
  61. data/views/layout.haml +55 -0
  62. data/views/partials/delete_form.haml +4 -0
  63. data/views/partials/footer.haml +5 -0
  64. data/views/partials/form_control.haml +20 -0
  65. data/views/partials/navbar.haml +24 -0
  66. data/views/partials/notifications.haml +24 -0
  67. data/views/partials/pager.haml +14 -0
  68. data/views/partials/sidebar.haml +35 -0
  69. data/views/roles/display.haml +18 -0
  70. data/views/roles/edit.haml +11 -0
  71. data/views/roles/form.haml +1 -0
  72. data/views/roles/index.haml +22 -0
  73. data/views/roles/new.haml +10 -0
  74. data/views/users/display.haml +50 -0
  75. data/views/users/edit.haml +11 -0
  76. data/views/users/identity.haml +3 -0
  77. data/views/users/index.haml +23 -0
  78. data/views/users/new.haml +11 -0
  79. data/views/users/profile.haml +39 -0
  80. data/views/users/user.haml +3 -0
  81. 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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wisper'
4
+
5
+ module Ditty
6
+ module Helpers
7
+ module Wisper
8
+ def log_action(action, args = {})
9
+ args[:user] ||= current_user
10
+ broadcast(action, args)
11
+ end
12
+ end
13
+ end
14
+ 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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ditty/models/base'
4
+
5
+ module Ditty
6
+ class AuditLog < Sequel::Model
7
+ include ::Ditty::Base
8
+ many_to_one :user
9
+
10
+ def validate
11
+ validates_presence [:action]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ module Ditty
2
+ module Base
3
+ def for_json
4
+ values
5
+ end
6
+ end
7
+ end
@@ -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