aerogel-users 1.4.3
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/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +16 -0
- data/Rakefile +1 -0
- data/aerogel-users.gemspec +30 -0
- data/app/helpers/access_control.rb +32 -0
- data/app/helpers/auth.rb +58 -0
- data/app/helpers/auth_login_providers.rb +18 -0
- data/app/mailers/user.rb +43 -0
- data/app/routes/access_control.rb +15 -0
- data/app/routes/auth.rb +53 -0
- data/app/routes/user.rb +192 -0
- data/assets/README.md +1 -0
- data/assets/javascripts/.gitkeep +0 -0
- data/assets/stylesheets/.gitkeep +0 -0
- data/config/auth.conf +43 -0
- data/config/development/.keep +0 -0
- data/config/production/.keep +0 -0
- data/db/model/access.rb +67 -0
- data/db/model/authentication.rb +54 -0
- data/db/model/role.rb +17 -0
- data/db/model/user.rb +223 -0
- data/db/model/user_email.rb +24 -0
- data/db/model/user_registration_form.rb +23 -0
- data/db/model/user_request_account_activation_form.rb +30 -0
- data/db/model/user_request_email_confirmation_form.rb +40 -0
- data/db/model/user_request_password_reset_form.rb +37 -0
- data/db/model/user_reset_password_form.rb +41 -0
- data/db/seed/01_user_role.seed +8 -0
- data/db/seed/02_access_user.seed +25 -0
- data/db/seed/development/.keep +0 -0
- data/db/seed/development/test-user.seed +77 -0
- data/db/seed/production/.keep +0 -0
- data/lib/aerogel/users.rb +22 -0
- data/lib/aerogel/users/auth.rb +67 -0
- data/lib/aerogel/users/omniauth-failure_endpoint_ex.rb +21 -0
- data/lib/aerogel/users/omniauth-password.rb +63 -0
- data/lib/aerogel/users/secure_password.rb +55 -0
- data/lib/aerogel/users/version.rb +5 -0
- data/locales/actions/en.yml +18 -0
- data/locales/actions/ru.yml +17 -0
- data/locales/auth/en.yml +22 -0
- data/locales/auth/ru.yml +22 -0
- data/locales/mailers/en.yml +69 -0
- data/locales/mailers/ru.yml +73 -0
- data/locales/models/en.yml +65 -0
- data/locales/models/ru.yml +64 -0
- data/locales/views/en.yml +122 -0
- data/locales/views/ru.yml +126 -0
- data/public/README.md +1 -0
- data/rake/README.md +3 -0
- data/views/mailers/user/account_activation.html.erb +3 -0
- data/views/mailers/user/account_activation.text.erb +3 -0
- data/views/mailers/user/email_confirmation.html.erb +3 -0
- data/views/mailers/user/email_confirmation.text.erb +3 -0
- data/views/mailers/user/password_reset.html.erb +3 -0
- data/views/mailers/user/password_reset.text.erb +3 -0
- data/views/user/activate_account.html.erb +3 -0
- data/views/user/activate_account_failure.html.erb +3 -0
- data/views/user/confirm_email.html.erb +3 -0
- data/views/user/confirm_email_failure.html.erb +3 -0
- data/views/user/edit.html.erb +6 -0
- data/views/user/index.html.erb +19 -0
- data/views/user/login.html.erb +3 -0
- data/views/user/login/_form.html.erb +28 -0
- data/views/user/login/_provider.html.erb +5 -0
- data/views/user/logout.html.erb +3 -0
- data/views/user/register.html.erb +10 -0
- data/views/user/register_success.html.erb +3 -0
- data/views/user/request_account_activation.html.erb +9 -0
- data/views/user/request_account_activation_success.html.erb +3 -0
- data/views/user/request_email_confirmation.html.erb +8 -0
- data/views/user/request_email_confirmation_success.html.erb +3 -0
- data/views/user/request_password_reset.html.erb +8 -0
- data/views/user/request_password_reset_success.html.erb +3 -0
- data/views/user/reset_password.html.erb +9 -0
- data/views/user/reset_password_success.html.erb +3 -0
- metadata +234 -0
data/assets/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
All subfolders of this one will be served by Sprockets pipeline. Exact names, i.e. ```css/``` and ```scripts/```, are irrelevant and serve only for better organization of assets.
|
File without changes
|
File without changes
|
data/config/auth.conf
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
auth {
|
2
|
+
providers {
|
3
|
+
#
|
4
|
+
# Known providers
|
5
|
+
#
|
6
|
+
password {
|
7
|
+
name "Email & Password"
|
8
|
+
gem_name nil
|
9
|
+
icon "fa-user"
|
10
|
+
}
|
11
|
+
|
12
|
+
github {
|
13
|
+
name "GitHub"
|
14
|
+
gem_name "omniauth-github"
|
15
|
+
icon "fa-github-square"
|
16
|
+
}
|
17
|
+
|
18
|
+
facebook {
|
19
|
+
name "Facebook"
|
20
|
+
gem_name "omniauth-facebook"
|
21
|
+
icon "fa-facebook-square"
|
22
|
+
}
|
23
|
+
|
24
|
+
twitter {
|
25
|
+
name "Twitter"
|
26
|
+
gem_name "omniauth-twitter"
|
27
|
+
icon "fa-twitter-square"
|
28
|
+
}
|
29
|
+
|
30
|
+
linkedin {
|
31
|
+
name "LinkedIn"
|
32
|
+
gem_name "omniauth-linkedin-oauth2"
|
33
|
+
icon "fa-linkedin-square"
|
34
|
+
}
|
35
|
+
|
36
|
+
vkontakte {
|
37
|
+
name "Vkontakte"
|
38
|
+
gem_name "omniauth-vkontakte"
|
39
|
+
icon "fa-vk"
|
40
|
+
}
|
41
|
+
|
42
|
+
}
|
43
|
+
}
|
File without changes
|
File without changes
|
data/db/model/access.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
class Access
|
2
|
+
|
3
|
+
include Model
|
4
|
+
|
5
|
+
READ = :R
|
6
|
+
WRITE = :RW
|
7
|
+
ACCESS_TYPES = [READ, WRITE]
|
8
|
+
|
9
|
+
field :path, type: String
|
10
|
+
field :access, type: Symbol
|
11
|
+
field :role, type: Symbol
|
12
|
+
field :path_matcher, type: Regexp
|
13
|
+
|
14
|
+
validates_presence_of :path, :access, :role
|
15
|
+
validates :access, inclusion: { in: ACCESS_TYPES }
|
16
|
+
|
17
|
+
validate do |record|
|
18
|
+
# validate roles
|
19
|
+
if record.role_changed?
|
20
|
+
unless Role.slugs.include? record.role
|
21
|
+
record.errors.add :role, :invalid
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Sets path pattern for the rule and compiles it to path matcher Regexp.
|
27
|
+
#
|
28
|
+
def path=( value )
|
29
|
+
self.path_matcher = self.class.compile_matcher value
|
30
|
+
super( value )
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns true if the rule matches path.
|
34
|
+
#
|
35
|
+
def match?( path )
|
36
|
+
path_matcher =~ path
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns true if the rule permits requested access.
|
40
|
+
# +path+ should match this rule's path
|
41
|
+
# +access+ should be granted by this rule
|
42
|
+
# +roles+ should include rule's role
|
43
|
+
#
|
44
|
+
def grants?( path, access, roles )
|
45
|
+
roles = [*roles]
|
46
|
+
self.match?( path ) && ( self.access == WRITE || self.access == access ) && roles.include?( role )
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns list of rules that restrict access to the given +path+.
|
50
|
+
#
|
51
|
+
def self.rules_for_path( path )
|
52
|
+
self.all.select{|r| r.match? path }
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Returns Regexp matcher for given +path+ pattern.
|
59
|
+
#
|
60
|
+
def self.compile_matcher( path )
|
61
|
+
re = path.gsub(/\*{1,2}/) do |match|
|
62
|
+
match == "**" ? ".*" : "[^\/]*"
|
63
|
+
end
|
64
|
+
Regexp.new "^#{re}$"
|
65
|
+
end
|
66
|
+
|
67
|
+
end # class Access
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class Authentication
|
2
|
+
|
3
|
+
include Model
|
4
|
+
include Aerogel::Db::SecurePassword
|
5
|
+
|
6
|
+
VALID_PROVIDERS = Aerogel::Auth.providers.keys
|
7
|
+
|
8
|
+
embedded_in :user
|
9
|
+
|
10
|
+
field :provider, type: Symbol
|
11
|
+
field :uid, type: String
|
12
|
+
field :info, type: Hash
|
13
|
+
|
14
|
+
# has one :email (through embedded user.emails), optional
|
15
|
+
field :email_id, type: String
|
16
|
+
|
17
|
+
field :password_reset_token, type: String
|
18
|
+
|
19
|
+
use_secure_password
|
20
|
+
|
21
|
+
|
22
|
+
# validations:
|
23
|
+
validates_presence_of :provider, :uid
|
24
|
+
validates :provider, inclusion: { in: VALID_PROVIDERS }
|
25
|
+
# validates :password, length: { minimum: 8 }, allow_nil: true
|
26
|
+
|
27
|
+
# validates uniqueness of provider & uid among all users
|
28
|
+
validate do |record|
|
29
|
+
if User.elem_match( :authentications => { :provider => record.provider, :uid => record.uid, :_id.ne => record.id } ).count > 0
|
30
|
+
record.errors.add :uid, :unique
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Only validate password if provider is :password
|
35
|
+
#
|
36
|
+
def validate_password?
|
37
|
+
provider == :password
|
38
|
+
end
|
39
|
+
|
40
|
+
# virtual attributes:
|
41
|
+
|
42
|
+
# Returns email associated with this Authentication
|
43
|
+
#
|
44
|
+
def email
|
45
|
+
user.emails.where( email: self.email_id ).first
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# methods:
|
50
|
+
|
51
|
+
|
52
|
+
end # class Authentication
|
53
|
+
|
54
|
+
|
data/db/model/role.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class Role
|
2
|
+
|
3
|
+
include Model
|
4
|
+
|
5
|
+
field :name, type: String
|
6
|
+
field :slug, type: Symbol
|
7
|
+
|
8
|
+
validates_presence_of :name, :slug
|
9
|
+
validates_uniqueness_of :slug
|
10
|
+
|
11
|
+
# Returns lisf of registered slugs
|
12
|
+
#
|
13
|
+
def self.slugs
|
14
|
+
self.only(:slug).map(&:slug)
|
15
|
+
end
|
16
|
+
|
17
|
+
end # class Role
|
data/db/model/user.rb
ADDED
@@ -0,0 +1,223 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
class User
|
4
|
+
|
5
|
+
include Model
|
6
|
+
include Model::Timestamps
|
7
|
+
|
8
|
+
field :full_name, type: String
|
9
|
+
field :roles, type: Array
|
10
|
+
field :authenticated_at, type: Time
|
11
|
+
|
12
|
+
validates_presence_of :full_name
|
13
|
+
|
14
|
+
embeds_many :authentications
|
15
|
+
accepts_nested_attributes_for :authentications
|
16
|
+
|
17
|
+
embeds_many :emails, class_name: "UserEmail"
|
18
|
+
accepts_nested_attributes_for :emails
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
# validations:
|
23
|
+
validate do |record|
|
24
|
+
# validate roles
|
25
|
+
if record.roles_changed?
|
26
|
+
unless Role.slugs.contains? record.roles
|
27
|
+
record.errors.add :roles, :invalid_roles
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# accessors:
|
33
|
+
def roles=( value )
|
34
|
+
if value.is_a? Array
|
35
|
+
self[:roles] = value.map(&:to_sym)
|
36
|
+
elsif value.is_a? String
|
37
|
+
self[:roles] = value.split(",").map{|v| v.strip.to_sym}
|
38
|
+
else
|
39
|
+
raise ArgumentError.new "Invalid value of class #{value.class} passed to roles= setter"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# methods:
|
44
|
+
|
45
|
+
# Find user authenticated by provider and params.
|
46
|
+
#
|
47
|
+
# For Password strategy, corresponding Authentication is found
|
48
|
+
# by params[:uid] and params[:password].
|
49
|
+
#
|
50
|
+
# For other strategies (github, facebook, twitter etc) only params[:uid] is used
|
51
|
+
#
|
52
|
+
def self.authenticate( provider, params )
|
53
|
+
logger.warn( "User.authenticate: #{provider} #{params}")
|
54
|
+
user = find_by_authentication( provider, params['uid'] )
|
55
|
+
return nil unless user
|
56
|
+
if provider == :password
|
57
|
+
a = user.authentications.where( provider: provider, uid: params['uid'] ).first
|
58
|
+
if a.password_is? params['password']
|
59
|
+
user
|
60
|
+
else
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
else
|
64
|
+
user
|
65
|
+
# self.elem_match( :authentications => { provider: provider, uid: params['uid'] } ).first
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Finds user by authentication (provider & uid).
|
70
|
+
#
|
71
|
+
def self.find_by_authentication( provider, uid )
|
72
|
+
self.elem_match( :authentications => { provider: provider, uid: uid } ).first
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
# Creates a User from another +object+.
|
77
|
+
# +object+ may be a UserRegistrationForm.
|
78
|
+
#
|
79
|
+
def self.create_from( object )
|
80
|
+
raise "Cannot create User from #{object.class}" unless object.is_a? UserRegistrationForm
|
81
|
+
|
82
|
+
self.new(
|
83
|
+
full_name: object.full_name,
|
84
|
+
roles: [:user],
|
85
|
+
emails: [{
|
86
|
+
email: object.email,
|
87
|
+
confirmed: false
|
88
|
+
}],
|
89
|
+
authentications: [{
|
90
|
+
provider: :password,
|
91
|
+
uid: object.email,
|
92
|
+
email_id: object.email,
|
93
|
+
password: object.password,
|
94
|
+
password_confirmation: object.password_confirmation
|
95
|
+
}]
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Creates a User from omniauth AuthHash
|
100
|
+
#
|
101
|
+
def self.create_from_omniauth( omniauth_hash )
|
102
|
+
raise "Cannot create User from #{omniauth_hash.class}" unless omniauth_hash.is_a? OmniAuth::AuthHash
|
103
|
+
|
104
|
+
info = omniauth_hash['info'].to_hash
|
105
|
+
emails = []
|
106
|
+
emails << { email: info['email'], confirmed: false } if info['email']
|
107
|
+
self.new(
|
108
|
+
full_name: info['name'],
|
109
|
+
roles: [:user],
|
110
|
+
emails: emails,
|
111
|
+
authentications: [{
|
112
|
+
provider: omniauth_hash.provider.to_sym,
|
113
|
+
uid: omniauth_hash.uid,
|
114
|
+
info: info
|
115
|
+
}]
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Generates secure confirmation token using +seed+ for random
|
120
|
+
#
|
121
|
+
def self.generate_confirmation_token()
|
122
|
+
SecureRandom::hex
|
123
|
+
end
|
124
|
+
|
125
|
+
# Requests activation of newly registered user:
|
126
|
+
# requests confirmation of email used in password authentication.
|
127
|
+
#
|
128
|
+
def request_activation!
|
129
|
+
object = emails.first
|
130
|
+
request_email_confirmation!( object.email )
|
131
|
+
end
|
132
|
+
|
133
|
+
# Activates account: confirms user email used in password authentication.
|
134
|
+
# Returns corresponding User object on success.
|
135
|
+
# Raises error if confirmation fails.
|
136
|
+
#
|
137
|
+
def self.activate!( email, token )
|
138
|
+
confirm_email! email, token
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns +true+ if user is activated and password authentication uses +email+.
|
142
|
+
#
|
143
|
+
def activated?( email )
|
144
|
+
a = authentications.where( uid: email ).first
|
145
|
+
return false unless a
|
146
|
+
a.email.email == email && a.email.confirmed
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
# Requests confirmation of given email address.
|
151
|
+
# Returns corresponding UserEmail object with newly generated confirmation_token
|
152
|
+
#
|
153
|
+
def request_email_confirmation!( email )
|
154
|
+
object = emails.where( email: email ).first
|
155
|
+
raise "Email '#{email}' does not belong to user" unless object
|
156
|
+
object.confirmation_token = User.generate_confirmation_token
|
157
|
+
object.save!
|
158
|
+
object
|
159
|
+
end
|
160
|
+
|
161
|
+
# Confirms user email using previously issued token.
|
162
|
+
# Returns corresponding User object on success.
|
163
|
+
# Raises error if confirmation fails.
|
164
|
+
#
|
165
|
+
def self.confirm_email!( email, token )
|
166
|
+
user = self.where( 'emails.email' => email ).first
|
167
|
+
raise NotFoundError.new :user_not_found unless user
|
168
|
+
user_email = user.emails.where( email: email ).first
|
169
|
+
raise NotFoundError.new :user_email_not_found unless user_email
|
170
|
+
raise InvalidOperationError.new :email_already_confirmed if user_email.confirmed?
|
171
|
+
raise InvalidOperationError.new :invalid_confirmation_token if !token.nil? && user_email.confirmation_token != token
|
172
|
+
user_email.confirmed = true
|
173
|
+
user_email.save!
|
174
|
+
user
|
175
|
+
end
|
176
|
+
|
177
|
+
# Requests password reset of authentication with given email address.
|
178
|
+
# Returns corresponding Authentication object with newly generated password_reset_token
|
179
|
+
#
|
180
|
+
def request_password_reset!( email )
|
181
|
+
object = authentications.where( provider: :password, uid: email ).first
|
182
|
+
raise NotFoundError.new "Failed to find password authentication for user with email:'#{email}'" unless object
|
183
|
+
object.password_reset_token = User.generate_confirmation_token
|
184
|
+
object.save!
|
185
|
+
object
|
186
|
+
end
|
187
|
+
|
188
|
+
# Resets user password using previously issued token.
|
189
|
+
# Returns corresponding User object on success.
|
190
|
+
# Raises error if password reset fails.
|
191
|
+
#
|
192
|
+
def self.reset_password!( email, token, password, password_confirmation )
|
193
|
+
user = self.where( 'emails.email' => email ).first
|
194
|
+
raise NotFoundError.new "Failed to find user by email" unless user
|
195
|
+
authentication = user.authentications.where( provider: :password, uid: email ).first
|
196
|
+
raise NotFoundError.new "Failed to find password authentication for user with email:'#{email}'" unless authentication
|
197
|
+
raise "Password reset is not requested" if authentication.password_reset_token.nil?
|
198
|
+
raise "Password reset token is invalid" if authentication.password_reset_token != token
|
199
|
+
authentication.password = password
|
200
|
+
authentication.password_confirmation = password_confirmation
|
201
|
+
authentication.password_reset_token = nil
|
202
|
+
authentication.save!
|
203
|
+
user
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
# Updates authenticated_at, does not change other timestamps.
|
208
|
+
# Resets password_reset_token if :provider is 'password'
|
209
|
+
# Returns self.
|
210
|
+
#
|
211
|
+
def authenticated!( opts = {} )
|
212
|
+
self.timeless.update_attributes authenticated_at: Time.now
|
213
|
+
if opts[:provider] == 'password'
|
214
|
+
authentication = self.authentications.where( provider: :password, uid: opts[:uid] ).first
|
215
|
+
unless authentication.password_reset_token.nil?
|
216
|
+
authentication.update_attributes password_reset_token: nil
|
217
|
+
end
|
218
|
+
end
|
219
|
+
self
|
220
|
+
end
|
221
|
+
|
222
|
+
end # class User
|
223
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class UserEmail
|
2
|
+
|
3
|
+
include Model
|
4
|
+
|
5
|
+
field :email, type: String
|
6
|
+
field :confirmed, type: Boolean
|
7
|
+
field :confirmation_token, type: String
|
8
|
+
|
9
|
+
embedded_in :user
|
10
|
+
|
11
|
+
# validates uniqueness of email among all users
|
12
|
+
validate do |record|
|
13
|
+
if User.elem_match( :emails => { :email => record.email, :_id.ne => record.id } ).count > 0
|
14
|
+
record.errors.add :email, :unique
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns Authentication associated with email, if any
|
19
|
+
#
|
20
|
+
# def authentication
|
21
|
+
# user.authentications.where( email_id: self.id )
|
22
|
+
# end
|
23
|
+
|
24
|
+
end # class UserEmail
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class UserRegistrationForm
|
2
|
+
|
3
|
+
include Model::NonPersistent
|
4
|
+
|
5
|
+
field :full_name, type: String
|
6
|
+
field :email, type: String
|
7
|
+
field :password, type: String
|
8
|
+
|
9
|
+
validates_presence_of :full_name, :email, :password, :password_confirmation
|
10
|
+
validates_confirmation_of :password
|
11
|
+
validates_format_of :email, with: /@/, message: :invalid_format
|
12
|
+
|
13
|
+
# validates uniqueness of provider & uid (email) among all users
|
14
|
+
validate do |record|
|
15
|
+
if User.elem_match( :authentications => { :provider => :password, :uid => record.email } ).count > 0
|
16
|
+
record.errors.add :email, :taken
|
17
|
+
elsif User.elem_match( :emails => { :email => record.email } ).count > 0
|
18
|
+
record.errors.add :email, :taken
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
end # class UserRegistrationForm
|