ditty 0.6.0 → 0.7.0.pre.rc1
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -4
- data/config.ru +4 -18
- data/ditty.gemspec +2 -0
- data/lib/ditty/components/app.rb +4 -3
- data/lib/ditty/controllers/application.rb +28 -5
- data/lib/ditty/controllers/auth.rb +179 -0
- data/lib/ditty/controllers/component.rb +1 -3
- data/lib/ditty/controllers/main.rb +6 -155
- data/lib/ditty/controllers/users.rb +1 -0
- data/lib/ditty/helpers/component.rb +50 -22
- data/lib/ditty/helpers/response.rb +1 -0
- data/lib/ditty/helpers/views.rb +10 -0
- data/lib/ditty/listener.rb +1 -1
- data/lib/ditty/middleware/accept_extension.rb +31 -0
- data/lib/ditty/models/user.rb +1 -5
- data/lib/ditty/policies/identity_policy.rb +10 -2
- data/lib/ditty/policies/user_policy.rb +8 -1
- data/lib/ditty/services/authentication.rb +16 -7
- data/lib/ditty/services/logger.rb +4 -3
- data/lib/ditty/services/settings.rb +8 -0
- data/lib/ditty/version.rb +1 -1
- data/views/400.haml +2 -0
- data/views/{identity/forgot.haml → auth/forgot_password.haml} +1 -1
- data/views/auth/identity.haml +15 -0
- data/views/auth/login.haml +18 -0
- data/views/auth/register.haml +19 -0
- data/views/auth/register_identity.haml +14 -0
- data/views/{identity/reset.haml → auth/reset_password.haml} +2 -3
- data/views/layout.haml +2 -2
- data/views/partials/actions.haml +6 -4
- data/views/partials/form_tag.haml +2 -1
- data/views/partials/navbar.haml +2 -3
- data/views/partials/search.haml +1 -1
- data/views/partials/sidebar.haml +3 -3
- data/views/roles/display.haml +1 -2
- data/views/roles/index.haml +0 -4
- data/views/users/display.haml +2 -4
- data/views/users/index.haml +11 -10
- data/views/users/profile.haml +2 -4
- metadata +41 -8
- data/views/identity/login.haml +0 -29
- data/views/identity/register.haml +0 -29
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'active_support'
|
4
4
|
require 'active_support/inflector'
|
5
|
+
require 'active_support/core_ext/object/blank'
|
5
6
|
require 'will_paginate/array'
|
6
7
|
|
7
8
|
module Ditty
|
@@ -9,18 +10,38 @@ module Ditty
|
|
9
10
|
module Component
|
10
11
|
include ActiveSupport::Inflector
|
11
12
|
|
13
|
+
# param :count, Integer, min: 1, default: 10 # Can't do this, since count can be `all`
|
14
|
+
def check_count
|
15
|
+
return 10 if params[:count].nil?
|
16
|
+
count = params[:count].to_i
|
17
|
+
return count if count >= 1
|
18
|
+
excp = Sinatra::Param::InvalidParameterError.new 'Parameter cannot be less than 1'
|
19
|
+
excp.param = :count
|
20
|
+
raise excp
|
21
|
+
end
|
22
|
+
|
12
23
|
def dataset
|
13
|
-
|
24
|
+
ds = policy_scope(settings.model_class)
|
25
|
+
ds = ds.where Sequel.|(*search_filters) unless search_filters.blank?
|
26
|
+
ds = ds.order ordering unless ordering.blank?
|
27
|
+
filtered(ds)
|
14
28
|
end
|
15
29
|
|
16
30
|
def list
|
17
|
-
|
18
|
-
page
|
31
|
+
param :q, String
|
32
|
+
param :page, Integer, min: 1, default: 1
|
33
|
+
param :sort, String
|
34
|
+
param :order, String, in: %w[asc desc], transform: :downcase, default: 'asc'
|
35
|
+
# TODO: Can we dynamically validate the search / filter fields?
|
36
|
+
|
37
|
+
ds = dataset
|
38
|
+
ds = ds.dataset if ds.respond_to?(:dataset)
|
39
|
+
return ds if params[:count] == 'all'
|
40
|
+
params[:count] = check_count
|
19
41
|
|
20
|
-
ds = dataset.respond_to?(:dataset) ? dataset.dataset : dataset
|
21
|
-
return ds if count == 'all'
|
22
42
|
# Account for difference between sequel paginate and will paginate
|
23
|
-
|
43
|
+
return ds.paginate(page: params[:page], per_page: params[:count]) if ds.is_a?(Array)
|
44
|
+
ds.paginate(params[:page], params[:count])
|
24
45
|
end
|
25
46
|
|
26
47
|
def heading(action = nil)
|
@@ -39,7 +60,7 @@ module Ditty
|
|
39
60
|
settings.dehumanized || underscore(heading)
|
40
61
|
end
|
41
62
|
|
42
|
-
def
|
63
|
+
def filter_fields
|
43
64
|
self.class.const_defined?('FILTERS') ? self.class::FILTERS : []
|
44
65
|
end
|
45
66
|
|
@@ -47,21 +68,32 @@ module Ditty
|
|
47
68
|
self.class.const_defined?('SEARCHABLE') ? self.class::SEARCHABLE : []
|
48
69
|
end
|
49
70
|
|
50
|
-
def filtered(
|
71
|
+
def filtered(ds)
|
51
72
|
filters.each do |filter|
|
52
|
-
|
53
|
-
filter[:field] ||= filter[:name]
|
54
|
-
filter[:modifier] ||= :to_s
|
55
|
-
dataset = apply_filter(dataset, filter)
|
73
|
+
ds = apply_filter(ds, filter)
|
56
74
|
end
|
57
|
-
|
75
|
+
ds
|
58
76
|
end
|
59
77
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
78
|
+
def filters
|
79
|
+
filter_fields.map do |filter|
|
80
|
+
next if params[filter[:name]].blank?
|
81
|
+
filter[:field] ||= filter[:name]
|
82
|
+
filter[:modifier] ||= :to_s # TODO: Do this with Sinatra Param?
|
83
|
+
filter
|
84
|
+
end.compact
|
85
|
+
end
|
63
86
|
|
64
|
-
|
87
|
+
def ordering
|
88
|
+
return if params[:sort].blank?
|
89
|
+
Sequel.send(params[:order].to_sym, params[:sort].to_sym)
|
90
|
+
end
|
91
|
+
|
92
|
+
def apply_filter(ds, filter)
|
93
|
+
value = params[filter[:name]].send(filter[:modifier])
|
94
|
+
return ds.where(filter[:field] => value) unless filter[:field].to_s.include? '.'
|
95
|
+
|
96
|
+
ds.where(filter_field(filter) => filter_value(filter))
|
65
97
|
end
|
66
98
|
|
67
99
|
def filter_field(filter)
|
@@ -84,13 +116,9 @@ module Ditty
|
|
84
116
|
end
|
85
117
|
|
86
118
|
def search_filters
|
119
|
+
return [] if params[:q].blank?
|
87
120
|
searchable_fields.map { |f| Sequel.ilike(f.to_sym, "%#{params[:q]}%") }
|
88
121
|
end
|
89
|
-
|
90
|
-
def search(dataset)
|
91
|
-
return dataset if ['', nil].include?(params['q']) || search_filters == []
|
92
|
-
dataset.where Sequel.|(*search_filters)
|
93
|
-
end
|
94
122
|
end
|
95
123
|
end
|
96
124
|
end
|
@@ -43,6 +43,7 @@ module Ditty
|
|
43
43
|
format.html do
|
44
44
|
actions = {}
|
45
45
|
actions["#{base_path}/#{entity.id}/edit"] = "Edit #{heading}" if policy(entity).update?
|
46
|
+
actions["#{base_path}/new"] = "New #{heading}" if policy(entity).create?
|
46
47
|
title = heading(:read) + (entity.respond_to?(:name) ? ": #{entity.name}" : '')
|
47
48
|
haml :"#{view_location}/display",
|
48
49
|
locals: { entity: entity, title: title, actions: actions },
|
data/lib/ditty/helpers/views.rb
CHANGED
@@ -104,6 +104,16 @@ module Ditty
|
|
104
104
|
}
|
105
105
|
haml :'partials/pager', locals: locals
|
106
106
|
end
|
107
|
+
|
108
|
+
def display(value, type = :string)
|
109
|
+
if [true, false].include?(value) || type.to_sym == :boolean
|
110
|
+
value ? 'Yes' : 'No'
|
111
|
+
elsif value.nil? || type.to_sym == :nil
|
112
|
+
'(Empty)'
|
113
|
+
else
|
114
|
+
value
|
115
|
+
end
|
116
|
+
end
|
107
117
|
end
|
108
118
|
end
|
109
119
|
end
|
data/lib/ditty/listener.rb
CHANGED
@@ -17,7 +17,7 @@ module Ditty
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def method_missing(method, *args)
|
20
|
-
return unless args[0].is_a?(Hash) && args[0][:target] && args[0][:target].settings.track_actions
|
20
|
+
return unless args[0].is_a?(Hash) && args[0][:target].is_a?(Sinatra::Base) && args[0][:target].settings.track_actions
|
21
21
|
|
22
22
|
log_action({
|
23
23
|
user: args[0][:target].current_user,
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Ditty
|
2
|
+
module Middleware
|
3
|
+
# Allow requests to be responded to in JSON if the URL has .json at the end.
|
4
|
+
# The regex and the content_type can be customized to allow for other formats.
|
5
|
+
# Some inspiration from https://gist.github.com/tstachl/6264249
|
6
|
+
class AcceptExtension
|
7
|
+
attr_reader :env, :regex, :content_type
|
8
|
+
|
9
|
+
def initialize(app, regex = /\A(.*)\.json(\/?)\Z/, content_type = 'application/json')
|
10
|
+
# @mutex = Mutex.new
|
11
|
+
@app = app
|
12
|
+
@regex = regex
|
13
|
+
@content_type = content_type
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
@env = env
|
18
|
+
|
19
|
+
request = Rack::Request.new(env)
|
20
|
+
if request.path =~ regex
|
21
|
+
request.path_info = request.path_info.gsub(regex, '\1\2')
|
22
|
+
env = request.env
|
23
|
+
env['ACCEPT'] = content_type
|
24
|
+
env['CONTENT_TYPE'] = content_type
|
25
|
+
end
|
26
|
+
|
27
|
+
@app.call env
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/ditty/models/user.rb
CHANGED
@@ -15,7 +15,7 @@ module Ditty
|
|
15
15
|
one_to_many :audit_logs
|
16
16
|
|
17
17
|
def role?(check)
|
18
|
-
@roles ||= Hash.new do |h,k|
|
18
|
+
@roles ||= Hash.new do |h, k|
|
19
19
|
h[k] = !roles_dataset.first(name: k).nil?
|
20
20
|
end
|
21
21
|
@roles[check]
|
@@ -56,10 +56,6 @@ module Ditty
|
|
56
56
|
add_role Role.find_or_create(name: 'user')
|
57
57
|
end
|
58
58
|
|
59
|
-
def index_prefix
|
60
|
-
email
|
61
|
-
end
|
62
|
-
|
63
59
|
def username
|
64
60
|
identity_dataset.first.username
|
65
61
|
end
|
@@ -4,8 +4,16 @@ require 'ditty/policies/application_policy'
|
|
4
4
|
|
5
5
|
module Ditty
|
6
6
|
class IdentityPolicy < ApplicationPolicy
|
7
|
-
def
|
8
|
-
|
7
|
+
def login?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
def forgot_password?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
def reset_password?
|
16
|
+
record.new? || (record.reset_requested && record.reset_requested > (Time.now - (24 * 60 * 60)))
|
9
17
|
end
|
10
18
|
|
11
19
|
def permitted_attributes
|
@@ -4,6 +4,11 @@ require 'ditty/policies/application_policy'
|
|
4
4
|
|
5
5
|
module Ditty
|
6
6
|
class UserPolicy < ApplicationPolicy
|
7
|
+
def register?
|
8
|
+
# TODO: Check email domain against settings
|
9
|
+
!['1', 1, 'true', true, 'yes'].include? ENV['DITTY_REGISTERING_DISABLED']
|
10
|
+
end
|
11
|
+
|
7
12
|
def create?
|
8
13
|
user && user.super_admin?
|
9
14
|
end
|
@@ -34,8 +39,10 @@ module Ditty
|
|
34
39
|
def resolve
|
35
40
|
if user && user.super_admin?
|
36
41
|
scope
|
37
|
-
|
42
|
+
elsif user
|
38
43
|
scope.where(id: user.id)
|
44
|
+
else
|
45
|
+
scope.where(id: -1)
|
39
46
|
end
|
40
47
|
end
|
41
48
|
end
|
@@ -1,11 +1,13 @@
|
|
1
1
|
require 'ditty/models/identity'
|
2
|
-
require 'ditty/controllers/
|
2
|
+
require 'ditty/controllers/auth'
|
3
3
|
require 'ditty/services/settings'
|
4
4
|
require 'ditty/services/logger'
|
5
5
|
|
6
6
|
require 'omniauth'
|
7
7
|
OmniAuth.config.logger = Ditty::Services::Logger.instance
|
8
|
+
OmniAuth.config.path_prefix = "#{Ditty::Application.map_path}/auth"
|
8
9
|
OmniAuth.config.on_failure = proc { |env|
|
10
|
+
next [400, {}, []] if env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
|
9
11
|
OmniAuth::FailureEndpoint.new(env).redirect_to_failure
|
10
12
|
}
|
11
13
|
|
@@ -13,13 +15,21 @@ module Ditty
|
|
13
15
|
module Services
|
14
16
|
module Authentication
|
15
17
|
class << self
|
18
|
+
def [](key)
|
19
|
+
config[key]
|
20
|
+
end
|
21
|
+
|
16
22
|
def providers
|
17
|
-
config.keys
|
23
|
+
config.compact.keys
|
18
24
|
end
|
19
25
|
|
20
26
|
def setup
|
21
27
|
providers.each do |provider|
|
22
|
-
|
28
|
+
begin
|
29
|
+
require "omniauth/#{provider}"
|
30
|
+
rescue LoadError
|
31
|
+
require "omniauth-#{provider}"
|
32
|
+
end
|
23
33
|
end
|
24
34
|
end
|
25
35
|
|
@@ -37,13 +47,12 @@ module Ditty
|
|
37
47
|
arguments: [
|
38
48
|
{
|
39
49
|
fields: [:username],
|
40
|
-
callback_path: '/auth/identity/callback',
|
41
50
|
model: Ditty::Identity,
|
42
|
-
on_login: Ditty::
|
43
|
-
on_registration: Ditty::
|
51
|
+
on_login: Ditty::Auth,
|
52
|
+
on_registration: Ditty::Auth,
|
44
53
|
locate_conditions: ->(req) { { username: req['username'] } }
|
45
54
|
}
|
46
|
-
]
|
55
|
+
]
|
47
56
|
}
|
48
57
|
}
|
49
58
|
end
|
@@ -9,6 +9,9 @@ require 'active_support/core_ext/object/blank'
|
|
9
9
|
|
10
10
|
module Ditty
|
11
11
|
module Services
|
12
|
+
# This is the central logger for Ditty. It can be configured to log to
|
13
|
+
# multiple endpoints through Ditty Settings. The default configuration is to
|
14
|
+
# send logs to $stdout
|
12
15
|
class Logger
|
13
16
|
include Singleton
|
14
17
|
|
@@ -21,9 +24,7 @@ module Ditty
|
|
21
24
|
klass = values[:class].constantize
|
22
25
|
opts = tr(values[:options]) || nil
|
23
26
|
logger = klass.new(opts)
|
24
|
-
if values[:level]
|
25
|
-
logger.level = klass.const_get(values[:level].to_sym)
|
26
|
-
end
|
27
|
+
logger.level = klass.const_get(values[:level].to_sym) if values[:level]
|
27
28
|
@loggers << logger
|
28
29
|
end
|
29
30
|
end
|
@@ -7,6 +7,14 @@ require 'active_support/core_ext/hash/keys'
|
|
7
7
|
|
8
8
|
module Ditty
|
9
9
|
module Services
|
10
|
+
# This is the central settings service Ditty. It is used to get the settings
|
11
|
+
# for various aspects of Ditty, and can also be used to configure your own
|
12
|
+
# application.
|
13
|
+
#
|
14
|
+
# It has the concept of sections which can either be included in the main
|
15
|
+
# settings.yml file, or as separate files in the `config` folder. The values
|
16
|
+
# in separate files will be used in preference of those in the `settings.yml`
|
17
|
+
# file.
|
10
18
|
module Settings
|
11
19
|
CONFIG_FOLDER = './config'.freeze
|
12
20
|
CONFIG_FILE = "#{CONFIG_FOLDER}/settings.yml".freeze
|
data/lib/ditty/version.rb
CHANGED
data/views/400.haml
ADDED
@@ -5,7 +5,7 @@
|
|
5
5
|
.panel-body
|
6
6
|
%p.text-center
|
7
7
|
Enter your email address and we will send you a link to reset your password if you've registered on the system.
|
8
|
-
|
8
|
+
= form_tag("#{settings.map_path}/auth/forgot-password") do
|
9
9
|
.form-group
|
10
10
|
.col-sm-12
|
11
11
|
%input.form-control{ name: 'email', type: 'text', placeholder: 'Enter your email address' }
|
@@ -0,0 +1,15 @@
|
|
1
|
+
= form_tag("#{settings.map_path}/auth/identity/callback", attributes: { class: '' }) do
|
2
|
+
.form-group
|
3
|
+
%label.control-label Email
|
4
|
+
%input.form-control.border-input{ name: 'username', tabindex: '1' }
|
5
|
+
.form-group
|
6
|
+
%label.control-label{ style: 'display: block' }
|
7
|
+
Password
|
8
|
+
%a{ href: "#{settings.map_path}/auth/forgot-password", style: 'float: right', tabindex: '5' }
|
9
|
+
Forgot?
|
10
|
+
%input.form-control.border-input{ name: 'password', type: 'password', tabindex: '2' }
|
11
|
+
%button.btn.btn-primary{ type: 'submit', tabindex: '3' } Log In
|
12
|
+
- if policy(::Ditty::User).register?
|
13
|
+
.pull-right
|
14
|
+
No account yet?
|
15
|
+
%a.btn.btn-default{ href: "#{settings.map_path}/auth/register", tabindex: '4' } Register
|
@@ -0,0 +1,18 @@
|
|
1
|
+
.row
|
2
|
+
.col-sm-2
|
3
|
+
.col-sm-8
|
4
|
+
.panel.panel-default
|
5
|
+
.panel-body
|
6
|
+
- if Ditty::Services::Authentication.provides? 'identity'
|
7
|
+
= haml :'auth/identity'
|
8
|
+
.row
|
9
|
+
.col-sm-12= " "
|
10
|
+
.row
|
11
|
+
.col-sm-8.col-sm-push-2
|
12
|
+
- Ditty::Services::Authentication.providers.each do |name|
|
13
|
+
- provider = Ditty::Services::Authentication[name]
|
14
|
+
- next if provider[:login_prompt].nil?
|
15
|
+
%a.btn.btn-block.btn-default{ href: "#{settings.map_path}/auth/#{name}" }
|
16
|
+
%i.fa{ class: "fa-#{provider[:icon] || 'key'}"}
|
17
|
+
= provider[:login_prompt]
|
18
|
+
.col-sm-2
|
@@ -0,0 +1,19 @@
|
|
1
|
+
/ TODO: How can we detect a google registration? Extra parameter to the callback? Or a custom callback page?
|
2
|
+
.row
|
3
|
+
.col-md-2
|
4
|
+
.col-md-8
|
5
|
+
.panel.panel-default
|
6
|
+
.panel-body
|
7
|
+
- if Ditty::Services::Authentication.provides? 'identity'
|
8
|
+
= haml :'auth/register_identity', locals: { identity: identity }
|
9
|
+
.row
|
10
|
+
.col-sm-12= " "
|
11
|
+
.row
|
12
|
+
.col-sm-8.col-sm-push-2
|
13
|
+
- Ditty::Services::Authentication.providers.each do |name|
|
14
|
+
- provider = Ditty::Services::Authentication[name]
|
15
|
+
- next if provider[:register_prompt].nil?
|
16
|
+
%a.btn.btn-block.btn-default{ href: "#{settings.map_path}/auth/#{name}" }
|
17
|
+
%i.fa{ class: "fa-#{provider[:icon] || 'key'}"}
|
18
|
+
= provider[:register_prompt]
|
19
|
+
.col-md-2
|
@@ -0,0 +1,14 @@
|
|
1
|
+
= form_tag("#{settings.map_path}/auth/register/identity") do
|
2
|
+
= form_control(:username, identity, label: 'Email', placeholder: 'your@email.com')
|
3
|
+
= form_control(:password, identity, label: 'Password', type: :password)
|
4
|
+
= form_control(:password_confirmation, identity, label: 'Confirm Password', type: :password)
|
5
|
+
|
6
|
+
- if identity.errors[:password] && identity.errors[:password].include?('is not strong enough')
|
7
|
+
.alert.alert-warning
|
8
|
+
%p Make sure your password is at least 8 characters long, and including the following
|
9
|
+
%ul
|
10
|
+
%li Upper- and lowercase letters
|
11
|
+
%li Numbers
|
12
|
+
%li Special Characters
|
13
|
+
|
14
|
+
%button.btn.btn-primary{ type: 'submit' } Register
|