decidim-friendly_signup 0.3 → 0.4.2

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 +52 -1
  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/decidim/friendly_signup/confirmation_code_form.rb +33 -0
  6. data/app/mailers/decidim/friendly_signup/confirmation_codes_mailer.rb +22 -0
  7. data/app/models/concerns/decidim/friendly_signup/needs_registration_codes.rb +24 -0
  8. data/app/packs/entrypoints/decidim_friendly_signup.js +1 -0
  9. data/app/packs/entrypoints/decidim_friendly_signup.scss +1 -0
  10. data/app/packs/src/decidim/friendly_signup/lib/instant_validator.js +10 -1
  11. data/app/packs/src/decidim/friendly_signup/setup_confirmations.js +57 -0
  12. data/app/packs/stylesheets/decidim/friendly_signup/_confirmation-codes.scss +23 -0
  13. data/app/views/decidim/devise/invitations/edit.html.erb +8 -6
  14. data/app/views/decidim/devise/passwords/edit.html.erb +2 -2
  15. data/app/views/decidim/devise/registrations/new.html.erb +5 -5
  16. data/app/views/decidim/devise/sessions/new.html.erb +58 -0
  17. data/app/views/decidim/friendly_signup/confirmation_codes/index.html.erb +38 -0
  18. data/app/views/decidim/friendly_signup/confirmation_codes_mailer/confirmation_instructions.html.erb +13 -0
  19. data/app/views/decidim/friendly_signup/shared/_password_fields.html.erb +13 -7
  20. data/config/locales/en.yml +103 -0
  21. data/lib/decidim/friendly_signup/engine.rb +19 -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 +12 -0
  25. metadata +12 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 88bd76deb088270153f2b8d9dc3376d1f6333c0429e2433c2f896870027cbd30
4
- data.tar.gz: d2e2efec13ef916c6835e8f09b4d50e8e20ddef3c05a1f41bf24b51a1054c074
3
+ metadata.gz: 156a94366f47dc7f364338feca03ba824d6e9d38a16b9c8f8a6fbac4774ccbd1
4
+ data.tar.gz: 131f4becfb5b981f0f026a970b0b38cf98fdb2d5970bc43873ad942d360df76d
5
5
  SHA512:
6
- metadata.gz: 4a23dbcb2623314b4fcfbe30b8025b97309049beb28a2ab66ab0bb070498624509805054e2fc476f04f078fd56ed3161f49c66accae5b5d06b9537547d1fcbdd
7
- data.tar.gz: f5312068c52ccb019f87e1af5beabc72e7f85396aee34ab4b1799001dcb905e17962c042186c55b749af4a4a69bdb19b7b915cfad4581e17bccb8e96529e31fe
6
+ metadata.gz: 0a384da56675c80fc63b1d3c8f1a197e5ac46658395caa5926dbedc6d3a20a37648dc7f50183ebe69d601940fa50b87f8ee124109a4dd44e12fdf137f79d2ec1
7
+ data.tar.gz: a5622ee4f5bbc17f42abbb24922007332df97361a7f00ca39702b4965e3117483170aafd15da5c68e38053606e9736bfae20b0f1414377eb1c522060a7f1e9da
data/README.md CHANGED
@@ -20,7 +20,7 @@ This module simply substitutes some pages to ease up the registration process in
20
20
 
21
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,7 @@ 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 |
54
67
  | 0.3.x | 0.26.x |
55
68
  | 0.2.x | 0.26.x |
56
69
  | 0.1.x | 0.26.x |
@@ -71,9 +84,47 @@ Decidim::FriendlySignup.configure do |config|
71
84
 
72
85
  # Hide nickname field and create one automatically from user's name or email (default is true)
73
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
74
90
  end
75
91
  ```
76
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
+
77
128
  ## Contributing
78
129
 
79
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,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, code: @code))
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,9 @@ export default class InstantValidator {
15
15
  }
16
16
 
17
17
  init() {
18
+ if (!this.url || !this.$form.length) {
19
+ return;
20
+ }
18
21
  this.$form.foundation("disableValidation");
19
22
  // this final validation prevents abide from resetting the field when user loses focus
20
23
  this.$inputs.on("blur", (evt) => {
@@ -50,10 +53,15 @@ export default class InstantValidator {
50
53
  }
51
54
 
52
55
  validate($input) {
56
+ let $recheck = $($input.data("instantRecheck"));
53
57
  this.tamper($input);
54
58
  this.post($input).done((response) => {
55
59
  this.setFeedback(response, $input);
56
60
  });
61
+
62
+ if ($recheck.length && this.isTampered($recheck)) {
63
+ this.validate($recheck)
64
+ }
57
65
  }
58
66
 
59
67
  setFeedback(data, $input) {
@@ -73,13 +81,14 @@ export default class InstantValidator {
73
81
  }
74
82
 
75
83
  addErrors($dest, msg) {
84
+ console.log("$dest", $dest, "%form", this.$form)
76
85
  if ($dest.closest("label").find(".form-error").length > 1) {
77
86
  // Decidim may add and additional error class that does not play well with abide
78
87
  $dest.closest("label").find(".form-error:last").remove();
79
88
  }
80
89
  this.$form.foundation("addErrorClasses", $dest);
81
90
  if (msg) {
82
- $dest.closest("label").find(".form-error").text(msg);
91
+ $dest.closest("label").find(".form-error").html(msg);
83
92
  }
84
93
  }
85
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,23 +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", data: { "instant-attribute": "nickname" } %>
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
41
  <% unless friendly_override_activated?(:hide_nickname) %>
42
42
  <div class="user-nickname">
43
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" } %>
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
45
  </div>
46
46
  </div>
47
47
  <% end %>
48
48
 
49
49
  <div class="field">
50
- <%= 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" } %>
51
51
  </div>
52
52
 
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" } }) %>
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" } }) %>
54
54
  </div>
55
55
  </div>
56
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, required: true, pattern: "email" %>
37
+ </div>
38
+ <div class="field">
39
+ <%= render("decidim/friendly_signup/shared/password_fields", form: f, options: { autocomplete: "off" }, skip_confirmation: true) %>
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>
@@ -9,17 +9,23 @@
9
9
  </div>
10
10
  </div>
11
11
 
12
- <div class="user-password-confirmation">
13
- <div class="field">
14
- <%= form.password_field :password_confirmation %>
12
+ <% unless defined?(skip_confirmation) && skip_confirmation %>
13
+ <div class="user-password-confirmation">
14
+ <div class="field">
15
+ <%= form.password_field :password_confirmation %>
16
+ </div>
15
17
  </div>
16
- </div>
18
+ <% end %>
19
+
17
20
  <% else %>
18
21
  <div class="field">
19
22
  <%= form.password_field :password, options %>
20
23
  </div>
21
24
 
22
- <div class="field">
23
- <%= form.password_field :password_confirmation %>
24
- </div>
25
+ <% unless defined?(skip_confirmation) && skip_confirmation %>
26
+ <div class="field">
27
+ <%= form.password_field :password_confirmation %>
28
+ </div>
29
+ <% end %>
30
+
25
31
  <% end %>
@@ -1,10 +1,113 @@
1
1
  ---
2
2
  en:
3
+ activemodel:
4
+ attributes:
5
+ confirmation_code:
6
+ code: Confirmation code
3
7
  decidim:
8
+ forms:
9
+ errors:
10
+ decidim/user:
11
+ email: Please enter a valid email address
4
12
  friendly_signup:
13
+ confirmation_code_form:
14
+ expired: Sorry, this code has expired, please generate a new one.
15
+ invalid: Code is invalid, please make sure to copy the code emailed to you.
16
+ invalid_token: Invalid token.
17
+ confirmation_codes:
18
+ create:
19
+ user_confirmed: Welcome %{name}! Your account has been succesfully confirmed!
20
+ index:
21
+ code_not_received: Didn't receive the code?
22
+ confirm_send_code: Do you want to resend the confirmation code to %{email}?<br>Be
23
+ sure to check the spam folder just in case!
24
+ description: You should receive a 4 digit code at %{email}.<br>If you can't
25
+ find it, please check your spam folder or wait up to 10 minutes.
26
+ enter_code: Enter code
27
+ subtitle: You need to confirm your email address to be able to submit proposals,
28
+ comment and vote.
29
+ title: One last step...
30
+ verify: Verify
31
+ mailer:
32
+ subject: "%{code} is your confirmation code for %{organization}"
33
+ resend_code:
34
+ sent: The confirmation code has been sent to %{email}.
35
+ confirmation_codes_mailer:
36
+ confirmation_instructions:
37
+ copy: 'Copy this code:'
38
+ expires_in: It will expire in %{time}.
39
+ ignore: |-
40
+ If you didn't request this comunication, please ignore this email.<br />
41
+ Your account won't be active until your account is fully confirmed.
42
+ subtitle: To finalize the registration you just need to copy the 4 digit
43
+ code below, go back to the %{organization} signup page and paste it!
44
+ title: You're almost there!
45
+ datetime:
46
+ distance_in_words:
47
+ about_x_hours:
48
+ one: about 1 hour
49
+ other: about %{count} hours
50
+ about_x_months:
51
+ one: about 1 month
52
+ other: about %{count} months
53
+ about_x_years:
54
+ one: about 1 year
55
+ other: about %{count} years
56
+ almost_x_years:
57
+ one: almost 1 year
58
+ other: almost %{count} years
59
+ half_a_minute: half a minute
60
+ less_than_x_minutes:
61
+ one: less than a minute
62
+ other: less than %{count} minutes
63
+ less_than_x_seconds:
64
+ one: less than 1 second
65
+ other: less than %{count} seconds
66
+ over_x_years:
67
+ one: over 1 year
68
+ other: over %{count} years
69
+ x_days:
70
+ one: 1 day
71
+ other: "%{count} days"
72
+ x_minutes:
73
+ one: 1 minute
74
+ other: "%{count} minutes"
75
+ x_months:
76
+ one: 1 month
77
+ other: "%{count} months"
78
+ x_seconds:
79
+ one: 1 second
80
+ other: "%{count} seconds"
81
+ errors:
82
+ messages:
83
+ blank: Looks like you haven’t entered anything in this field
84
+ email:
85
+ blank: Please enter an email address
86
+ invalid: The email address looks incomplete
87
+ taken: This email is already in use for another account. Try signing in
88
+ or use another email
89
+ password:
90
+ email_included_in_password: The password you have entered is too similar
91
+ to your email
92
+ name_included_in_password: The password you have entered is too similar
93
+ to your name
94
+ nickname_included_in_password: The password you have entered is too similar
95
+ to your nickname
96
+ not_enough_unique_characters: The password you have entered does not have
97
+ enough different characters
98
+ password_too_common: The password you have entered is very common - we
99
+ suggest using a different password
100
+ password_too_short: The password you have entered is too short
5
101
  shared:
6
102
  password_fields:
7
103
  hidden_password: Your password is hidden
8
104
  hide_password: Hide password
9
105
  show_password: Show password
10
106
  shown_password: Your password is shown
107
+ devise:
108
+ confirmations:
109
+ signed_up_but_code_required: A message with a code has been sent to your email
110
+ address. Please copy and paste the received code in this page.
111
+ registrations:
112
+ signed_up_but_code_required: A message with a code has been sent to your email
113
+ 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,21 +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)
24
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
25
42
  end
26
43
 
27
- initializer "FriendlySignup.webpacker.assets_path" do
44
+ initializer "friendly_signup.webpacker.assets_path" do
28
45
  Decidim.register_assets_path File.expand_path("app/packs", root)
29
46
  end
30
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.3"
8
+ VERSION = "0.4.2"
9
9
  end
10
10
  end
@@ -23,6 +23,18 @@ module Decidim
23
23
  config_accessor :hide_nickname do
24
24
  true
25
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
26
38
  end
27
39
  end
28
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.3'
4
+ version: 0.4.2
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-14 00:00:00.000000000 Z
11
+ date: 2022-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: decidim-core
@@ -49,21 +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
54
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
55
60
  - app/packs/entrypoints/decidim_friendly_signup.js
56
61
  - app/packs/entrypoints/decidim_friendly_signup.scss
57
62
  - app/packs/images/decidim/friendly_signup/icon.svg
58
63
  - app/packs/src/decidim/friendly_signup/lib/instant_validator.js
59
64
  - app/packs/src/decidim/friendly_signup/lib/password_toggler.js
65
+ - app/packs/src/decidim/friendly_signup/setup_confirmations.js
60
66
  - app/packs/src/decidim/friendly_signup/setup_password.js
61
67
  - app/packs/src/decidim/friendly_signup/setup_validations.js
68
+ - app/packs/stylesheets/decidim/friendly_signup/_confirmation-codes.scss
62
69
  - app/packs/stylesheets/decidim/friendly_signup/_input-groups.scss
63
70
  - app/views/decidim/account/_password_fields.html.erb
64
71
  - app/views/decidim/devise/invitations/edit.html.erb
65
72
  - app/views/decidim/devise/passwords/edit.html.erb
66
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
67
77
  - app/views/decidim/friendly_signup/shared/_password_fields.html.erb
68
78
  - config/assets.rb
69
79
  - config/i18n-tasks.yml