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.
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