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.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6c4bdcc080384afbc372c0811697ff421ab6ed3d552ec912cd0a3c0f7ed82da
4
- data.tar.gz: 1f09c726e251d380cd0b55d8c552b064dae7c6811f3a24b0e16a38e55a0c6408
3
+ metadata.gz: b6858577e8b718519ffc9090091e2709558d0ffd52d938469cb28758bcb45317
4
+ data.tar.gz: 55d43869a651d32b3dcbcedf093a5c7ef2a01fa83335d7a36c7f3475b61d695b
5
5
  SHA512:
6
- metadata.gz: d0f86ea3db1913ad4c603a6ba96c535705589fd7fbd28ea3fbe3a262d956838fdc2c27e1950e6aae2f6e1bce20e3ffd0dc32e0bde74ae371869c3ac1922a5b03
7
- data.tar.gz: ad5ea5fe818c8f8d20177082f11ecfeb176b4969b21cb5155410a4f8dbbc581dde05d3cb4c468898a86dd16a8aede7f487f3fd36b60e187df094280f805e1c8e
6
+ metadata.gz: e5606f0ac962ce44b97b08408f6d18bcff35271b87ff4fd88af1a42f85d4f890edafaca116a438b7652fe7b52eb7407074a531c7fba7c63387fe5ff71093ccb7
7
+ data.tar.gz: ca27dfe35f0249414bf54367880bc11c199cead75a45a2fbb809e20d42d626ba734c43b0c02a82729473b8365f4ff1ec835708a29b188d5f2d1700d112a5eaec
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  /.bundle/
2
+ vendor
2
3
  /.yardoc
3
4
  /Gemfile.lock
4
5
  /_yardoc/
@@ -12,3 +13,4 @@
12
13
  .vagrant
13
14
  *.db
14
15
  /Gemfile.dev.lock
16
+ migrations
data/Readme.md CHANGED
@@ -32,18 +32,9 @@ gem install ditty
32
32
  ## Usage
33
33
 
34
34
  1. Add the components to your rack config file. See the included [`config.ru`](https://github.com/EagerELK/ditty/blob/master/config.ru) file for an example setup
35
- 2. Add the Ditty rake tasks to your Rakefile: `require 'ditty/rake_tasks'`
36
- 3. Set the DB connection as the `DATABASE_URL` ENV variable: `DATABASE_URL=sqlite://development.db`
37
- 4. Create and populate the DB and secret tokens:
38
-
39
- ```bash
40
- bundle exec rake proxes:prep
41
- bundle exec rake proxes:generate_tokens
42
- bundle exec rake proxes:migrate
43
- bundle exec rake proxes:seed
44
- ```
45
-
46
- 4. Start up the web app: `bundle exec rackup`
35
+ 2. Set the DB connection as the `DATABASE_URL` ENV variable: `DATABASE_URL=sqlite://development.db`
36
+ 3. Run the Ditty migrations: `bundle exec ditty migrate`
37
+ 4. Run the Ditty server: `bundle exec ditty server`
47
38
 
48
39
  ## Components
49
40
 
data/ditty.gemspec CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
 
18
18
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
19
  spec.bindir = 'exe'
20
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.executables = ['ditty']
21
21
  spec.require_paths = ['lib']
22
22
 
23
23
  spec.add_development_dependency 'bundler', '~> 1.12'
@@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
33
33
  spec.add_dependency 'haml', '~> 5.0'
34
34
  spec.add_dependency 'logger', '~> 1.0'
35
35
  spec.add_dependency 'oga', '>= 2.14'
36
+ spec.add_dependency 'mail', '>= 1.7'
36
37
  spec.add_dependency 'omniauth', '~> 1.0'
37
38
  spec.add_dependency 'omniauth-identity', '~> 1.0'
38
39
  spec.add_dependency 'pundit', '~> 1.0'
@@ -43,5 +44,7 @@ Gem::Specification.new do |spec|
43
44
  spec.add_dependency 'sinatra-contrib', '~> 2.0'
44
45
  spec.add_dependency 'sinatra-flash', '~> 0.3'
45
46
  spec.add_dependency 'tilt', '>= 2'
47
+ spec.add_dependency 'thor', '>= 0.20'
48
+ spec.add_dependency 'will_paginate', '>= 3.1'
46
49
  spec.add_dependency 'wisper', '~> 2.0'
47
50
  end
data/exe/ditty ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'ditty/cli'
3
+
4
+ Ditty::CLI.start
data/lib/ditty/cli.rb ADDED
@@ -0,0 +1,44 @@
1
+ # https://nandovieira.com/creating-generators-and-executables-with-thor
2
+ require 'dotenv/load'
3
+ require 'thor'
4
+ require 'ditty'
5
+ require 'ditty/rake_tasks'
6
+ require 'rack'
7
+ require 'rake'
8
+
9
+ module Ditty
10
+ class CLI < Thor
11
+ include Thor::Actions
12
+
13
+ desc 'server', 'Start the Ditty server'
14
+ require './application' if File.exist?('application.rb')
15
+ def server
16
+ # Ensure the token files are present
17
+ Rake::Task['ditty:generate_tokens'].invoke
18
+
19
+ # Prep Ditty
20
+ Rake::Task['ditty:prep'].invoke
21
+
22
+ # Check the migrations
23
+ Rake::Task['ditty:migrate:check'].invoke
24
+
25
+ # Seed Ditty DB
26
+ puts 'Seeding the Ditty DB'
27
+ Rake::Task['ditty:seed'].invoke
28
+
29
+ # RackUP!
30
+ puts 'Starting the Ditty Server'
31
+ Rack::Server.start(config: 'config.ru')
32
+ end
33
+
34
+ desc 'migrate', 'Run the Ditty migrations'
35
+ def migrate
36
+ # Prep Ditty
37
+ Rake::Task['ditty:prep'].invoke
38
+
39
+ # Run the migrations
40
+ Rake::Task['ditty:migrate:up'].invoke
41
+ puts 'Ditty Migrations Executed'
42
+ end
43
+ end
44
+ end
@@ -7,7 +7,6 @@ require 'sinatra/flash'
7
7
  require 'sinatra/respond_with'
8
8
  require 'ditty/helpers/views'
9
9
  require 'ditty/helpers/pundit'
10
- require 'ditty/helpers/wisper'
11
10
  require 'ditty/helpers/authentication'
12
11
  require 'ditty/services/logger'
13
12
  require 'active_support'
@@ -23,7 +22,7 @@ module Ditty
23
22
  set :view_location, nil
24
23
  set :model_class, nil
25
24
  # The order here is important, since Wisper has a deprecated method respond_with method
26
- helpers Wisper::Publisher, Helpers::Wisper
25
+ helpers Wisper::Publisher
27
26
  helpers Helpers::Pundit, Helpers::Views, Helpers::Authentication
28
27
 
29
28
  register Sinatra::Flash, Sinatra::RespondWith
@@ -53,7 +52,7 @@ module Ditty
53
52
  end
54
53
 
55
54
  configure :production, :development do
56
- enable :logging
55
+ disable :logging
57
56
  use Rack::CommonLogger, Ditty::Services::Logger.instance
58
57
  end
59
58
 
@@ -131,7 +130,7 @@ module Ditty
131
130
  ::Ditty::Services::Logger.instance.debug "Running with #{self.class}"
132
131
  if request.path =~ /.*\.json\Z/
133
132
  content_type :json
134
- request.path_info = request.path_info.gsub(/.json$/,'')
133
+ request.path_info = request.path_info.gsub(/.json$/, '')
135
134
  end
136
135
  # Ensure the accept header is set. People forget to include it in API requests
137
136
  content_type(:json) if request.accept.count.eql?(1) && request.accept.first.to_s.eql?('*/*')
@@ -18,18 +18,36 @@ module Ditty
18
18
  dataset.first(settings.model_class.primary_key => id)
19
19
  end
20
20
 
21
+ def skip_verify!
22
+ @skip_verify = true
23
+ end
24
+
25
+ after do
26
+ return if settings.environment == 'production'
27
+ if (response.successful? || response.redirection?) && @skip_verify == false
28
+ verify_authorized if settings.environment != 'production'
29
+ end
30
+ end
31
+
32
+ after '/' do
33
+ return if settings.environment == 'production' || request.request_method != 'GET'
34
+ if (response.successful? || response.redirection?) && @skip_verify == false
35
+ verify_policy_scoped
36
+ end
37
+ end
38
+
21
39
  # List
22
40
  get '/' do
23
41
  authorize settings.model_class, :list
24
42
 
25
43
  result = list
26
44
 
27
- log_action("#{dehumanized}_list".to_sym) if settings.track_actions
45
+ broadcast(:component_list, target: self)
28
46
  list_response(result)
29
47
  end
30
48
 
31
49
  # Create Form
32
- get '/new' do
50
+ get '/new/?' do
33
51
  authorize settings.model_class, :create
34
52
 
35
53
  entity = settings.model_class.new(permitted_attributes(settings.model_class, :create))
@@ -44,24 +62,26 @@ module Ditty
44
62
  entity = settings.model_class.new(permitted_attributes(settings.model_class, :create))
45
63
  authorize entity, :create
46
64
 
47
- entity.save # Will trigger a Sequel::ValidationFailed exception if the model is incorrect
65
+ entity.db.transaction do
66
+ entity.save # Will trigger a Sequel::ValidationFailed exception if the model is incorrect
67
+ broadcast(:component_create, target: self, entity: entity)
68
+ end
48
69
 
49
- log_action("#{dehumanized}_create".to_sym) if settings.track_actions
50
70
  create_response(entity)
51
71
  end
52
72
 
53
73
  # Read
54
- get '/:id' do |id|
74
+ get '/:id/?' do |id|
55
75
  entity = read(id)
56
76
  halt 404 unless entity
57
77
  authorize entity, :read
58
78
 
59
- log_action("#{dehumanized}_read".to_sym) if settings.track_actions
79
+ broadcast(:component_read, target: self, entity: entity)
60
80
  read_response(entity)
61
81
  end
62
82
 
63
83
  # Update Form
64
- get '/:id/edit' do |id|
84
+ get '/:id/edit/?' do |id|
65
85
  entity = read(id)
66
86
  halt 404 unless entity
67
87
  authorize entity, :update
@@ -72,26 +92,30 @@ module Ditty
72
92
  end
73
93
 
74
94
  # Update
75
- put '/:id' do |id|
95
+ put '/:id/?' do |id|
76
96
  entity = read(id)
77
97
  halt 404 unless entity
78
98
  authorize entity, :update
79
99
 
80
- entity.set(permitted_attributes(settings.model_class, :update))
81
- entity.save # Will trigger a Sequel::ValidationFailed exception if the model is incorrect
100
+ entity.db.transaction do
101
+ entity.set(permitted_attributes(settings.model_class, :update))
102
+ entity.save # Will trigger a Sequel::ValidationFailed exception if the model is incorrect
103
+ broadcast(:component_update, target: self, entity: entity)
104
+ end
82
105
 
83
- log_action("#{dehumanized}_update".to_sym) if settings.track_actions
84
106
  update_response(entity)
85
107
  end
86
108
 
87
- delete '/:id' do |id|
109
+ delete '/:id/?' do |id|
88
110
  entity = read(id)
89
111
  halt 404 unless entity
90
112
  authorize entity, :delete
91
113
 
92
- entity.destroy
114
+ entity.db.transaction do
115
+ entity.destroy
116
+ broadcast(:component_delete, target: self, entity: entity)
117
+ end
93
118
 
94
- log_action("#{dehumanized}_delete".to_sym) if settings.track_actions
95
119
  delete_response(entity)
96
120
  end
97
121
  end
@@ -1,14 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'ditty/controllers/application'
4
+ require 'ditty/services/email'
5
+ require 'securerandom'
4
6
 
5
7
  module Ditty
6
8
  class Main < Application
9
+ set track_actions: true
10
+
7
11
  def find_template(views, name, engine, &block)
8
12
  super(views, name, engine, &block) # Root
9
13
  super(::Ditty::App.view_folder, name, engine, &block) # Basic Plugin
10
14
  end
11
15
 
16
+ CHECK_PATHS = [settings.map_path, "#{settings.map_path}/auth/identity"].freeze
17
+
18
+ before(/.*/) do
19
+ return unless CHECK_PATHS.include? request.path
20
+ # Redirect to the registration page if there's no SA user
21
+ sa = Role.find_or_create(name: 'super_admin')
22
+ if User.where(roles: sa).count == 0
23
+ flash[:info] = 'Please register the super admin user.'
24
+ redirect "#{settings.map_path}/auth/identity/register"
25
+ end
26
+ end
27
+
12
28
  # Home Page
13
29
  get '/' do
14
30
  authenticate!
@@ -18,37 +34,62 @@ module Ditty
18
34
  # OmniAuth Identity Stuff
19
35
  # Log in Page
20
36
  get '/auth/identity' do
21
- # Redirect to the registration page if there's no SA user
22
- sa = Role.find_or_create(name: 'super_admin')
23
- if User.where(roles: sa).count == 0
24
- flash[:info] = 'Please register the super admin user.'
25
- redirect "#{settings.map_path}/auth/identity/register"
26
- end
27
37
  haml :'identity/login', locals: { title: 'Log In' }
28
38
  end
29
39
 
30
- get '/auth/failure' do
31
- broadcast(:identity_failed_login)
32
- flash[:warning] = 'Invalid credentials. Please try again.'
33
- redirect "#{settings.map_path}/auth/identity"
40
+ get '/auth/identity/forgot' do
41
+ haml :'identity/forgot', locals: { title: 'Forgot your password?' }
34
42
  end
35
43
 
36
- post '/auth/identity/callback' do
37
- if env['omniauth.auth']
38
- # Successful Login
39
- user = User.find(email: env['omniauth.auth']['info']['email'])
40
- self.current_user = user
41
- log_action(:identity_login, user: user)
42
- flash[:success] = 'Logged In'
43
- redirect env['omniauth.origin'] || "#{settings.map_path}/"
44
- else
45
- # Failed Login
46
- broadcast(:identity_failed_login)
47
- flash[:warning] = 'Invalid credentials. Please try again.'
44
+ post '/auth/identity/forgot' do
45
+ email = params['email']
46
+ identity = Identity[username: email]
47
+ if identity
48
+ # Update record
49
+ token = SecureRandom.hex(16)
50
+ identity.update(reset_token: token, reset_requested: Time.now)
51
+ # Send Email
52
+ reset_url = "#{request.base_url}#{settings.map_path}/auth/identity/reset?token=#{token}"
53
+ Ditty::Services::Email.deliver(
54
+ :forgot_password,
55
+ email,
56
+ locals: { identity: identity, reset_url: reset_url, request: request }
57
+ )
58
+ end
59
+ flash[:info] = 'An email was sent to the email provided with instructions on how to reset your password'
60
+ redirect '/auth/identity'
61
+ end
62
+
63
+ get '/auth/identity/reset' do
64
+ identity = Identity[reset_token: params['token']]
65
+ halt 404 unless identity && identity.reset_requested && identity.reset_requested > (Time.now - (24 * 60 * 60))
66
+
67
+ haml :'identity/reset', locals: { title: 'Reset your password', identity: identity }
68
+ end
69
+
70
+ put '/auth/identity/reset' do
71
+ identity = Identity[reset_token: params['token']]
72
+ halt 404 unless identity && identity.reset_requested && identity.reset_requested > (Time.now - (24 * 60 * 60))
73
+
74
+ identity_params = permitted_attributes(Identity, :update)
75
+
76
+ identity.set identity_params.merge(reset_token: nil, reset_requested: nil)
77
+ if identity.valid? && identity.save
78
+ broadcast(:identity_update_password, target: self, details: "IP: #{request.ip}")
79
+ flash[:success] = 'Password Updated'
48
80
  redirect "#{settings.map_path}/auth/identity"
81
+ else
82
+ broadcast(:identity_update_password_failed, target: self, details: "IP: #{request.ip}")
83
+ haml :'identity/reset', locals: { title: 'Reset your password', identity: identity }
49
84
  end
50
85
  end
51
86
 
87
+ get '/auth/failure' do
88
+ broadcast(:user_failed_login, target: self, details: "IP: #{request.ip}")
89
+ flash[:warning] = 'Invalid credentials. Please try again.'
90
+ redirect "#{settings.map_path}/auth/identity"
91
+ end
92
+
52
93
  # Register Page
53
94
  get '/auth/identity/register' do
54
95
  authorize ::Ditty::Identity, :register
@@ -62,18 +103,20 @@ module Ditty
62
103
  authorize ::Ditty::Identity, :register
63
104
 
64
105
  identity = Identity.new(params['identity'])
65
- if identity.valid? && identity.save
66
- user = User.find_or_create(email: identity.username)
67
- user.add_identity identity
68
-
69
- # Create the SA user if none is present
70
- sa = Role.find_or_create(name: 'super_admin')
71
- user.add_role sa if User.where(roles: sa).count == 0
106
+ begin
107
+ DB.transaction do
108
+ identity.save # Will trigger a Sequel::ValidationFailed exception if the model is incorrect
109
+ user = User.find(email: identity.username)
110
+ if user.nil?
111
+ user = User.create(email: identity.username)
72
112
 
73
- log_action(:identity_register, user: user)
74
- flash[:info] = 'Successfully Registered. Please log in'
75
- redirect "#{settings.map_path}/auth/identity"
76
- else
113
+ broadcast(:user_register, target: self, values: { user: user }, details: "IP: #{request.ip}")
114
+ end
115
+ user.add_identity identity
116
+ flash[:info] = 'Successfully Registered. Please log in'
117
+ redirect "#{settings.map_path}/auth/identity"
118
+ end
119
+ rescue Sequel::ValidationFailed
77
120
  flash.now[:warning] = 'Could not complete the registration. Please try again.'
78
121
  haml :'identity/register', locals: { identity: identity }
79
122
  end
@@ -81,13 +124,51 @@ module Ditty
81
124
 
82
125
  # Logout Action
83
126
  delete '/auth/identity' do
84
- log_action(:identity_logout)
127
+ broadcast(:user_logout, target: self, details: "IP: #{request.ip}")
85
128
  logout
86
129
  flash[:info] = 'Logged Out'
87
130
 
88
131
  redirect "#{settings.map_path}/"
89
132
  end
90
133
 
134
+ post '/auth/identity/callback' do
135
+ if env['omniauth.auth']
136
+ # Successful Login
137
+ user = User.find(email: env['omniauth.auth']['info']['email'])
138
+ self.current_user = user
139
+ broadcast(:user_login, target: self, details: "IP: #{request.ip}")
140
+ flash[:success] = 'Logged In'
141
+ redirect env['omniauth.origin'] || "#{settings.map_path}/"
142
+ else
143
+ # Failed Login
144
+ broadcast(:identity_failed_login, target: self, details: "IP: #{request.ip}")
145
+ flash[:warning] = 'Invalid credentials. Please try again.'
146
+ redirect "#{settings.map_path}/auth/identity"
147
+ end
148
+ end
149
+
150
+ get '/auth/:provider/callback' do
151
+ if env['omniauth.auth']
152
+ # Successful Login
153
+ user = User.find(email: env['omniauth.auth']['info']['email'])
154
+ if user.nil?
155
+ DB.transaction do
156
+ user = User.create(email: env['omniauth.auth']['info']['email'])
157
+ broadcast(:user_register, target: self, values: { user: user }, details: "IP: #{request.ip}")
158
+ end
159
+ end
160
+ self.current_user = user
161
+ broadcast(:user_login, target: self, details: "IP: #{request.ip}")
162
+ flash[:success] = 'Logged In'
163
+ redirect env['omniauth.origin'] || "#{settings.map_path}/"
164
+ else
165
+ # Failed Login
166
+ broadcast(:user_failed_login, target: self, details: "IP: #{request.ip}")
167
+ flash[:warning] = 'Invalid credentials. Please try again.'
168
+ redirect "#{settings.map_path}/auth/identity"
169
+ end
170
+ end
171
+
91
172
  # Unauthenticated
92
173
  get '/unauthenticated' do
93
174
  redirect "#{settings.map_path}/auth/identity"