Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,19 @@ def new
end
end

def create
def create # rubocop:todo Metrics/MethodLength
unless agreements_accepted?
handle_agreements_not_accepted
return
end

# Validate captcha if enabled by host application
unless validate_captcha_if_enabled?
build_resource(sign_up_params)
handle_captcha_validation_failure(resource)
return
end

ActiveRecord::Base.transaction do
super do |user|
handle_user_creation(user) if user.persisted?
Expand All @@ -104,6 +111,25 @@ def set_required_agreements
@code_of_conduct_agreement = BetterTogether::Agreement.find_by(identifier: 'code_of_conduct')
end

# Hook method for host applications to implement captcha validation
# Override this method in host applications to add Turnstile or other captcha validation
# @return [Boolean] true if captcha is valid or not enabled, false if validation fails
def validate_captcha_if_enabled?
# Default implementation - no captcha validation
# Host applications should override this method to implement their captcha logic
true
end

# Hook method for host applications to handle captcha validation failures
# Override this method in host applications to customize error handling
# @param resource [User] the user resource being created
def handle_captcha_validation_failure(resource)
# Default implementation - adds a generic error message
resource.errors.add(:base, I18n.t('better_together.registrations.captcha_validation_failed',
default: 'Security verification failed. Please try again.'))
respond_with resource
end

def after_sign_up_path_for(resource)
# Redirect to event if signed up via event invitation
return better_together.event_path(@event_invitation.event) if @event_invitation&.event
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%# Override this partial in your host app to add additional fields to the registration form %>
<%# Place custom form elements here (e.g., CAPTCHA, extra validation fields, etc.) %>
<%# Available locals: form (form builder), resource (user resource being created) %>
101 changes: 54 additions & 47 deletions app/views/devise/registrations/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -29,112 +29,119 @@
<% else %>
<div class="row justify-content-center">
<div class="col-md-6">
<h2 class="mb-4"><%= t('.sign_up') %></h2>
<h2 id="registration-title" class="mb-4"><%= t('.sign_up') %></h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: 'card card-body needs-validation', novalidate: true }, data: { controller: 'better_together--form-validation', turbo: false }) do |f| %>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { id: 'registration-form', class: 'card card-body needs-validation registration-form', novalidate: true }, data: { controller: 'better_together--form-validation', turbo: false }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>

<% if @platform_invitation %>
<%= hidden_field_tag :invitation_code, @platform_invitation.token %>
<%= hidden_field_tag :invitation_code, @platform_invitation.token, id: 'invitation-code-field' %>
<% end %>

<!-- Email Field -->
<div class="mb-3">
<%= f.label :email, t('.email.label'), class: 'form-label' %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: 'form-control', required: true %>
<small class="form-text text-muted"><%= t('.email.help') %></small>
<div id="email-field-group" class="mb-3 form-field-group">
<%= f.label :email, t('.email.label'), class: 'form-label', for: 'user_email' %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: 'form-control', required: true, id: 'user_email' %>
<small id="email-help-text" class="form-text text-muted"><%= t('.email.help') %></small>
</div>

<!-- Password Field -->
<div class="mb-3" data-controller="better_together--password-toggle">
<%= f.label :password, t('.password.label'), class: 'form-label' %>
<div id="password-field-group" class="mb-3 form-field-group" data-controller="better_together--password-toggle">
<%= f.label :password, t('.password.label'), class: 'form-label', for: 'user_password' %>
<% if @minimum_password_length %>
<em><%= t('devise.shared.minimum_password_length', count: @minimum_password_length) %></em>
<em id="password-length-requirement"><%= t('devise.shared.minimum_password_length', count: @minimum_password_length) %></em>
<% end %>
<div class="input-group">
<%= f.password_field :password, autocomplete: "current-password", class: 'form-control', "data-target": "better_together--password-toggle.field", required: true, minlength: @minimum_password_length || 12 %>
<button type="button" data-action="click->better_together--password-toggle#password" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="<%= t('devise.sessions.new.password.toggle') %>">
<%= f.password_field :password, autocomplete: "current-password", class: 'form-control', "data-target": "better_together--password-toggle.field", required: true, minlength: @minimum_password_length || 12, id: 'user_password' %>
<button id="password-toggle-btn" type="button" data-action="click->better_together--password-toggle#password" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="<%= t('devise.sessions.new.password.toggle') %>">
<i class="password-field-icon-1 far fa-eye-slash" data-target="better_together--password-toggle.icon"></i>
</button>
</div>
<small class="form-text text-muted"><%= t('.password.help') %></small>
<small id="password-help-text" class="form-text text-muted"><%= t('.password.help') %></small>
</div>

<!-- Password Confirmation Field -->
<div class="mb-3" data-controller="better_together--password-toggle">
<%= f.label :password_confirmation, t('.password_confirmation.label'), class: 'form-label' %>
<div id="password-confirmation-field-group" class="mb-3 form-field-group" data-controller="better_together--password-toggle">
<%= f.label :password_confirmation, t('.password_confirmation.label'), class: 'form-label', for: 'user_password_confirmation' %>

<div class="input-group">
<%= f.password_field :password_confirmation, autocomplete: "current-password", class: 'form-control', "data-target": "better_together--password-toggle.field", required: true, minlength: @minimum_password_length || 12 %>
<button type="button" data-action="click->better_together--password-toggle#password" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="<%= t('devise.sessions.new.password.toggle') %>">
<%= f.password_field :password_confirmation, autocomplete: "current-password", class: 'form-control', "data-target": "better_together--password-toggle.field", required: true, minlength: @minimum_password_length || 12, id: 'user_password_confirmation' %>
<button id="password-confirmation-toggle-btn" type="button" data-action="click->better_together--password-toggle#password" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="<%= t('devise.sessions.new.password.toggle') %>">
<i class="password-field-icon-1 far fa-eye-slash" data-target="better_together--password-toggle.icon"></i>
</button>
</div>
<small class="form-text text-muted"><%= t('.password_confirmation.help') %></small>
<small id="password-confirmation-help-text" class="form-text text-muted"><%= t('.password_confirmation.help') %></small>
</div>

<div id="profile-details" class="mb-4">
<h4><%= t('.profile_details') %></h4>
<div id="profile-details" class="mb-4 profile-details-section">
<h4 id="profile-details-title"><%= t('.profile_details') %></h4>
<!-- Person Identification Fields -->
<%= f.fields_for :person do |person_form| %>
<!-- Name Field -->
<div class="mb-3">
<%= person_form.label :name, t('.person.name'), class: 'form-label' %>
<%= person_form.text_field :name, class: "form-control", required: true %>
<small class="form-text text-muted"><%= t('.person.name_hint') %></small>
<div id="name-field-group" class="mb-3 form-field-group">
<%= person_form.label :name, t('.person.name'), class: 'form-label', for: 'user_person_attributes_name' %>
<%= person_form.text_field :name, class: "form-control", required: true, id: 'user_person_attributes_name' %>
<small id="name-help-text" class="form-text text-muted"><%= t('.person.name_hint') %></small>
</div>

<!-- Username Field -->
<div class="mb-3">
<%= person_form.label :identifier, t('.person.identifier'), class: 'form-label' %>
<%= person_form.text_field :identifier, class: "form-control", required: true, minlength: 3 %>
<div id="identifier-field-group" class="mb-3 form-field-group">
<%= person_form.label :identifier, t('.person.identifier'), class: 'form-label', for: 'user_person_attributes_identifier' %>
<%= person_form.text_field :identifier, class: "form-control", required: true, minlength: 3, id: 'user_person_attributes_identifier' %>
<!-- Hint text for the Handle -->
<small class="form-text text-muted"><%= t('.person.identifier_hint_html', platform: host_platform) %></small>
<small id="identifier-help-text" class="form-text text-muted"><%= t('.person.identifier_hint_html', platform: host_platform) %></small>
</div>

<!-- Description Field -->
<div class="mb-3">
<%= person_form.label :description, t('.person.description'), class: 'form-label' %>
<%= person_form.text_area :description, class: "form-control" %>
<small class="form-text text-muted"><%= t('.person.description_hint') %></small>
<div id="description-field-group" class="mb-3 form-field-group">
<%= person_form.label :description, t('.person.description'), class: 'form-label', for: 'user_person_attributes_description' %>
<%= person_form.text_area :description, class: "form-control", id: 'user_person_attributes_description' %>
<small id="description-help-text" class="form-text text-muted"><%= t('.person.description_hint') %></small>
</div>
<% end %>
</div>

<% if @terms_of_service_agreement || @privacy_policy_agreement || @code_of_conduct_agreement %>
<div id="agreements-section" class="agreements-section">
<% if @terms_of_service_agreement %>
<div class="mb-3 form-check">
<%= check_box_tag :terms_of_service_agreement, '1', false, class: 'form-check-input agreement-checkbox', data: { agreement_identifier: 'terms_of_service' }, required: true, aria: { describedby: 'terms-of-service-summary', disabled: 'true' } %>
<%= label_tag :terms_of_service_agreement, t('.terms_of_service.label'), class: 'form-check-label' %>
<div id="terms-of-service-agreement-group" class="mb-3 form-check agreement-group">
<%= check_box_tag :terms_of_service_agreement, '1', false, class: 'form-check-input agreement-checkbox', data: { agreement_identifier: 'terms_of_service' }, required: true, aria: { describedby: 'terms-of-service-summary', disabled: 'true' }, id: 'terms_of_service_agreement_checkbox' %>
<%= label_tag :terms_of_service_agreement, t('.terms_of_service.label'), class: 'form-check-label', for: 'terms_of_service_agreement_checkbox' %>
<%= link_to t('.view', default: 'View'), agreement_path(@terms_of_service_agreement, locale: I18n.locale), class: 'ms-2 agreement-modal-link', role: 'button', data: { agreement_identifier: 'terms_of_service' } %>
<p id="terms-of-service-summary" class="form-text text-muted"><%= @terms_of_service_agreement.description %></p>
<small id="terms-of-service-summary" class="form-text text-muted"><%= @terms_of_service_agreement.description %></small>
</div>
<% end %>

<% if @privacy_policy_agreement %>
<div class="mb-3 form-check">
<%= check_box_tag :privacy_policy_agreement, '1', false, class: 'form-check-input agreement-checkbox', data: { agreement_identifier: 'privacy_policy' }, required: true, aria: { describedby: 'privacy-policy-summary', disabled: 'true' } %>
<%= label_tag :privacy_policy_agreement, t('.privacy_policy.label'), class: 'form-check-label' %>
<div id="privacy-policy-agreement-group" class="mb-3 form-check agreement-group">
<%= check_box_tag :privacy_policy_agreement, '1', false, class: 'form-check-input agreement-checkbox', data: { agreement_identifier: 'privacy_policy' }, required: true, aria: { describedby: 'privacy-policy-summary', disabled: 'true' }, id: 'privacy_policy_agreement_checkbox' %>
<%= label_tag :privacy_policy_agreement, t('.privacy_policy.label'), class: 'form-check-label', for: 'privacy_policy_agreement_checkbox' %>
<%= link_to t('.view', default: 'View'), agreement_path(@privacy_policy_agreement, locale: I18n.locale), class: 'ms-2 agreement-modal-link', role: 'button', data: { agreement_identifier: 'privacy_policy' } %>
<p id="privacy-policy-summary" class="form-text text-muted"><%= @privacy_policy_agreement.description %></p>
<small id="privacy-policy-summary" class="form-text text-muted"><%= @privacy_policy_agreement.description %></small>
</div>
<% end %>

<% if @code_of_conduct_agreement %>
<div class="mb-3 form-check">
<%= check_box_tag :code_of_conduct_agreement, '1', false, class: 'form-check-input agreement-checkbox', data: { agreement_identifier: 'code_of_conduct' }, required: true, aria: { describedby: 'code-of-conduct-summary', disabled: 'true' } %>
<%= label_tag :code_of_conduct_agreement, t('.code_of_conduct.label'), class: 'form-check-label' %>
<div id="code-of-conduct-agreement-group" class="mb-3 form-check agreement-group">
<%= check_box_tag :code_of_conduct_agreement, '1', false, class: 'form-check-input agreement-checkbox', data: { agreement_identifier: 'code_of_conduct' }, required: true, aria: { describedby: 'code-of-conduct-summary', disabled: 'true' }, id: 'code_of_conduct_agreement_checkbox' %>
<%= label_tag :code_of_conduct_agreement, t('.code_of_conduct.label'), class: 'form-check-label', for: 'code_of_conduct_agreement_checkbox' %>
<%= link_to t('.view', default: 'View'), agreement_path(@code_of_conduct_agreement, locale: I18n.locale), class: 'ms-2 agreement-modal-link', role: 'button', data: { agreement_identifier: 'code_of_conduct' } %>
<p id="code-of-conduct-summary" class="form-text text-muted"><%= @code_of_conduct_agreement.description %></p>
<small id="code-of-conduct-summary" class="form-text text-muted"><%= @code_of_conduct_agreement.description %></small>
</div>
<% end %>
</div>
<% end %>

<!-- Host App Extensible Content (e.g., CAPTCHA, additional fields) -->
<%= render partial: 'devise/registrations/extra_registration_fields', locals: { form: f, resource: resource } %>

<!-- Submit Button -->
<div class="text-center">
<%= f.submit t('.sign_up'), class: 'btn btn-primary' %>
<div id="submit-section" class="text-center submit-section">
<%= f.submit t('.sign_up'), class: 'btn btn-primary', id: 'registration-submit-btn' %>
<!-- Additional Links -->
<%= render "devise/shared/links" %>
<div id="additional-links" class="additional-links">
<%= render "devise/shared/links" %>
</div>
</div>
<% end %>
</div>
Expand Down
4 changes: 4 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,8 @@ en:
updated_on_short: Updated
view_post: View Post
primary: Primary
registrations:
captcha_validation_failed: Security verification failed. Please try again.
remove: Remove
resource_permissions:
index:
Expand Down Expand Up @@ -1721,6 +1723,8 @@ en:
agreements_must_accept: You must accept the Terms of Service and Privacy Policy
to continue.
agreements_required: You must accept the Privacy Policy and Terms of Service
captcha_validation_failed: Security verification failed. Please complete the
security check and try again.
code_of_conduct:
label: I agree to the Code of Conduct
email:
Expand Down
24 changes: 15 additions & 9 deletions config/locales/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1428,6 +1428,9 @@ es:
updated_on_short: Actualizado
view_post: Ver publicación
primary: Primario
registrations:
captcha_validation_failed: La verificación de seguridad falló. Por favor, inténtelo
de nuevo.
remove: Eliminar
resource_permissions:
index:
Expand Down Expand Up @@ -1732,13 +1735,16 @@ es:
actual para confirmar los cambios
new:
agreements:
privacy_policy: I agree to the Privacy Policy
terms_of_service: I agree to the Terms of Service
agreements_must_accept: You must accept the Terms of Service and Privacy Policy
to continue.
agreements_required: You must accept the Privacy Policy and Terms of Service
privacy_policy: Acepto la Política de Privacidad
terms_of_service: Acepto los Términos de Servicio
agreements_must_accept: Debe aceptar los Términos de Servicio y la Política
de Privacidad para continuar.
agreements_required: Debe aceptar la Política de Privacidad y los Términos
de Servicio
captcha_validation_failed: La verificación de seguridad falló. Por favor,
completa la verificación de seguridad e inténtalo de nuevo.
code_of_conduct:
label: I agree to the Code of Conduct
label: Acepto el Código de Conducta
email:
help: Por favor, introduzca una dirección de correo válida.
label: Correo Electrónico
Expand All @@ -1764,13 +1770,13 @@ es:
name: Nombre
name_hint: Por favor, proporcione su nombre completo.
privacy_policy:
label: I agree to the Privacy Policy
label: Acepto la Política de Privacidad
profile_details: Detalles del Perfil
sign_up: Registrarse
submit: Enviar
terms_of_service:
label: I agree to the Terms of Service
view: View
label: Acepto los Términos de Servicio
view: Ver
signed_up: Bienvenido. Tu cuenta fue creada.
signed_up_but_inactive: Tu cuenta ha sido creada correctamente. Sin embargo,
no hemos podido iniciar la sesión porque tu cuenta aún no está activada.
Expand Down
Loading
Loading