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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +16 -0
  6. data/Rakefile +1 -0
  7. data/aerogel-users.gemspec +30 -0
  8. data/app/helpers/access_control.rb +32 -0
  9. data/app/helpers/auth.rb +58 -0
  10. data/app/helpers/auth_login_providers.rb +18 -0
  11. data/app/mailers/user.rb +43 -0
  12. data/app/routes/access_control.rb +15 -0
  13. data/app/routes/auth.rb +53 -0
  14. data/app/routes/user.rb +192 -0
  15. data/assets/README.md +1 -0
  16. data/assets/javascripts/.gitkeep +0 -0
  17. data/assets/stylesheets/.gitkeep +0 -0
  18. data/config/auth.conf +43 -0
  19. data/config/development/.keep +0 -0
  20. data/config/production/.keep +0 -0
  21. data/db/model/access.rb +67 -0
  22. data/db/model/authentication.rb +54 -0
  23. data/db/model/role.rb +17 -0
  24. data/db/model/user.rb +223 -0
  25. data/db/model/user_email.rb +24 -0
  26. data/db/model/user_registration_form.rb +23 -0
  27. data/db/model/user_request_account_activation_form.rb +30 -0
  28. data/db/model/user_request_email_confirmation_form.rb +40 -0
  29. data/db/model/user_request_password_reset_form.rb +37 -0
  30. data/db/model/user_reset_password_form.rb +41 -0
  31. data/db/seed/01_user_role.seed +8 -0
  32. data/db/seed/02_access_user.seed +25 -0
  33. data/db/seed/development/.keep +0 -0
  34. data/db/seed/development/test-user.seed +77 -0
  35. data/db/seed/production/.keep +0 -0
  36. data/lib/aerogel/users.rb +22 -0
  37. data/lib/aerogel/users/auth.rb +67 -0
  38. data/lib/aerogel/users/omniauth-failure_endpoint_ex.rb +21 -0
  39. data/lib/aerogel/users/omniauth-password.rb +63 -0
  40. data/lib/aerogel/users/secure_password.rb +55 -0
  41. data/lib/aerogel/users/version.rb +5 -0
  42. data/locales/actions/en.yml +18 -0
  43. data/locales/actions/ru.yml +17 -0
  44. data/locales/auth/en.yml +22 -0
  45. data/locales/auth/ru.yml +22 -0
  46. data/locales/mailers/en.yml +69 -0
  47. data/locales/mailers/ru.yml +73 -0
  48. data/locales/models/en.yml +65 -0
  49. data/locales/models/ru.yml +64 -0
  50. data/locales/views/en.yml +122 -0
  51. data/locales/views/ru.yml +126 -0
  52. data/public/README.md +1 -0
  53. data/rake/README.md +3 -0
  54. data/views/mailers/user/account_activation.html.erb +3 -0
  55. data/views/mailers/user/account_activation.text.erb +3 -0
  56. data/views/mailers/user/email_confirmation.html.erb +3 -0
  57. data/views/mailers/user/email_confirmation.text.erb +3 -0
  58. data/views/mailers/user/password_reset.html.erb +3 -0
  59. data/views/mailers/user/password_reset.text.erb +3 -0
  60. data/views/user/activate_account.html.erb +3 -0
  61. data/views/user/activate_account_failure.html.erb +3 -0
  62. data/views/user/confirm_email.html.erb +3 -0
  63. data/views/user/confirm_email_failure.html.erb +3 -0
  64. data/views/user/edit.html.erb +6 -0
  65. data/views/user/index.html.erb +19 -0
  66. data/views/user/login.html.erb +3 -0
  67. data/views/user/login/_form.html.erb +28 -0
  68. data/views/user/login/_provider.html.erb +5 -0
  69. data/views/user/logout.html.erb +3 -0
  70. data/views/user/register.html.erb +10 -0
  71. data/views/user/register_success.html.erb +3 -0
  72. data/views/user/request_account_activation.html.erb +9 -0
  73. data/views/user/request_account_activation_success.html.erb +3 -0
  74. data/views/user/request_email_confirmation.html.erb +8 -0
  75. data/views/user/request_email_confirmation_success.html.erb +3 -0
  76. data/views/user/request_password_reset.html.erb +8 -0
  77. data/views/user/request_password_reset_success.html.erb +3 -0
  78. data/views/user/reset_password.html.erb +9 -0
  79. data/views/user/reset_password_success.html.erb +3 -0
  80. metadata +234 -0
@@ -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
@@ -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
@@ -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
+
@@ -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
@@ -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