ditty 0.4.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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"