softwear 2.0.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +37 -0
- data/app/assets/javascripts/softwear/application.js +13 -0
- data/app/assets/javascripts/softwear/error_utils.js.coffee +82 -0
- data/app/assets/javascripts/softwear/modals.js.coffee +171 -0
- data/app/assets/stylesheets/softwear/application.css +33 -0
- data/app/controllers/softwear/application_controller.rb +4 -0
- data/app/controllers/softwear/error_reports_controller.rb +37 -0
- data/app/helpers/softwear/application_helper.rb +4 -0
- data/app/helpers/softwear/emails_helper.rb +9 -0
- data/app/mailers/error_report_mailer.rb +53 -0
- data/app/views/error_report_mailer/send_report.html.erb +30 -0
- data/app/views/layouts/softwear/application.html.erb +14 -0
- data/app/views/softwear/errors/_error.html.erb +35 -0
- data/app/views/softwear/errors/internal_server_error.html.erb +6 -0
- data/app/views/softwear/errors/internal_server_error.js.erb +10 -0
- data/bin/rails +12 -0
- data/config/routes.rb +3 -0
- data/lib/softwear/auth/belongs_to_user.rb +43 -0
- data/lib/softwear/auth/controller.rb +43 -0
- data/lib/softwear/auth/helper.rb +35 -0
- data/lib/softwear/auth/model.rb +16 -0
- data/lib/softwear/auth/spec.rb +62 -0
- data/lib/softwear/auth/standard_model.rb +498 -0
- data/lib/softwear/auth/stubbed_model.rb +130 -0
- data/lib/softwear/auth/token_authentication.rb +72 -0
- data/lib/softwear/engine.rb +5 -0
- data/lib/softwear/error_catcher.rb +65 -0
- data/lib/softwear/library/api_controller.rb +169 -0
- data/lib/softwear/library/capistrano.rb +94 -0
- data/lib/softwear/library/controller_authentication.rb +145 -0
- data/lib/softwear/library/enqueue.rb +87 -0
- data/lib/softwear/library/spec.rb +127 -0
- data/lib/softwear/library/tcp_server.rb +107 -0
- data/lib/softwear/version.rb +3 -0
- data/lib/softwear.rb +107 -0
- metadata +172 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
module Softwear
|
2
|
+
module Auth
|
3
|
+
class Controller < ApplicationController
|
4
|
+
skip_before_filter :authenticate_user!, only: [:set_session_token, :clear_query_cache]
|
5
|
+
|
6
|
+
def self.abstract_class?
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
# ====================
|
11
|
+
# Comes from an img tag on softwear-hub to let an authorized app know that
|
12
|
+
# a user has signed in.
|
13
|
+
# ====================
|
14
|
+
def set_session_token
|
15
|
+
encrypted_token = params[:token]
|
16
|
+
redirect_to Figaro.env.softwear_hub_url and return if encrypted_token.blank?
|
17
|
+
|
18
|
+
Rails.logger.info "RECEIVED ENCRYPTED TOKEN: #{encrypted_token}"
|
19
|
+
|
20
|
+
decipher = OpenSSL::Cipher::AES.new(256, :CBC)
|
21
|
+
decipher.decrypt
|
22
|
+
decipher.key = Figaro.env.token_cipher_key
|
23
|
+
decipher.iv = Figaro.env.token_cipher_iv
|
24
|
+
|
25
|
+
session[:user_token] = decipher.update(Base64.urlsafe_decode64(encrypted_token)) + decipher.final
|
26
|
+
|
27
|
+
render inline: 'Done'
|
28
|
+
end
|
29
|
+
|
30
|
+
# ====================
|
31
|
+
# Comes from an img tag on softwear-hub when there has been a change to user
|
32
|
+
# attributes or roles and the cache should be cleared.
|
33
|
+
# ====================
|
34
|
+
def clear_query_cache
|
35
|
+
Softwear::Auth::Model.descendants.each do |user|
|
36
|
+
user.query_cache.clear
|
37
|
+
end
|
38
|
+
|
39
|
+
render inline: 'Done'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Softwear
|
2
|
+
module Auth
|
3
|
+
module Helper
|
4
|
+
def profile_picture_of(user = nil, options = {})
|
5
|
+
options[:class] ||= ''
|
6
|
+
options[:class] += ' media-object img-rounded profile-pic img img-responsive'
|
7
|
+
options[:alt] ||= "#{user.try(:full_name) || '(Unknown)'}'s Avatar"
|
8
|
+
options[:title] ||= user.try(:full_name) || 'Someone'
|
9
|
+
|
10
|
+
image_tag user.try(:profile_picture_url) || 'avatar/masarie.jpg', options
|
11
|
+
end
|
12
|
+
|
13
|
+
def auth_server_error_banner
|
14
|
+
return unless Softwear::Auth::Model.auth_server_down?
|
15
|
+
|
16
|
+
content_tag :div, class: 'alert alert-danger' do
|
17
|
+
content_tag :strong do
|
18
|
+
(Softwear::Auth::Model.auth_server_went_down_at || Time.now).strftime(
|
19
|
+
"WARNING: The authentication server is unreachable as of %I:%M%p. "\
|
20
|
+
"Some site features might not function properly."
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# module EmailsHelper
|
28
|
+
# def backtrace_is_from_app?(line)
|
29
|
+
# !(line.include?('/gems/') || /^kernel\// =~ line || line.include?('/vendor_ruby/'))
|
30
|
+
# end
|
31
|
+
|
32
|
+
# extend self
|
33
|
+
# end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'softwear/auth/standard_model'
|
2
|
+
require 'softwear/auth/stubbed_model'
|
3
|
+
|
4
|
+
module Softwear
|
5
|
+
module Auth
|
6
|
+
if Rails.env.development? && ENV['AUTH_SERVER'] != 'true'
|
7
|
+
class Model < StubbedModel
|
8
|
+
STUBBED = true
|
9
|
+
end
|
10
|
+
else
|
11
|
+
class Model < StandardModel
|
12
|
+
STUBBED = false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Softwear
|
2
|
+
module Auth
|
3
|
+
module Spec
|
4
|
+
def spec_users
|
5
|
+
User.instance_variable_get(:@_spec_users)
|
6
|
+
end
|
7
|
+
|
8
|
+
def stub_authentication!(config, *a)
|
9
|
+
config.before(:each, *a) do
|
10
|
+
User.instance_variable_set(:@_spec_users, [])
|
11
|
+
|
12
|
+
allow(User).to receive(:all) { spec_users }
|
13
|
+
allow(User).to receive(:find) { |n| spec_users.find { |u| u.id.to_s == n.to_s } }
|
14
|
+
allow(User).to receive(:auth) { @_signed_in_user or false }
|
15
|
+
allow(User).to receive(:raw_query) { |q| raise "Unstubbed authentication query \"#{q}\"" }
|
16
|
+
|
17
|
+
allow(Figaro.env).to receive(:softwear_hub_url).and_return 'http://hub.example.com'
|
18
|
+
|
19
|
+
allow_any_instance_of(Softwear::Library::ControllerAuthentication)
|
20
|
+
.to receive(:user_token)
|
21
|
+
.and_return('')
|
22
|
+
|
23
|
+
if (controller rescue false)
|
24
|
+
controller.class_eval { helper Softwear::Auth::Helper }
|
25
|
+
|
26
|
+
allow(controller).to receive(:current_user) { @_signed_in_user }
|
27
|
+
controller.class_eval { helper_method :current_user }
|
28
|
+
|
29
|
+
allow(controller).to receive(:user_signed_in?) { !!@_signed_in_user }
|
30
|
+
controller.class_eval { helper_method :user_signed_in? }
|
31
|
+
|
32
|
+
allow(controller).to receive(:destroy_user_session_path) { '#' }
|
33
|
+
controller.class_eval { helper_method :destroy_user_session_path }
|
34
|
+
|
35
|
+
allow(controller).to receive(:users_path) { '#' }
|
36
|
+
controller.class_eval { helper_method :users_path }
|
37
|
+
|
38
|
+
allow(controller).to receive(:edit_user_path) { '#' }
|
39
|
+
controller.class_eval { protected; helper_method :edit_user_path }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
config.after(:each, *a) do
|
44
|
+
User.instance_variable_set(:@_spec_users, nil)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def sign_in_as(user)
|
49
|
+
@_signed_in_user = user
|
50
|
+
|
51
|
+
allow_any_instance_of(Softwear::Library::ControllerAuthentication)
|
52
|
+
.to receive(:user_token).and_return 'abc123'
|
53
|
+
|
54
|
+
if respond_to?(:session) && session.respond_to?(:[]=)
|
55
|
+
session[:user_token] = 'abc123'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
alias_method :sign_in, :sign_in_as
|
59
|
+
alias_method :login_as, :sign_in_as
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,498 @@
|
|
1
|
+
module Softwear
|
2
|
+
module Auth
|
3
|
+
class StandardModel
|
4
|
+
include ActiveModel::Model
|
5
|
+
include ActiveModel::Conversion
|
6
|
+
|
7
|
+
class AccessDeniedError < StandardError
|
8
|
+
end
|
9
|
+
class InvalidCommandError < StandardError
|
10
|
+
end
|
11
|
+
class AuthServerError < StandardError
|
12
|
+
end
|
13
|
+
class AuthServerDown < StandardError
|
14
|
+
end
|
15
|
+
|
16
|
+
# ============================= CLASS METHODS ======================
|
17
|
+
class << self
|
18
|
+
def abstract_class?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_writer :query_cache
|
23
|
+
attr_accessor :total_query_cache
|
24
|
+
attr_writer :query_cache_expiry
|
25
|
+
alias_method :expire_query_cache_every, :query_cache_expiry=
|
26
|
+
attr_accessor :auth_server_went_down_at
|
27
|
+
attr_accessor :sent_auth_server_down_email
|
28
|
+
attr_accessor :time_before_down_email
|
29
|
+
alias_method :email_when_down_after, :time_before_down_email=
|
30
|
+
|
31
|
+
# ====================
|
32
|
+
# Returns true if the authentication server was unreachable for the previous query.
|
33
|
+
# ====================
|
34
|
+
def auth_server_down?
|
35
|
+
!!auth_server_went_down_at
|
36
|
+
end
|
37
|
+
|
38
|
+
# ====================
|
39
|
+
# The query cache takes message keys (such as "get 12") with response values straight from
|
40
|
+
# the server. So yes, this will cache error responses.
|
41
|
+
# You can clear this with <User Class>.query_cache.clear or <User Class>.query_cache = nil
|
42
|
+
# ====================
|
43
|
+
def query_cache
|
44
|
+
@query_cache ||= ThreadSafe::Cache.new
|
45
|
+
end
|
46
|
+
|
47
|
+
def query_cache_expiry
|
48
|
+
@query_cache_expiry || Figaro.env.query_cache_expiry.try(:to_f) || 1.hour
|
49
|
+
end
|
50
|
+
|
51
|
+
# ===================
|
52
|
+
# Override this in your subclasses! The mailer should have auth_server_down(time) and
|
53
|
+
# auth_server_up(time)
|
54
|
+
# ===================
|
55
|
+
def auth_server_down_mailer
|
56
|
+
# override me
|
57
|
+
end
|
58
|
+
|
59
|
+
# ======================================
|
60
|
+
def primary_key
|
61
|
+
:id
|
62
|
+
end
|
63
|
+
|
64
|
+
def base_class
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def relation_delegate_class(*)
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
def unscoped
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
def new(*args)
|
77
|
+
if args.size == 3
|
78
|
+
assoc_class = args[2].owner.class.name
|
79
|
+
assoc_name = args[2].reflection.name
|
80
|
+
raise "Unsupported user association: #{assoc_class}##{assoc_name}. If this is a belongs_to "\
|
81
|
+
"association, you may have #{assoc_class} include Softwear::Auth::BelongsToUser and call "\
|
82
|
+
"`belongs_to_user_called :#{assoc_name}' instead of the traditional rails method."
|
83
|
+
else
|
84
|
+
super
|
85
|
+
end
|
86
|
+
end
|
87
|
+
# ======================================
|
88
|
+
|
89
|
+
# ====================
|
90
|
+
# Not a fully featured has_many - must specify foreign_key if the association doesn't match
|
91
|
+
# the model name, through is inefficient.
|
92
|
+
# ====================
|
93
|
+
def has_many(assoc, options = {})
|
94
|
+
assoc = assoc.to_s
|
95
|
+
|
96
|
+
if through = options[:through]
|
97
|
+
source = options[:source] || assoc
|
98
|
+
|
99
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
100
|
+
def #{assoc}
|
101
|
+
#{through}.flat_map(&:#{source})
|
102
|
+
end
|
103
|
+
RUBY
|
104
|
+
|
105
|
+
else
|
106
|
+
class_name = options[:class_name] || assoc.singularize.camelize
|
107
|
+
foreign_key = options[:foreign_key] || 'user_id'
|
108
|
+
|
109
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
110
|
+
def #{assoc}
|
111
|
+
#{class_name}.where(#{foreign_key}: id)
|
112
|
+
end
|
113
|
+
RUBY
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# ====================
|
118
|
+
# Pretty much a map function - for activerecord compatibility.
|
119
|
+
# ====================
|
120
|
+
def pluck(*attrs)
|
121
|
+
if attrs.size == 1
|
122
|
+
all.map do |user|
|
123
|
+
user.send(attrs.first)
|
124
|
+
end
|
125
|
+
else
|
126
|
+
all.map do |user|
|
127
|
+
attrs.map { |a| user.send(a) }
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def arel_table
|
133
|
+
@arel_table ||= Arel::Table.new(model_name.plural, self)
|
134
|
+
end
|
135
|
+
|
136
|
+
# ====================
|
137
|
+
# This is only used to record how long it takes to perform queries for development.
|
138
|
+
# ====================
|
139
|
+
def record(before, after, type, body)
|
140
|
+
ms = (after - before) * 1000
|
141
|
+
# The garbage in this string gives us the bold and color
|
142
|
+
Rails.logger.info " \033[1m\033[33m#{type} (#{'%.1f' % ms}ms)\033[0m #{body}"
|
143
|
+
end
|
144
|
+
|
145
|
+
# ====================
|
146
|
+
# Host of the auth server, from 'auth_server_endpoint' env variable.
|
147
|
+
# Defaults to localhost.
|
148
|
+
# ====================
|
149
|
+
def auth_server_host
|
150
|
+
endpoint = Figaro.env.auth_server_endpoint
|
151
|
+
if endpoint.blank?
|
152
|
+
'localhost'
|
153
|
+
elsif endpoint.include?(':')
|
154
|
+
endpoint.split(':').first
|
155
|
+
else
|
156
|
+
endpoint
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# ====================
|
161
|
+
# Port of the auth server, from 'auth_server_endpoint' env variable.
|
162
|
+
# Defaults to 2900.
|
163
|
+
# ====================
|
164
|
+
def auth_server_port
|
165
|
+
endpoint = Figaro.env.auth_server_endpoint
|
166
|
+
if endpoint.try(:include?, ':')
|
167
|
+
endpoint.split(':').last
|
168
|
+
else
|
169
|
+
2900
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def default_socket
|
174
|
+
@default_socket ||= TCPSocket.open(auth_server_host, auth_server_port)
|
175
|
+
end
|
176
|
+
|
177
|
+
# ====================
|
178
|
+
# Bare minimum query function - sends a message and returns the response, and
|
179
|
+
# handles a broken socket. #query and #force_query call this function.
|
180
|
+
# ====================
|
181
|
+
def raw_query(message)
|
182
|
+
begin
|
183
|
+
default_socket.puts message
|
184
|
+
|
185
|
+
rescue Errno::EPIPE => e
|
186
|
+
@default_socket = TCPSocket.open(auth_server_host, auth_server_port)
|
187
|
+
@default_socket.puts message
|
188
|
+
end
|
189
|
+
|
190
|
+
response = default_socket.gets.try(:chomp)
|
191
|
+
if response.nil?
|
192
|
+
@default_socket.close rescue nil
|
193
|
+
@default_socket = nil
|
194
|
+
return raw_query(message)
|
195
|
+
end
|
196
|
+
response
|
197
|
+
|
198
|
+
rescue Errno::ECONNREFUSED => e
|
199
|
+
raise AuthServerDown, "Unable to connect to the authentication server."
|
200
|
+
|
201
|
+
rescue Errno::ETIMEDOUT => e
|
202
|
+
raise AuthServerDown, "Connection to authentication server timed out."
|
203
|
+
end
|
204
|
+
|
205
|
+
# ====================
|
206
|
+
# Expires the query cache, setting a new expiration time as well as merging
|
207
|
+
# with the previous query cache, in case of an auth server outage.
|
208
|
+
# ====================
|
209
|
+
def expire_query_cache
|
210
|
+
before = Time.now
|
211
|
+
if total_query_cache
|
212
|
+
query_cache.each_pair do |key, value|
|
213
|
+
total_query_cache[key] = value
|
214
|
+
end
|
215
|
+
else
|
216
|
+
self.total_query_cache = query_cache.clone
|
217
|
+
end
|
218
|
+
|
219
|
+
query_cache.clear
|
220
|
+
query_cache['_expire_at'] = (query_cache_expiry || 1.hour).from_now
|
221
|
+
after = Time.now
|
222
|
+
|
223
|
+
record(before, after, "Authentication Expire Cache", "")
|
224
|
+
end
|
225
|
+
|
226
|
+
# ====================
|
227
|
+
# Queries the authentication server only if there isn't a cached response.
|
228
|
+
# Also keeps track of whether or not the server is reachable, and sends emails
|
229
|
+
# when the server goes down and back up.
|
230
|
+
# ====================
|
231
|
+
def query(message)
|
232
|
+
before = Time.now
|
233
|
+
|
234
|
+
expire_at = query_cache['_expire_at']
|
235
|
+
expire_query_cache if expire_at.blank? || Time.now > expire_at
|
236
|
+
|
237
|
+
if cached_response = query_cache[message]
|
238
|
+
response = cached_response
|
239
|
+
action = "Authentication Cache"
|
240
|
+
else
|
241
|
+
begin
|
242
|
+
response = raw_query(message)
|
243
|
+
action = "Authentication Query"
|
244
|
+
query_cache[message] = response
|
245
|
+
|
246
|
+
if auth_server_went_down_at
|
247
|
+
self.auth_server_went_down_at = nil
|
248
|
+
|
249
|
+
if sent_auth_server_down_email
|
250
|
+
self.sent_auth_server_down_email = false
|
251
|
+
if (mailer = auth_server_down_mailer) && mailer.respond_to?(:auth_server_up)
|
252
|
+
mailer.auth_server_up(Time.now).deliver_now
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
rescue AuthServerError => e
|
258
|
+
raise unless total_query_cache
|
259
|
+
|
260
|
+
old_response = total_query_cache[message]
|
261
|
+
if old_response
|
262
|
+
response = old_response
|
263
|
+
action = "Authentication Cache (due to error)"
|
264
|
+
Rails.logger.error "AUTHENTICATION: The authentication server encountered an error. "\
|
265
|
+
"You should probably check the auth server's logs. "\
|
266
|
+
"A cached response was used."
|
267
|
+
else
|
268
|
+
raise
|
269
|
+
end
|
270
|
+
|
271
|
+
rescue AuthServerDown => e
|
272
|
+
if auth_server_went_down_at.nil?
|
273
|
+
self.auth_server_went_down_at = Time.now
|
274
|
+
expire_query_cache
|
275
|
+
|
276
|
+
elsif auth_server_went_down_at > (time_before_down_email || 5.minutes).ago
|
277
|
+
unless sent_auth_server_down_email
|
278
|
+
self.sent_auth_server_down_email = true
|
279
|
+
|
280
|
+
if (mailer = auth_server_down_mailer) && mailer.respond_to?(:auth_server_down)
|
281
|
+
mailer.auth_server_down(auth_server_went_down_at).deliver_now
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
old_response = total_query_cache[message]
|
287
|
+
if old_response
|
288
|
+
response = old_response
|
289
|
+
action = "Authentication Cache (server down)"
|
290
|
+
else
|
291
|
+
raise AuthServerDown, "An uncached query was attempted, and the authentication server is down."
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
after = Time.now
|
296
|
+
|
297
|
+
record(before, after, action, message)
|
298
|
+
response
|
299
|
+
end
|
300
|
+
|
301
|
+
# ====================
|
302
|
+
# Runs a query through the server without error or cache checking.
|
303
|
+
# ====================
|
304
|
+
def force_query(message)
|
305
|
+
before = Time.now
|
306
|
+
response = raw_query(message)
|
307
|
+
after = Time.now
|
308
|
+
|
309
|
+
record(before, after, "Authentication Query (forced)", message)
|
310
|
+
response
|
311
|
+
end
|
312
|
+
|
313
|
+
# ====================
|
314
|
+
# Expects a response string returned from #query and raises an error for the
|
315
|
+
# following cases:
|
316
|
+
#
|
317
|
+
# - Access denied (AccessDeniedError)
|
318
|
+
# - Invalid command (bad query message) (InvalidCommandError)
|
319
|
+
# - Error on auth server's side (AuthServerError)
|
320
|
+
# ====================
|
321
|
+
def validate_response(response_string)
|
322
|
+
case response_string
|
323
|
+
when 'denied' then raise AccessDeniedError, "Denied"
|
324
|
+
when 'invalid' then raise InvalidCommandError, "Invalid command"
|
325
|
+
when 'sorry'
|
326
|
+
expire_query_cache
|
327
|
+
raise AuthServerError, "Authentication server encountered an error"
|
328
|
+
else
|
329
|
+
response_string
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
# ====================
|
334
|
+
# Finds a user with the given ID
|
335
|
+
# ====================
|
336
|
+
def find(target_id)
|
337
|
+
json = validate_response query "get #{target_id}"
|
338
|
+
|
339
|
+
if json == 'nosuchuser'
|
340
|
+
nil
|
341
|
+
else
|
342
|
+
object = new(JSON.parse(json))
|
343
|
+
object.instance_variable_set(:@persisted, true)
|
344
|
+
object
|
345
|
+
end
|
346
|
+
|
347
|
+
rescue JSON::ParserError => e
|
348
|
+
Rails.logger.error "Bad user model JSON: ``` #{json} ```"
|
349
|
+
nil
|
350
|
+
end
|
351
|
+
|
352
|
+
def filter_all(method, options)
|
353
|
+
all.send(method) do |user|
|
354
|
+
options.all? { |field, wanted_value| user.send(field) == wanted_value }
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
# ====================
|
359
|
+
# Finds a user with the given attributes (just queries for 'all' and uses ruby filters)
|
360
|
+
# ====================
|
361
|
+
def find_by(options)
|
362
|
+
filter_all(:find, options)
|
363
|
+
end
|
364
|
+
|
365
|
+
# ====================
|
366
|
+
# Finds users with the given attributes (just queries for 'all' and uses ruby filters)
|
367
|
+
# ====================
|
368
|
+
def where(options)
|
369
|
+
filter_all(:select, options)
|
370
|
+
end
|
371
|
+
|
372
|
+
# ====================
|
373
|
+
# Returns an array of all registered users
|
374
|
+
# ====================
|
375
|
+
def all
|
376
|
+
json = validate_response query "all"
|
377
|
+
|
378
|
+
objects = JSON.parse(json).map(&method(:new))
|
379
|
+
objects.each { |u| u.instance_variable_set(:@persisted, true) }
|
380
|
+
end
|
381
|
+
|
382
|
+
# ====================
|
383
|
+
# Returns array of all users with the given roles
|
384
|
+
# ====================
|
385
|
+
def of_role(*roles)
|
386
|
+
roles = roles.flatten.compact
|
387
|
+
return [] if roles.empty?
|
388
|
+
|
389
|
+
json = validate_response query "ofrole #{Figaro.env.hub_app_name} #{roles.split(' ')}"
|
390
|
+
|
391
|
+
objects = JSON.parse(json).map(&method(:new))
|
392
|
+
objects.each { |u| u.instance_variable_set(:@persisted, true) }
|
393
|
+
end
|
394
|
+
|
395
|
+
# ====================
|
396
|
+
# Given a valid signin token:
|
397
|
+
# Returns the authenticated user for the given token
|
398
|
+
# Given an invalid signin token:
|
399
|
+
# Returns false
|
400
|
+
# ====================
|
401
|
+
def auth(token, app_name = nil)
|
402
|
+
response = validate_response query "auth #{app_name || Figaro.env.hub_app_name} #{token}"
|
403
|
+
|
404
|
+
return false unless response =~ /^yes .+$/
|
405
|
+
|
406
|
+
_yes, json = response.split(' ', 2)
|
407
|
+
object = new(JSON.parse(json))
|
408
|
+
object.instance_variable_set(:@persisted, true)
|
409
|
+
object
|
410
|
+
end
|
411
|
+
|
412
|
+
# ====================
|
413
|
+
# Overridable logger method used when recording query benchmarks
|
414
|
+
# ====================
|
415
|
+
def logger
|
416
|
+
Rails.logger
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
# ============================= INSTANCE METHODS ======================
|
421
|
+
|
422
|
+
REMOTE_ATTRIBUTES = [
|
423
|
+
:id, :email, :first_name, :last_name,
|
424
|
+
:roles, :profile_picture_url,
|
425
|
+
:default_view, :rights
|
426
|
+
]
|
427
|
+
REMOTE_ATTRIBUTES.each(&method(:attr_accessor))
|
428
|
+
|
429
|
+
attr_reader :persisted
|
430
|
+
alias_method :persisted?, :persisted
|
431
|
+
|
432
|
+
# ====================
|
433
|
+
# Various class methods accessible on instances
|
434
|
+
def query(*a)
|
435
|
+
self.class.query(*a)
|
436
|
+
end
|
437
|
+
def raw_query(*a)
|
438
|
+
self.class.raw_query(*a)
|
439
|
+
end
|
440
|
+
def force_query(*a)
|
441
|
+
self.class.force_query(*a)
|
442
|
+
end
|
443
|
+
def logger
|
444
|
+
self.class.logger
|
445
|
+
end
|
446
|
+
# ====================
|
447
|
+
|
448
|
+
def initialize(attributes = {})
|
449
|
+
update_attributes(attributes)
|
450
|
+
end
|
451
|
+
|
452
|
+
def update_attributes(attributes={})
|
453
|
+
return if attributes.blank?
|
454
|
+
attributes = attributes.with_indifferent_access
|
455
|
+
|
456
|
+
REMOTE_ATTRIBUTES.each do |attr|
|
457
|
+
instance_variable_set("@#{attr}", attributes[attr])
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
def to_json
|
462
|
+
{
|
463
|
+
id: @id,
|
464
|
+
email: @email,
|
465
|
+
first_name: @first_name,
|
466
|
+
last_name: @last_name
|
467
|
+
}
|
468
|
+
.to_json
|
469
|
+
end
|
470
|
+
|
471
|
+
def reload
|
472
|
+
json = validate_response query "get #{id}"
|
473
|
+
|
474
|
+
update_attributes(JSON.parse(json))
|
475
|
+
@persisted = true
|
476
|
+
self
|
477
|
+
end
|
478
|
+
|
479
|
+
def full_name
|
480
|
+
"#{@first_name} #{@last_name}"
|
481
|
+
end
|
482
|
+
|
483
|
+
def valid_password?(pass)
|
484
|
+
query("pass #{id} #{pass}") == 'yes'
|
485
|
+
end
|
486
|
+
|
487
|
+
def role?(*wanted_roles)
|
488
|
+
return true if wanted_roles.empty?
|
489
|
+
|
490
|
+
if @roles.nil?
|
491
|
+
query("role #{Figaro.env.hub_app_name} #{id} #{wanted_roles.join(' ')}") == 'yes'
|
492
|
+
else
|
493
|
+
wanted_roles.any? { |r| @roles.include?(r.to_s) }
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|