aerogel-users 1.4.3

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