Skip to content

Commit 980f1a0

Browse files
authored
Feat/password complexity (#1133)
This pull request introduces password strength validation using the `devise_zxcvbn` gem and improves internationalization for password-related messages. The most significant changes include adding the new dependency, integrating the module into the user model, and updating translations for password strength feedback in English, Spanish, and French. Minor localization improvements and code comments addressing registration issues are also included. **Password Strength Validation Integration** * Added `devise_zxcvbn` as a dependency in `better_together.gemspec` and required it in `lib/better_together/engine.rb` to enable password strength checking. [[1]](diffhunk://#diff-24601cecbf8b67ba22917d400c4fc5c238e320bb1d4080343f1e83c323e1ad49R40) [[2]](diffhunk://#diff-4d654c2606392ee71bcc9f10c2846596452c072e8412ccca13c49971ee48dd57R17) * Integrated the `:zxcvbnable` module into the `User` model via Devise to enforce strong password requirements. **Internationalization and Localization** * Added new `weak_password` validation messages in English (`config/locales/en.yml`), Spanish (`config/locales/es.yml`), and French (`config/locales/fr.yml`) to provide clear feedback when a password is too weak. [[1]](diffhunk://#diff-44438ce218f5287c58d0017f965d888715635d94280669896f75841fbd7b4cd7R1850-R1852) [[2]](diffhunk://#diff-4bbf4ee302c9607c80408361708b8b9fde3ee7afc5b505bfd69d429dd433f915R1873-R1875) [[3]](diffhunk://#diff-2c5ab6165f7efe573a84107e0e51102ad47cefb0c65629759d7458eee14326e7R1881-R1883) * Added translations for "password" and corrected locale names in Spanish and French files; also added Ukrainian as a supported locale. [[1]](diffhunk://#diff-4bbf4ee302c9607c80408361708b8b9fde3ee7afc5b505bfd69d429dd433f915R412) [[2]](diffhunk://#diff-2c5ab6165f7efe573a84107e0e51102ad47cefb0c65629759d7458eee14326e7R412) [[3]](diffhunk://#diff-4bbf4ee302c9607c80408361708b8b9fde3ee7afc5b505bfd69d429dd433f915R2074) [[4]](diffhunk://#diff-2c5ab6165f7efe573a84107e0e51102ad47cefb0c65629759d7458eee14326e7L2075-R2081) **Code Comments and Registration Flow** * Added a comment in the `User#person` method and a TODO block for `weak_words` to highlight potential save issues during registration due to automatic association building. [[1]](diffhunk://#diff-f998d6bc00cf682534504b180b280e4f4d33f62aac154bffe2d05c4ea72eb0eeL44-R45) [[2]](diffhunk://#diff-f998d6bc00cf682534504b180b280e4f4d33f62aac154bffe2d05c4ea72eb0eeR70-R76)
2 parents 4ca413d + fa742ac commit 980f1a0

File tree

49 files changed

+2802
-113
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2802
-113
lines changed

.rubocop.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
inherit_from: .rubocop_todo.yml
2+
13
AllCops:
24
Exclude:
35
- 'bin/*'

.rubocop_todo.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# This configuration was generated by
2+
# `rubocop --auto-gen-config`
3+
# on 2025-11-04 02:06:37 UTC using RuboCop version 1.81.6.
4+
# The point is for the user to remove these configuration records
5+
# one by one as the offenses are removed from the code base.
6+
# Note that changes in the inspected code, or installation of new
7+
# versions of RuboCop, may require this file to be generated again.
8+
9+
# Offense count: 52
10+
# This cop supports safe autocorrection (--autocorrect).
11+
Lint/RedundantCopDisableDirective:
12+
Enabled: false
13+
14+
# Offense count: 2
15+
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
16+
Metrics/MethodLength:
17+
Max: 14
18+
19+
# Offense count: 1
20+
# This cop supports safe autocorrection (--autocorrect).
21+
# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
22+
# SupportedStyles: assign_to_condition, assign_inside_condition
23+
Style/ConditionalAssignment:
24+
Exclude:
25+
- 'app/controllers/better_together/joatu/hub_controller.rb'
26+
27+
# Offense count: 29
28+
# This cop supports safe autocorrection (--autocorrect).
29+
Style/IfUnlessModifier:
30+
Enabled: false
31+
32+
# Offense count: 1
33+
# Configuration parameters: Max.
34+
Style/SafeNavigationChainLength:
35+
Exclude:
36+
- 'spec/features/devise/registration_spec.rb'
37+
38+
# Offense count: 1
39+
# This cop supports safe autocorrection (--autocorrect).
40+
# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
41+
# URISchemes: http, https
42+
Layout/LineLength:
43+
Max: 145

Gemfile.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ PATH
2929
devise
3030
devise-i18n
3131
devise-jwt
32+
devise_zxcvbn
3233
elasticsearch-model (~> 7)
3334
elasticsearch-rails (~> 7)
3435
font-awesome-sass (~> 6.5)
@@ -266,6 +267,9 @@ GEM
266267
devise-jwt (0.12.1)
267268
devise (~> 4.0)
268269
warden-jwt_auth (~> 0.10)
270+
devise_zxcvbn (6.0.0)
271+
devise
272+
zxcvbn (~> 0.1.7)
269273
diff-lcs (1.6.2)
270274
disposable (0.6.3)
271275
declarative (>= 0.0.9, < 1.0.0)
@@ -833,6 +837,7 @@ GEM
833837
xpath (3.2.0)
834838
nokogiri (~> 1.8)
835839
zeitwerk (2.7.3)
840+
zxcvbn (0.1.13)
836841

837842
PLATFORMS
838843
aarch64-linux

app/controllers/better_together/users/registrations_controller.rb

Lines changed: 142 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class RegistrationsController < ::Devise::RegistrationsController # rubocop:todo
77
include DeviseLocales
88

99
skip_before_action :check_platform_privacy
10+
before_action :configure_permitted_parameters
1011
before_action :set_required_agreements, only: %i[new create]
1112
before_action :set_event_invitation_from_session, only: %i[new create]
1213
before_action :configure_account_update_params, only: [:update]
@@ -63,18 +64,12 @@ def update # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
6364

6465
def new
6566
super do |user|
66-
# Pre-fill email from platform invitation
67-
user.email = @platform_invitation.invitee_email if @platform_invitation && user.email.empty?
68-
69-
if @event_invitation
70-
# Pre-fill email from event invitation
71-
user.email = @event_invitation.invitee_email if @event_invitation && user.email.empty?
72-
user.person = @event_invitation.invitee if @event_invitation.invitee.present?
73-
end
67+
setup_user_from_invitations(user)
68+
user.build_person unless user.person
7469
end
7570
end
7671

77-
def create # rubocop:todo Metrics/MethodLength
72+
def create # rubocop:todo Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
7873
unless agreements_accepted?
7974
handle_agreements_not_accepted
8075
return
@@ -87,11 +82,25 @@ def create # rubocop:todo Metrics/MethodLength
8782
return
8883
end
8984

85+
# Use transaction for all user creation and associated records
9086
ActiveRecord::Base.transaction do
91-
super do |user|
92-
handle_user_creation(user) if user.persisted?
87+
# Call Devise's default create behavior
88+
super
89+
90+
# Handle post-registration setup if user was created successfully
91+
if resource.persisted? && resource.errors.empty?
92+
handle_user_creation(resource)
93+
elsif resource.persisted?
94+
# User was created but has errors - rollback to maintain consistency
95+
raise ActiveRecord::Rollback
9396
end
9497
end
98+
rescue ActiveRecord::RecordInvalid, ActiveRecord::InvalidForeignKey => e
99+
# Clean up and show user-friendly error
100+
Rails.logger.error "Registration failed: #{e.message}"
101+
build_resource(sign_up_params) if resource.nil?
102+
resource&.errors&.add(:base, 'Registration could not be completed. Please try again.')
103+
respond_with resource
95104
end
96105

97106
protected
@@ -105,6 +114,10 @@ def configure_account_update_params
105114
keys: %i[email password password_confirmation current_password])
106115
end
107116

117+
def configure_permitted_parameters
118+
devise_parameter_sanitizer.permit(:sign_up, keys: [person_attributes: %i[identifier name description]])
119+
end
120+
108121
def set_required_agreements
109122
@privacy_policy_agreement = BetterTogether::Agreement.find_by(identifier: 'privacy_policy')
110123
@terms_of_service_agreement = BetterTogether::Agreement.find_by(identifier: 'terms_of_service')
@@ -170,32 +183,116 @@ def handle_agreements_not_accepted
170183
respond_with resource
171184
end
172185

186+
def setup_user_from_invitations(user)
187+
# Pre-fill email from platform invitation
188+
user.email = @platform_invitation.invitee_email if @platform_invitation && user.email.empty?
189+
190+
return unless @event_invitation
191+
192+
# Pre-fill email from event invitation
193+
user.email = @event_invitation.invitee_email if @event_invitation && user.email.empty?
194+
user.person = @event_invitation.invitee if @event_invitation.invitee.present?
195+
end
196+
173197
def handle_user_creation(user)
174-
setup_person_for_user(user)
175-
return unless user.save!
198+
return unless event_invitation_person_updated?(user)
176199

200+
# Reload user to ensure all nested attributes and associations are properly loaded
177201
user.reload
178-
setup_community_membership(user)
202+
person = user.person
203+
204+
return unless person_persisted?(user, person)
205+
206+
setup_community_membership(user, person)
179207
handle_platform_invitation(user)
180208
handle_event_invitation(user)
181-
create_agreement_participants(user.person)
209+
create_agreement_participants(person)
210+
end
211+
212+
def event_invitation_person_updated?(user)
213+
return true unless @event_invitation&.invitee.present?
214+
215+
return true if user.person.update(person_params)
216+
217+
Rails.logger.error "Failed to update person for event invitation: #{user.person.errors.full_messages}"
218+
false
219+
end
220+
221+
def person_persisted?(user, person)
222+
return true if person&.persisted?
223+
224+
Rails.logger.error "Person not found or not persisted for user #{user.id}"
225+
false
182226
end
183227

184228
def setup_person_for_user(user)
185-
if @event_invitation && @event_invitation.invitee.present?
186-
user.person = @event_invitation.invitee
187-
user.person.update(person_params)
188-
else
189-
user.build_person(person_params)
190-
end
229+
return update_existing_person_for_event(user) if @event_invitation&.invitee.present?
230+
231+
create_new_person_for_user(user)
232+
end
233+
234+
def update_existing_person_for_event(user)
235+
user.person = @event_invitation.invitee
236+
return if user.person.update(person_params)
237+
238+
Rails.logger.error "Failed to update person for event invitation: #{user.person.errors.full_messages}"
239+
user.errors.add(:person, 'Could not update person information')
191240
end
192241

193-
def setup_community_membership(user)
242+
def create_new_person_for_user(user)
243+
return handle_empty_person_params(user) if person_params.empty?
244+
245+
user.build_person(person_params)
246+
return unless person_validated_and_saved?(user)
247+
248+
save_person_identification(user)
249+
end
250+
251+
def handle_empty_person_params(user)
252+
Rails.logger.error 'Person params are empty, cannot build person'
253+
user.errors.add(:person, 'Person information is required')
254+
end
255+
256+
def person_validated_and_saved?(user)
257+
return save_person?(user) if user.person.valid?
258+
259+
Rails.logger.error "Person validation failed: #{user.person.errors.full_messages}"
260+
user.errors.add(:person, 'Person information is invalid')
261+
false
262+
end
263+
264+
def save_person?(user)
265+
return true if user.person.save
266+
267+
Rails.logger.error "Failed to save person: #{user.person.errors.full_messages}"
268+
user.errors.add(:person, 'Could not save person information')
269+
false
270+
end
271+
272+
def save_person_identification(user)
273+
person_identification = user.person_identification
274+
return if person_identification&.save
275+
276+
Rails.logger.error "Failed to save person identification: #{person_identification&.errors&.full_messages}"
277+
user.errors.add(:person, 'Could not link person to user')
278+
end
279+
280+
def setup_community_membership(user, person_param = nil)
281+
person = person_param || user.person
194282
community_role = determine_community_role
195-
helpers.host_community.person_community_memberships.find_or_create_by!(
196-
member: user.person,
197-
role: community_role
198-
)
283+
284+
begin
285+
helpers.host_community.person_community_memberships.find_or_create_by!(
286+
member: person,
287+
role: community_role
288+
)
289+
rescue ActiveRecord::InvalidForeignKey => e
290+
Rails.logger.error "Foreign key violation creating community membership: #{e.message}"
291+
raise e
292+
rescue StandardError => e
293+
Rails.logger.error "Unexpected error creating community membership: #{e.message}"
294+
raise e
295+
end
199296
end
200297

201298
def handle_platform_invitation(user)
@@ -235,10 +332,18 @@ def after_update_path_for(_resource)
235332
end
236333

237334
def person_params
335+
return {} unless params[:user] && params[:user][:person_attributes]
336+
238337
params.require(:user).require(:person_attributes).permit(%i[identifier name description])
338+
rescue ActionController::ParameterMissing => e
339+
Rails.logger.error "Missing person parameters: #{e.message}"
340+
{}
239341
end
240342

241343
def agreements_accepted?
344+
# Ensure required agreements are set
345+
set_required_agreements if @privacy_policy_agreement.nil?
346+
242347
required = [params[:privacy_policy_agreement], params[:terms_of_service_agreement]]
243348
# If a code of conduct agreement exists, require it as well
244349
required << params[:code_of_conduct_agreement] if @code_of_conduct_agreement.present?
@@ -247,11 +352,21 @@ def agreements_accepted?
247352
end
248353

249354
def create_agreement_participants(person)
355+
unless person&.persisted?
356+
Rails.logger.error 'Cannot create agreement participants - person not persisted'
357+
return
358+
end
359+
250360
identifiers = %w[privacy_policy terms_of_service]
251361
identifiers << 'code_of_conduct' if BetterTogether::Agreement.exists?(identifier: 'code_of_conduct')
252362
agreements = BetterTogether::Agreement.where(identifier: identifiers)
363+
253364
agreements.find_each do |agreement|
254-
BetterTogether::AgreementParticipant.create!(agreement: agreement, person: person, accepted_at: Time.current)
365+
BetterTogether::AgreementParticipant.create!(
366+
agreement: agreement,
367+
person: person,
368+
accepted_at: Time.current
369+
)
255370
end
256371
end
257372
end

app/models/better_together/identification.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class Identification < ApplicationRecord
1010
polymorphic: true,
1111
autosave: true
1212

13+
accepts_nested_attributes_for :identity
14+
1315
validates :identity,
1416
presence: true
1517
validates :agent,

0 commit comments

Comments
 (0)