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