decidim-friendly_signup 0.3 → 0.4.2

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