decidim-friendly_signup 0.2 → 0.4.1

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 (25) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +57 -2
  3. data/app/controllers/concerns/decidim/friendly_signup/registrations_redirect.rb +37 -0
  4. data/app/controllers/decidim/friendly_signup/confirmation_codes_controller.rb +50 -0
  5. data/app/forms/concerns/decidim/friendly_signup/auto_nickname.rb +35 -0
  6. data/app/forms/decidim/friendly_signup/confirmation_code_form.rb +33 -0
  7. data/app/mailers/decidim/friendly_signup/confirmation_codes_mailer.rb +22 -0
  8. data/app/models/concerns/decidim/friendly_signup/needs_registration_codes.rb +24 -0
  9. data/app/packs/entrypoints/decidim_friendly_signup.js +1 -0
  10. data/app/packs/entrypoints/decidim_friendly_signup.scss +1 -0
  11. data/app/packs/src/decidim/friendly_signup/lib/instant_validator.js +11 -1
  12. data/app/packs/src/decidim/friendly_signup/setup_confirmations.js +57 -0
  13. data/app/packs/stylesheets/decidim/friendly_signup/_confirmation-codes.scss +23 -0
  14. data/app/views/decidim/devise/invitations/edit.html.erb +8 -6
  15. data/app/views/decidim/devise/passwords/edit.html.erb +2 -2
  16. data/app/views/decidim/devise/registrations/new.html.erb +10 -8
  17. data/app/views/decidim/devise/sessions/new.html.erb +58 -0
  18. data/app/views/decidim/friendly_signup/confirmation_codes/index.html.erb +38 -0
  19. data/app/views/decidim/friendly_signup/confirmation_codes_mailer/confirmation_instructions.html.erb +13 -0
  20. data/config/locales/en.yml +99 -0
  21. data/lib/decidim/friendly_signup/engine.rb +20 -2
  22. data/lib/decidim/friendly_signup/user_attribute_validator.rb +34 -5
  23. data/lib/decidim/friendly_signup/version.rb +1 -1
  24. data/lib/decidim/friendly_signup.rb +17 -0
  25. metadata +13 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c631ec440674f278ea3a0f3369435831014443e36f2f4247d1a87d914e7e66c8
4
- data.tar.gz: cf5ca968cfe4459a63471e02ff36dd38f28cfb042c3146e175c3766dab4a6571
3
+ metadata.gz: 4759703c0e6890c53f828d2db7be0ab349ca526cbe6d064d9d5b809e9429aea7
4
+ data.tar.gz: 6854425281b3d148d6cba8e8ed8d3b91ba3ae462a32b08d4b51540ac995c676f
5
5
  SHA512:
6
- metadata.gz: cdd1a58fad3b2b40cb5f73497c3ca408a621afce743073b40c9fee720a4e9050e0d5a8f3b495e5024b6e807ce7fe8c61859e264259fd3ccec30424d025114bb3
7
- data.tar.gz: b0437d886f9018aaed1a3f405f52032d77c8c01ec7f1f40bb8a61b708138f1eeedbacb424ab394d26048c7bb643d14b3579ddce4954cd5c2af521fea6ffc8f38
6
+ metadata.gz: aca757d63345ea7c3e0207809ccbe744dc976d056ff8f2c860b49af82842f7610ec5f4e5b381d1fd3e8355b0c461995c3f4dc054d5adb1f9fae987fd994fb409
7
+ data.tar.gz: 975dae306e8db7c651e597c7c83aa2bc59c5ae7b16109cc2e0969a307aa9bc575e01a17fe35249cfe772b97e01115871539688e6e9059db5bfdbdc5e1bf89c98
data/README.md CHANGED
@@ -18,9 +18,9 @@ This module simply substitutes some pages to ease up the registration process in
18
18
 
19
19
  - [x] Simplify the password field and add a button with a "show password". ![Show/hide password](examples/passwords.png)
20
20
 
21
- - [ ] Remove the nickname field from the registration process and automatically create one on registering
21
+ - [x] Remove the nickname field from the registration process and automatically create one on registering. ![Hide nickname](examples/nickname.png)
22
22
  - [x] Instant validate parameters when registering without having to send it for backend validation. ![Instant validation](examples/instant_validation.png)
23
- - [ ] Use checkout codes to validate the email instead of a link
23
+ - [x] Use numeric, confirmation codes to validate the email instead of a link. ![Confirmation codes](examples/confirmation_codes.png)
24
24
 
25
25
  ## Installation
26
26
 
@@ -42,6 +42,18 @@ And then execute:
42
42
  bundle
43
43
  ```
44
44
 
45
+ For security reasons, it is also recomended to set a expiration time on confirmation tokens, to do that, make sure your Devise initializer has the variable `confirm_within` to certain amount of time.
46
+
47
+ For instance, you can do that by creating an initializer such as:
48
+
49
+ ```ruby
50
+ # config/initializers/devise.rb
51
+
52
+ Devise.setup do |config|
53
+ config.confirm_within = 12.hours
54
+ end
55
+ ```
56
+
45
57
  **Note:**
46
58
 
47
59
  The correct version of FriendlySignup should resolved automatically by the Bundler.
@@ -51,6 +63,8 @@ Depending on your Decidim version, choose the corresponding FriendlySignup versi
51
63
 
52
64
  | FriendlySignup version | Compatible Decidim versions |
53
65
  |---|---|
66
+ | 0.4.x | 0.26.x |
67
+ | 0.3.x | 0.26.x |
54
68
  | 0.2.x | 0.26.x |
55
69
  | 0.1.x | 0.26.x |
56
70
 
@@ -67,9 +81,50 @@ Decidim::FriendlySignup.configure do |config|
67
81
 
68
82
  # Automatically validate user inputs in the register form (default is true):
69
83
  config.use_instant_validation = false
84
+
85
+ # Hide nickname field and create one automatically from user's name or email (default is true)
86
+ config.hide_nickname = false
87
+
88
+ # Send the users a 4-digit number that needs to be entered in a confirmation page instead of a confirmation link (default is true)
89
+ config.use_confirmation_codes = false
70
90
  end
71
91
  ```
72
92
 
93
+ ### Customize error messages in instant validation
94
+
95
+ You can customize any message either overriding in your application the files in `config/locales/*.yml` or by using a module like Term Customizer.
96
+
97
+ This plugin uses a cascade-style fallback looking for a series of I18n keys and returns the first available. For instance, for the attribute `email` and the validation key `blank` it will look for these 3 possibilities, returning the first matching one:
98
+
99
+ Specific attribute error:
100
+ ```yaml
101
+ en:
102
+ decidim:
103
+ friendly_signup:
104
+ errors:
105
+ messages:
106
+ email:
107
+ blank: Please enter an email address
108
+ ```
109
+
110
+ Generic error:
111
+ ```yaml
112
+ en:
113
+ decidim:
114
+ friendly_signup:
115
+ errors:
116
+ messages:
117
+ blank: Looks like you haven’t entered anything in this field
118
+ ```
119
+
120
+ Rails' default error:
121
+ ```yaml
122
+ en:
123
+ errors:
124
+ messages:
125
+ blank: can't be blank
126
+ ```
127
+
73
128
  ## Contributing
74
129
 
75
130
  Bug reports and pull requests are welcome on GitHub at https://github.com/OpenSourcePolitics/decidim-module-friendly_signup.
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Decidim
6
+ module FriendlySignup
7
+ module RegistrationsRedirect
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ def after_sign_up_path_for(user)
12
+ codes_confirmation_path(user) || super(user)
13
+ end
14
+
15
+ def after_inactive_sign_up_path_for(user)
16
+ codes_confirmation_path(user) || super(user)
17
+ end
18
+
19
+ def after_resending_confirmation_instructions_path_for(resource_name)
20
+ user = Decidim::User.find_by email: params.dig(:user, :email)
21
+ codes_confirmation_path(user) || super(resource_name)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def codes_confirmation_path(user)
28
+ return if Decidim::FriendlySignup.use_confirmation_codes.blank?
29
+ return if user.blank?
30
+ return unless user.inactive_message.to_s == "unconfirmed"
31
+
32
+ set_flash_message! :notice, :signed_up_but_code_required
33
+ decidim_friendly_signup.confirmation_codes_path(confirmation_token: user.confirmation_token)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module FriendlySignup
5
+ class ConfirmationCodesController < Decidim::Devise::ConfirmationsController
6
+ include Decidim::FormFactory
7
+ include NeedsHeaderSnippets
8
+
9
+ before_action :require_unconfirmed_user
10
+ helper_method :user, :confirmation_form
11
+
12
+ def index; end
13
+
14
+ def create
15
+ if confirmation_form.valid?
16
+ user.confirm
17
+ flash[:success] = I18n.t("confirmation_codes.create.user_confirmed", name: user.name, scope: "decidim.friendly_signup")
18
+ return sign_in_and_redirect user, event: :authentication
19
+ end
20
+
21
+ flash.now[:alert] = confirmation_form.errors.messages.values.flatten.join(" ")
22
+ render :index
23
+ end
24
+
25
+ private
26
+
27
+ def confirmation_form
28
+ @confirmation_form ||= form(ConfirmationCodeForm).from_params(params)
29
+ end
30
+
31
+ def user
32
+ @user ||= User.find_by(confirmation_token: params[:confirmation_token], organization: current_organization)
33
+ end
34
+
35
+ def require_unconfirmed_user
36
+ return redirect_to decidim.user_confirmation_path unless FriendlySignup.use_confirmation_codes
37
+
38
+ unless user.present? && !user.confirmed?
39
+ flash[:alert] = I18n.t("confirmation_code_form.invalid_token", scope: "decidim.friendly_signup")
40
+ return redirect_to decidim.new_user_session_path
41
+ end
42
+
43
+ if user.send(:confirmation_period_expired?)
44
+ flash[:alert] = I18n.t("confirmation_code_form.expired", scope: "decidim.friendly_signup")
45
+ redirect_to decidim.user_confirmation_path
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Decidim
6
+ module FriendlySignup
7
+ module AutoNickname
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ def nickname
12
+ return super unless FriendlySignup.hide_nickname
13
+
14
+ UserBaseEntity.nicknamize(name || email, organization: current_organization)
15
+ end
16
+
17
+ private
18
+
19
+ # nickname is removed from the view, so put any (that shouldn't ever happen) error on the base
20
+ def nickname_unique_in_organization
21
+ return false unless nickname
22
+
23
+ if valid_users.find_by("LOWER(nickname)= ? AND decidim_organization_id = ?", nickname.downcase, current_organization.id).present?
24
+ errors.add :base, :taken
25
+ errors.add :nickname, :taken
26
+ end
27
+ end
28
+
29
+ def valid_users
30
+ UserBaseEntity.where(invitation_token: nil)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module FriendlySignup
5
+ class ConfirmationCodeForm < Decidim::Form
6
+ attribute :confirmation_token, String
7
+ attribute :code, Integer
8
+ attribute :confirmation_numbers, Array[Integer]
9
+
10
+ validates :confirmation_token, presence: true
11
+ validate :code_matches_confirmation_token
12
+ validate :user_exists
13
+
14
+ def user_code
15
+ code || confirmation_numbers.map(&:to_s).join("").to_i
16
+ end
17
+
18
+ private
19
+
20
+ def code_matches_confirmation_token
21
+ return if FriendlySignup.confirmation_code(confirmation_token) == user_code
22
+
23
+ errors.add(:code, I18n.t("confirmation_code_form.invalid", scope: "decidim.friendly_signup"))
24
+ end
25
+
26
+ def user_exists
27
+ return if User.exists?(confirmation_token: confirmation_token, organization: current_organization)
28
+
29
+ errors.add(:code, I18n.t("confirmation_code_form.invalid_token", scope: "decidim.friendly_signup"))
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module FriendlySignup
5
+ class ConfirmationCodesMailer < ApplicationMailer
6
+ include Decidim::LocalisedMailer
7
+
8
+ def confirmation_instructions(user, opts)
9
+ @user = user
10
+ @email = opts[:to] || user.email
11
+ @token = opts[:token]
12
+ @organization = user.organization
13
+ @code = FriendlySignup.confirmation_code(@token)
14
+ @expires_at = @user.confirmation_sent_at + @user.class.confirm_within if @user.class.confirm_within
15
+
16
+ with_user(user) do
17
+ mail(to: "#{user.name} <#{@email}>", subject: I18n.t("decidim.friendly_signup.confirmation_codes.mailer.subject", organization: @organization.name))
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Decidim
6
+ module FriendlySignup
7
+ module NeedsRegistrationCodes
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ def send_confirmation_instructions
12
+ return super if Decidim::FriendlySignup.use_confirmation_codes.blank?
13
+ return super unless inactive_message.to_s == "unconfirmed"
14
+
15
+ generate_confirmation_token! unless @raw_confirmation_token
16
+
17
+ opts = pending_reconfirmation? ? { to: unconfirmed_email } : {}
18
+ opts[:token] = @raw_confirmation_token
19
+ ConfirmationCodesMailer.confirmation_instructions(self, opts).deliver_now
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,5 +1,6 @@
1
1
  import "src/decidim/friendly_signup/setup_password"
2
2
  import "src/decidim/friendly_signup/setup_validations"
3
+ import "src/decidim/friendly_signup/setup_confirmations"
3
4
 
4
5
  // CSS
5
6
  import "entrypoints/decidim_friendly_signup.scss";
@@ -1,2 +1,3 @@
1
1
  /* css for decidim_friendly_signup */
2
2
  @import "stylesheets/decidim/friendly_signup/input-groups";
3
+ @import "stylesheets/decidim/friendly_signup/confirmation-codes";
@@ -15,6 +15,10 @@ export default class InstantValidator {
15
15
  }
16
16
 
17
17
  init() {
18
+ if (!this.url || !this.$form.length) {
19
+ return;
20
+ }
21
+ this.$form.foundation("disableValidation");
18
22
  // this final validation prevents abide from resetting the field when user loses focus
19
23
  this.$inputs.on("blur", (evt) => {
20
24
  this.validate($(evt.currentTarget));
@@ -49,10 +53,15 @@ export default class InstantValidator {
49
53
  }
50
54
 
51
55
  validate($input) {
56
+ let $recheck = $($input.data("instantRecheck"));
52
57
  this.tamper($input);
53
58
  this.post($input).done((response) => {
54
59
  this.setFeedback(response, $input);
55
60
  });
61
+
62
+ if ($recheck.length && this.isTampered($recheck)) {
63
+ this.validate($recheck)
64
+ }
56
65
  }
57
66
 
58
67
  setFeedback(data, $input) {
@@ -72,13 +81,14 @@ export default class InstantValidator {
72
81
  }
73
82
 
74
83
  addErrors($dest, msg) {
84
+ console.log("$dest", $dest, "%form", this.$form)
75
85
  if ($dest.closest("label").find(".form-error").length > 1) {
76
86
  // Decidim may add and additional error class that does not play well with abide
77
87
  $dest.closest("label").find(".form-error:last").remove();
78
88
  }
79
89
  this.$form.foundation("addErrorClasses", $dest);
80
90
  if (msg) {
81
- $dest.closest("label").find(".form-error").text(msg);
91
+ $dest.closest("label").find(".form-error").html(msg);
82
92
  }
83
93
  }
84
94
 
@@ -0,0 +1,57 @@
1
+ $(() => {
2
+ const $inputs = $('.confirmation-code-inputs input[type="number"]');
3
+ const $form = $inputs.closest("form");
4
+ const intRegex = /^\d+$/;
5
+ let disableManual = false;
6
+
7
+ // Parses the individual digits into the individual boxes.
8
+ const pasteValues = (element, $first) => {
9
+ const values = element.split("");
10
+ let $inputBox = $first;
11
+
12
+ $(values).each((index) => {
13
+ $inputBox.val(values[index]);
14
+ $inputBox = $inputBox.next('input[type="number"]');
15
+ if ($inputBox.length === 0) {
16
+ $form.submit();
17
+ }
18
+ });
19
+ };
20
+
21
+ $inputs.on("focus", (e) => {
22
+ $(e.target).select();
23
+ });
24
+
25
+ // Prevents user from manually entering non-digits.
26
+ $inputs.on("input.fromManual", (e) => {
27
+ if (disableManual) {
28
+ return;
29
+ }
30
+ const $this = $(e.target);
31
+ if (intRegex.test($this.val())) {
32
+ pasteValues($this.val(), $this);
33
+ $this.next('input[type="number"]').focus();
34
+ } else {
35
+ $this.val("");
36
+ }
37
+ });
38
+
39
+ $inputs.on("paste", (evt) => {
40
+ const $this = $(evt.target);
41
+ const originalValue = $this.val();
42
+ $this.val("");
43
+ disableManual = true;
44
+ $this.one("input.fromPaste", (e) => {
45
+ const $currentInputBox = $(e.target);
46
+
47
+ const pastedValue = $currentInputBox.val();
48
+ if (intRegex.test(pastedValue)) {
49
+ pasteValues(pastedValue, $inputs.eq(0));
50
+ }
51
+ else {
52
+ $this.val(originalValue);
53
+ }
54
+ disableManual = false;
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,23 @@
1
+ .confirmation-code-inputs {
2
+ display: flex;
3
+ justify-content: center;
4
+ margin: 2rem;
5
+
6
+ input[type="number"] {
7
+ width: 15%;
8
+ font-size: 5em;
9
+ height: 1.2em;
10
+ line-height: 1;
11
+ padding: 0;
12
+ margin: 0 2%;
13
+ text-align: center;
14
+ box-shadow: 1px 3px 3px #bbb;
15
+ -moz-appearance: textfield;
16
+
17
+ &::-webkit-inner-spin-button,
18
+ &::-webkit-outer-spin-button {
19
+ -webkit-appearance: none;
20
+ margin: 0;
21
+ }
22
+ }
23
+ }
@@ -10,7 +10,7 @@
10
10
 
11
11
  <div class="row">
12
12
  <div class="columns large-6 medium-10 medium-centered">
13
- <%= decidim_form_for resource, namespace: "invitation", as: resource_name, url: invitation_path(resource_name, invite_redirect: params[:invite_redirect]), html: { method: :put, class: "register-form new_user" } do |f| %>
13
+ <%= decidim_form_for(resource, namespace: "invitation", as: resource_name, url: invitation_path(resource_name, invite_redirect: params[:invite_redirect]), html: { method: :put, class: "register-form new_user#{friendly_override_activated?(:use_instant_validation) ? ' instant-validation' : ''}" } , data: { "validation-url" => decidim_friendly_signup.validate_path }) do |f| %>
14
14
  <div class="card">
15
15
  <div class="card__content">
16
16
  <legend><%= t("sign_up_as.legend", scope: "decidim.devise.registrations.new") %></legend>
@@ -19,14 +19,16 @@
19
19
 
20
20
  <%= f.hidden_field :invitation_token %>
21
21
 
22
- <div class="user-nickname">
23
- <div class="field">
24
- <%= f.text_field :nickname, help_text: t("devise.invitations.edit.nickname_help", organization: current_organization.name), required: "required", prefix: { value: "@", small: 1, large: 1 } %>
22
+ <% unless friendly_override_activated?(:hide_nickname) %>
23
+ <div class="user-nickname">
24
+ <div class="field">
25
+ <%= f.text_field :nickname, help_text: t("devise.invitations.edit.nickname_help", organization: current_organization.name), required: "required", prefix: { value: "@", small: 1, large: 1 }, data: { "instant-attribute" => "nickname", "instant-recheck" => "password" } %>
26
+ </div>
25
27
  </div>
26
- </div>
28
+ <% end %>
27
29
 
28
30
  <% if f.object.class.require_password_on_accepting %>
29
- <%= render("decidim/friendly_signup/shared/password_fields", form: f, options: { required: "required", minlength: ::PasswordValidator::MINIMUM_LENGTH, maxlength: ::PasswordValidator::MAX_LENGTH }) %>
31
+ <%= render("decidim/friendly_signup/shared/password_fields", form: f, options: { required: true, autocomplete: "off", help_text: t("devise.passwords.edit.password_help", minimun_characters: ::PasswordValidator::MINIMUM_LENGTH), minlength: ::PasswordValidator::MINIMUM_LENGTH, maxlength: ::PasswordValidator::MAX_LENGTH, data: { "instant-attribute" => "password" } }) %>
30
32
  <% end %>
31
33
  </div>
32
34
  </div>
@@ -10,12 +10,12 @@
10
10
  <div class="columns medium-7 large-5 medium-centered">
11
11
  <div class="card">
12
12
  <div class="card__content">
13
- <%= decidim_form_for(resource, namespace: "password", as: resource_name, url: password_path(resource_name), html: { method: :put, class: "register-form new_user" }) do |f| %>
13
+ <%= decidim_form_for(resource, namespace: "password", as: resource_name, url: password_path(resource_name), html: { method: :put, class: "register-form new_user#{friendly_override_activated?(:use_instant_validation) ? ' instant-validation' : ''}" } , data: { "validation-url" => decidim_friendly_signup.validate_path }) do |f| %>
14
14
  <%= form_required_explanation %>
15
15
 
16
16
  <%= f.hidden_field :reset_password_token %>
17
17
 
18
- <%= render("decidim/friendly_signup/shared/password_fields", form: f, options: { autocomplete: "off", help_text: t("devise.passwords.edit.password_help", minimun_characters: ::PasswordValidator::MINIMUM_LENGTH) }) %>
18
+ <%= render("decidim/friendly_signup/shared/password_fields", form: f, options: { required: true, autocomplete: "off", help_text: t("devise.passwords.edit.password_help", minimun_characters: ::PasswordValidator::MINIMUM_LENGTH), minlength: ::PasswordValidator::MINIMUM_LENGTH, maxlength: ::PasswordValidator::MAX_LENGTH, data: { "instant-attribute" => "password" } }) %>
19
19
 
20
20
  <div class="actions">
21
21
  <%= f.submit t("devise.passwords.edit.change_my_password"), class: "button expanded" %>
@@ -26,7 +26,7 @@
26
26
  <div class="row">
27
27
  <div class="columns large-6 medium-10 medium-centered">
28
28
 
29
- <%= decidim_form_for(@form, namespace: "registration", as: resource_name, url: registration_path(resource_name), html: { class: "register-form new_user#{friendly_override_activated?(:use_instant_validation) && ' instant-validation'}", id: "register-form" }, data: { "validation-url": decidim_friendly_signup.validate_path }) do |f| %>
29
+ <%= decidim_form_for(@form, namespace: "registration", as: resource_name, url: registration_path(resource_name), html: { class: "register-form new_user#{friendly_override_activated?(:use_instant_validation) ? ' instant-validation' : ''}", id: "register-form" }, data: { "validation-url" => decidim_friendly_signup.validate_path }) do |f| %>
30
30
  <%= invisible_captcha %>
31
31
  <div class="card">
32
32
  <div class="card__content">
@@ -34,21 +34,23 @@
34
34
 
35
35
  <div class="user-person">
36
36
  <div class="field">
37
- <%= f.text_field :name, help_text: t(".username_help"), autocomplete: "off" %>
37
+ <%= f.text_field :name, help_text: t(".username_help"), autocomplete: "off", data: { "instant-attribute" => "name", "instant-recheck" => "#registration_user_password" } %>
38
38
  </div>
39
39
  </div>
40
40
 
41
- <div class="user-nickname">
42
- <div class="field">
43
- <%= f.text_field :nickname, help_text: t(".nickname_help", organization: current_organization.name), prefix: { value: "@", small: 1, large: 1 }, autocomplete: "off" %>
41
+ <% unless friendly_override_activated?(:hide_nickname) %>
42
+ <div class="user-nickname">
43
+ <div class="field">
44
+ <%= f.text_field :nickname, help_text: t(".nickname_help", organization: current_organization.name), prefix: { value: "@", small: 1, large: 1 }, autocomplete: "off", data: { "instant-attribute" => "nickname", "instant-recheck" => "#registration_user_password" } %>
45
+ </div>
44
46
  </div>
45
- </div>
47
+ <% end %>
46
48
 
47
49
  <div class="field">
48
- <%= f.email_field :email, autocomplete: "email", data: { "instant-attribute": "email" } %>
50
+ <%= f.email_field :email, autocomplete: "email", data: { "instant-attribute" => "email", "instant-recheck" => "#registration_user_password" } %>
49
51
  </div>
50
52
 
51
- <%= render("decidim/friendly_signup/shared/password_fields", form: f, options: { required: true, help_text: t(".password_help", minimun_characters: ::PasswordValidator::MINIMUM_LENGTH), autocomplete: "off", data: { "instant-attribute": "password" } }) %>
53
+ <%= render("decidim/friendly_signup/shared/password_fields", form: f, options: { required: true, help_text: t(".password_help", minimun_characters: ::PasswordValidator::MINIMUM_LENGTH), autocomplete: "off", data: { "instant-attribute" => "password" } }) %>
52
54
  </div>
53
55
  </div>
54
56
 
@@ -0,0 +1,58 @@
1
+ <% add_decidim_page_title(t("devise.sessions.new.sign_in")) %>
2
+
3
+ <div class="wrapper">
4
+ <div class="row collapse">
5
+ <div class="row collapse">
6
+ <div class="columns large-8 large-centered text-center page-title">
7
+ <h1><%= t("devise.sessions.new.sign_in") %></h1>
8
+ <% if current_organization.sign_up_enabled? %>
9
+ <p>
10
+ <%= t(".are_you_new?") %>
11
+ <%= link_to t(".register"), new_user_registration_path %>
12
+ </p>
13
+ <% elsif current_organization.sign_in_enabled? %>
14
+ <p>
15
+ <%= t(".sign_up_disabled") %>
16
+ </p>
17
+ <% else %>
18
+ <p>
19
+ <%= t(".sign_in_disabled") %>
20
+ </p>
21
+ <% end %>
22
+ </div>
23
+ </div>
24
+ <% cache current_organization do %>
25
+ <%= render "decidim/devise/shared/omniauth_buttons" %>
26
+ <% end %>
27
+
28
+ <% if current_organization.sign_in_enabled? %>
29
+ <div class="row">
30
+ <div class="columns large-6 medium-centered">
31
+ <div class="card">
32
+ <div class="card__content">
33
+ <%= decidim_form_for(resource, namespace: "session", as: resource_name, url: session_path(resource_name), html: { class: "register-form new_user" }) do |f| %>
34
+ <div>
35
+ <div class="field">
36
+ <%= f.email_field :email %>
37
+ </div>
38
+ <div class="field">
39
+ <%= render("decidim/friendly_signup/shared/password_fields", form: f, options: { autocomplete: "off" }) %>
40
+ </div>
41
+ </div>
42
+ <% if devise_mapping.rememberable? %>
43
+ <div class="field">
44
+ <%= f.check_box :remember_me %>
45
+ </div>
46
+ <% end %>
47
+ <div class="actions">
48
+ <%= f.submit t("devise.sessions.new.sign_in"), class: "button expanded" %>
49
+ </div>
50
+ <% end %>
51
+ <%= render "decidim/devise/shared/links" %>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ <% end %>
57
+ </div>
58
+ </div>
@@ -0,0 +1,38 @@
1
+ <% add_decidim_page_title(t(".enter_code")) %>
2
+
3
+ <div class="wrapper">
4
+ <div class="row collapse">
5
+ <div class="row collapse">
6
+ <div class="columns large-8 large-centered text-center page-title">
7
+ <h1><%= t(".title") %></h1>
8
+ <h6><%= t(".subtitle") %></h6>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="row">
13
+ <div class="columns medium-10 large-8 medium-centered">
14
+ <div class="card">
15
+ <div class="card__content">
16
+ <p class="text-center"><%= t(".description", email: "<strong>#{user.email}</strong>").html_safe %></p>
17
+ <%= decidim_form_for(confirmation_form, url: decidim_friendly_signup.confirmation_codes_path(confirmation_token: confirmation_form.confirmation_token)) do |f| %>
18
+ <div class="field confirmation-code-inputs">
19
+ <%= number_field_tag "confirmation_numbers[0]", "", autofocus: true, autocomplete: :off %>
20
+ <%= number_field_tag "confirmation_numbers[1]", "", autocomplete: :off %>
21
+ <%= number_field_tag "confirmation_numbers[2]", "", autocomplete: :off %>
22
+ <%= number_field_tag "confirmation_numbers[3]", "", autocomplete: :off %>
23
+ </div>
24
+
25
+ <div class="actions text-center">
26
+ <%= f.submit t(".verify"), class: "button text-uppercase" %>
27
+ </div>
28
+ <% end %>
29
+
30
+ <p class="text-center">
31
+ <%= link_to t(".code_not_received"), decidim.user_confirmation_path(confirmation_token: confirmation_form.confirmation_token, "user[email]" => user.email), method: :post, data: { confirm: t(".confirm_send_code", email: user.email) } %>
32
+ </p>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </div>
@@ -0,0 +1,13 @@
1
+ <h1 class="text-center"><%= t(".title") %></h1>
2
+
3
+ <p class="email-instructions text-center"><%= t(".subtitle", organization: link_to(@organization.name, decidim_friendly_signup.confirmation_codes_path(confirmation_token: @token))).html_safe %></p>
4
+
5
+ <h3 class="email-instruction text-center" style="margin:1em 0 0.2em 0;"><%= t(".copy") %></h3>
6
+
7
+ <p class="stat text-center" style="margin:1em 0 0.5em 0;"><b style="font-size: 2em"><%= @code %></b></p>
8
+
9
+ <% if @expires_at %>
10
+ <p class="email-small text-center" style="margin-bottom: 2em;"><%= t(".expires_in", time: distance_of_time_in_words(Time.now, @expires_at, scope: "decidim.friendly_signup.datetime.distance_in_words")) %></p>
11
+ <% end %>
12
+
13
+ <p class="email-small email-closing text-center"><%= t(".ignore").html_safe %></p>
@@ -1,10 +1,109 @@
1
1
  ---
2
2
  en:
3
+ activemodel:
4
+ attributes:
5
+ confirmation_code:
6
+ code: Confirmation code
3
7
  decidim:
4
8
  friendly_signup:
9
+ confirmation_code_form:
10
+ expired: Sorry, this code has expired, please generate a new one.
11
+ invalid: Code is invalid, please make sure to copy the code emailed to you.
12
+ invalid_token: Invalid token.
13
+ confirmation_codes:
14
+ create:
15
+ user_confirmed: Welcome %{name}! Your account has been succesfully confirmed!
16
+ index:
17
+ code_not_received: Didn't receive the code?
18
+ confirm_send_code: Do you want to resend the confirmation code to %{email}?<br>Be
19
+ sure to check the spam folder just in case!
20
+ description: You should receive a 4 digit code at %{email}.<br>If you can't
21
+ find it, please check your spam folder or wait up to 10 minutes.
22
+ enter_code: Enter code
23
+ subtitle: You need to confirm your email address to be able to submit proposals,
24
+ comment and vote.
25
+ title: One last step...
26
+ verify: Verify
27
+ mailer:
28
+ subject: Confirm your account at %{organization}
29
+ resend_code:
30
+ sent: The confirmation code has been sent to %{email}.
31
+ confirmation_codes_mailer:
32
+ confirmation_instructions:
33
+ copy: 'Copy this code:'
34
+ expires_in: It will expire in %{time}.
35
+ ignore: |-
36
+ If you didn't request this comunication, please ignore this email.<br />
37
+ Your account won't be active until your account is fully confirmed.
38
+ subtitle: To finalize the registration you just need to copy the 4 digit
39
+ code below, go back to the %{organization} signup page and paste it!
40
+ title: You're almost there!
41
+ datetime:
42
+ distance_in_words:
43
+ about_x_hours:
44
+ one: about 1 hour
45
+ other: about %{count} hours
46
+ about_x_months:
47
+ one: about 1 month
48
+ other: about %{count} months
49
+ about_x_years:
50
+ one: about 1 year
51
+ other: about %{count} years
52
+ almost_x_years:
53
+ one: almost 1 year
54
+ other: almost %{count} years
55
+ half_a_minute: half a minute
56
+ less_than_x_minutes:
57
+ one: less than a minute
58
+ other: less than %{count} minutes
59
+ less_than_x_seconds:
60
+ one: less than 1 second
61
+ other: less than %{count} seconds
62
+ over_x_years:
63
+ one: over 1 year
64
+ other: over %{count} years
65
+ x_days:
66
+ one: 1 day
67
+ other: "%{count} days"
68
+ x_minutes:
69
+ one: 1 minute
70
+ other: "%{count} minutes"
71
+ x_months:
72
+ one: 1 month
73
+ other: "%{count} months"
74
+ x_seconds:
75
+ one: 1 second
76
+ other: "%{count} seconds"
77
+ errors:
78
+ messages:
79
+ blank: Looks like you haven’t entered anything in this field
80
+ email:
81
+ blank: Please enter an email address
82
+ invalid: The email address looks incomplete
83
+ taken: This email is already in use for another account. Try signing in
84
+ or use another email
85
+ password:
86
+ email_included_in_password: The password you have entered is too similar
87
+ to your email
88
+ name_included_in_password: The password you have entered is too similar
89
+ to your name
90
+ nickname_included_in_password: The password you have entered is too similar
91
+ to your nickname
92
+ not_enough_unique_characters: The password you have entered does not have
93
+ enough different characters
94
+ password_too_common: The password you have entered is very common - we
95
+ suggest using a different password
96
+ password_too_short: The password you have entered is too short
5
97
  shared:
6
98
  password_fields:
7
99
  hidden_password: Your password is hidden
8
100
  hide_password: Hide password
9
101
  show_password: Show password
10
102
  shown_password: Your password is shown
103
+ devise:
104
+ confirmations:
105
+ signed_up_but_code_required: A message with a code has been sent to your email
106
+ address. Please copy and paste the received code in this page.
107
+ registrations:
108
+ signed_up_but_code_required: A message with a code has been sent to your email
109
+ address. Please copy and paste the received code in this page.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "rails"
4
4
  require "decidim/core"
5
+ require "rack/attack"
5
6
 
6
7
  module Decidim
7
8
  module FriendlySignup
@@ -10,20 +11,37 @@ module Decidim
10
11
  isolate_namespace Decidim::FriendlySignup
11
12
 
12
13
  routes do
14
+ devise_scope :user do
15
+ resources :confirmation_codes, only: [:index, :create]
16
+ end
13
17
  post :validate, to: "validator#validate"
18
+ put :validate, to: "validator#validate"
14
19
  end
15
20
 
16
21
  # Prepare a zone to create overrides
17
22
  # https://edgeguides.rubyonrails.org/engines.html#overriding-models-and-controllers
18
23
  # overrides
19
24
  config.after_initialize do
25
+ Decidim::Devise::SessionsController.include(Decidim::FriendlySignup::NeedsHeaderSnippets)
20
26
  Decidim::Devise::RegistrationsController.include(Decidim::FriendlySignup::NeedsHeaderSnippets)
21
- Decidim::Devise::InvitationsController.include(Decidim::FriendlySignup::NeedsHeaderSnippets)
22
27
  Decidim::Devise::PasswordsController.include(Decidim::FriendlySignup::NeedsHeaderSnippets)
28
+ Decidim::Devise::InvitationsController.include(Decidim::FriendlySignup::NeedsHeaderSnippets)
29
+ Decidim::Devise::RegistrationsController.include(Decidim::FriendlySignup::RegistrationsRedirect)
30
+ Decidim::Devise::ConfirmationsController.include(Decidim::FriendlySignup::RegistrationsRedirect)
23
31
  Decidim::AccountController.include(Decidim::FriendlySignup::NeedsHeaderSnippets)
32
+ Decidim::RegistrationForm.include(Decidim::FriendlySignup::AutoNickname)
33
+ Decidim::User.include(Decidim::FriendlySignup::NeedsRegistrationCodes)
34
+ end
35
+
36
+ initializer "friendly_signup.confirmation_throttling" do
37
+ # Throttle confirmation attempts for a given code parameter to 6 reqs/minute
38
+ # Return the confirmation_token as a discriminator on POST /users/sign_in requests
39
+ Rack::Attack.throttle("limit confirmations attempts per code", limit: 5, period: 60.seconds) do |request|
40
+ request.params["confirmation_token"] if request.path == "/friendly_signup/confirmation_codes" && request.post?
41
+ end
24
42
  end
25
43
 
26
- initializer "FriendlySignup.webpacker.assets_path" do
44
+ initializer "friendly_signup.webpacker.assets_path" do
27
45
  Decidim.register_assets_path File.expand_path("app/packs", root)
28
46
  end
29
47
  end
@@ -28,11 +28,44 @@ module Decidim
28
28
  end
29
29
 
30
30
  def error
31
- errors.flatten.map(&:upcase_first).join(". ") unless valid?
31
+ return if valid?
32
+
33
+ errors.map do |msg|
34
+ key = find_key(msg)
35
+ next if key == :nickname_included_in_password && FriendlySignup.hide_nickname.present?
36
+
37
+ custom_error(key).presence || msg.upcase_first
38
+ end.join(".<br>")
32
39
  end
33
40
 
34
41
  private
35
42
 
43
+ def custom_error(key)
44
+ return if key.blank?
45
+
46
+ generic = I18n.t(key, scope: "decidim.friendly_signup.errors.messages", default: "")
47
+ I18n.t("#{attribute}.#{key}", scope: "decidim.friendly_signup.errors.messages", default: generic)
48
+ end
49
+
50
+ def find_key(msg)
51
+ case attribute
52
+ when "password"
53
+ [:blacklisted,
54
+ :domain_included_in_password,
55
+ :email_included_in_password,
56
+ :fallback,
57
+ :name_included_in_password,
58
+ :nickname_included_in_password,
59
+ :not_enough_unique_characters,
60
+ :password_not_allowed,
61
+ :password_too_common,
62
+ :password_too_long,
63
+ :password_too_short].find { |key| msg == I18n.t(key, scope: "password_validator") }
64
+ else
65
+ [:blank, :invalid, :taken].find { |key| msg == I18n.t(key, scope: "errors.messages") }
66
+ end
67
+ end
68
+
36
69
  def valid_attribute?
37
70
  %w(nickname email name password).include? attribute.to_s
38
71
  end
@@ -40,10 +73,6 @@ module Decidim
40
73
  def valid_suggestor?
41
74
  ["nickname"].include? attribute.to_s
42
75
  end
43
-
44
- def valid_users
45
- Decidim::UserBaseEntity.where(invitation_token: nil, organization: current_organization)
46
- end
47
76
  end
48
77
  end
49
78
  end
@@ -5,6 +5,6 @@ module Decidim
5
5
  module FriendlySignup
6
6
  DECIDIM_VERSION = "0.26.2"
7
7
  COMPAT_DECIDIM_VERSION = "~> 0.26.0"
8
- VERSION = "0.2"
8
+ VERSION = "0.4.1"
9
9
  end
10
10
  end
@@ -18,6 +18,23 @@ module Decidim
18
18
  config_accessor :use_instant_validation do
19
19
  true
20
20
  end
21
+
22
+ # Whether to hide nickname and generate it automatically
23
+ config_accessor :hide_nickname do
24
+ true
25
+ end
26
+
27
+ # Use confirmation codes instead of confirmation links
28
+ config_accessor :use_confirmation_codes do
29
+ true
30
+ end
31
+
32
+ # Generates a secure code from a string
33
+ def self.confirmation_code(hash)
34
+ num = Decidim::Tokenizer.new(salt: Rails.application.secret_key_base).int_digest(hash).to_s[0..3]
35
+ num += "0" while num.size < 4
36
+ num.to_i
37
+ end
21
38
  end
22
39
  end
23
40
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decidim-friendly_signup
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Vergés
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-13 00:00:00.000000000 Z
11
+ date: 2022-07-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: decidim-core
@@ -49,20 +49,31 @@ files:
49
49
  - README.md
50
50
  - Rakefile
51
51
  - app/controllers/concerns/decidim/friendly_signup/needs_header_snippets.rb
52
+ - app/controllers/concerns/decidim/friendly_signup/registrations_redirect.rb
52
53
  - app/controllers/decidim/friendly_signup/application_controller.rb
54
+ - app/controllers/decidim/friendly_signup/confirmation_codes_controller.rb
53
55
  - app/controllers/decidim/friendly_signup/validator_controller.rb
56
+ - app/forms/concerns/decidim/friendly_signup/auto_nickname.rb
57
+ - app/forms/decidim/friendly_signup/confirmation_code_form.rb
58
+ - app/mailers/decidim/friendly_signup/confirmation_codes_mailer.rb
59
+ - app/models/concerns/decidim/friendly_signup/needs_registration_codes.rb
54
60
  - app/packs/entrypoints/decidim_friendly_signup.js
55
61
  - app/packs/entrypoints/decidim_friendly_signup.scss
56
62
  - app/packs/images/decidim/friendly_signup/icon.svg
57
63
  - app/packs/src/decidim/friendly_signup/lib/instant_validator.js
58
64
  - app/packs/src/decidim/friendly_signup/lib/password_toggler.js
65
+ - app/packs/src/decidim/friendly_signup/setup_confirmations.js
59
66
  - app/packs/src/decidim/friendly_signup/setup_password.js
60
67
  - app/packs/src/decidim/friendly_signup/setup_validations.js
68
+ - app/packs/stylesheets/decidim/friendly_signup/_confirmation-codes.scss
61
69
  - app/packs/stylesheets/decidim/friendly_signup/_input-groups.scss
62
70
  - app/views/decidim/account/_password_fields.html.erb
63
71
  - app/views/decidim/devise/invitations/edit.html.erb
64
72
  - app/views/decidim/devise/passwords/edit.html.erb
65
73
  - app/views/decidim/devise/registrations/new.html.erb
74
+ - app/views/decidim/devise/sessions/new.html.erb
75
+ - app/views/decidim/friendly_signup/confirmation_codes/index.html.erb
76
+ - app/views/decidim/friendly_signup/confirmation_codes_mailer/confirmation_instructions.html.erb
66
77
  - app/views/decidim/friendly_signup/shared/_password_fields.html.erb
67
78
  - config/assets.rb
68
79
  - config/i18n-tasks.yml