ditty 0.4.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/Readme.md +3 -12
- data/ditty.gemspec +4 -1
- data/exe/ditty +4 -0
- data/lib/ditty/cli.rb +44 -0
- data/lib/ditty/controllers/application.rb +3 -4
- data/lib/ditty/controllers/component.rb +38 -14
- data/lib/ditty/controllers/main.rb +115 -34
- data/lib/ditty/controllers/users.rb +7 -10
- data/lib/ditty/db.rb +6 -1
- data/lib/ditty/emails/base.rb +74 -0
- data/lib/ditty/emails/forgot_password.rb +15 -0
- data/lib/ditty/helpers/authentication.rb +2 -2
- data/lib/ditty/helpers/component.rb +7 -4
- data/lib/ditty/helpers/views.rb +5 -2
- data/lib/ditty/listener.rb +41 -7
- data/lib/ditty/models/user.rb +8 -4
- data/lib/ditty/policies/identity_policy.rb +2 -2
- data/lib/ditty/rake_tasks.rb +6 -5
- data/lib/ditty/services/authentication.rb +55 -0
- data/lib/ditty/services/email.rb +18 -20
- data/lib/ditty/services/logger.rb +10 -8
- data/lib/ditty/services/pagination_wrapper.rb +82 -0
- data/lib/ditty/services/settings.rb +45 -0
- data/lib/ditty/version.rb +1 -1
- data/migrate/20180307_password_reset.rb +10 -0
- data/views/audit_logs/index.haml +1 -1
- data/views/emails/base.haml +2 -0
- data/views/emails/forgot_password.haml +26 -0
- data/views/emails/layouts/action.haml +68 -0
- data/views/emails/layouts/alert.haml +88 -0
- data/views/emails/layouts/billing.haml +108 -0
- data/views/identity/forgot.haml +16 -0
- data/views/identity/login.haml +16 -5
- data/views/identity/register.haml +8 -0
- data/views/identity/reset.haml +20 -0
- data/views/layout.haml +2 -0
- metadata +62 -5
- data/lib/ditty/helpers/wisper.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b6858577e8b718519ffc9090091e2709558d0ffd52d938469cb28758bcb45317
|
4
|
+
data.tar.gz: 55d43869a651d32b3dcbcedf093a5c7ef2a01fa83335d7a36c7f3475b61d695b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5606f0ac962ce44b97b08408f6d18bcff35271b87ff4fd88af1a42f85d4f890edafaca116a438b7652fe7b52eb7407074a531c7fba7c63387fe5ff71093ccb7
|
7
|
+
data.tar.gz: ca27dfe35f0249414bf54367880bc11c199cead75a45a2fbb809e20d42d626ba734c43b0c02a82729473b8365f4ff1ec835708a29b188d5f2d1700d112a5eaec
|
data/.gitignore
CHANGED
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.
|
36
|
-
3.
|
37
|
-
4.
|
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 =
|
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
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
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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.
|
81
|
-
|
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.
|
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/
|
31
|
-
|
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/
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
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"
|