decidim-friendly_signup 0.1 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +55 -20
  3. data/app/controllers/concerns/decidim/friendly_signup/needs_header_snippets.rb +2 -5
  4. data/app/controllers/concerns/decidim/friendly_signup/registrations_redirect.rb +37 -0
  5. data/app/controllers/decidim/friendly_signup/application_controller.rb +8 -0
  6. data/app/controllers/decidim/friendly_signup/confirmation_codes_controller.rb +50 -0
  7. data/app/controllers/decidim/friendly_signup/validator_controller.rb +18 -0
  8. data/app/forms/concerns/decidim/friendly_signup/auto_nickname.rb +35 -0
  9. data/app/forms/decidim/friendly_signup/confirmation_code_form.rb +33 -0
  10. data/app/mailers/decidim/friendly_signup/confirmation_codes_mailer.rb +22 -0
  11. data/app/models/concerns/decidim/friendly_signup/needs_registration_codes.rb +24 -0
  12. data/app/packs/entrypoints/decidim_friendly_signup.js +3 -1
  13. data/app/packs/entrypoints/decidim_friendly_signup.scss +1 -0
  14. data/app/packs/src/decidim/friendly_signup/lib/instant_validator.js +99 -0
  15. data/app/packs/src/decidim/friendly_signup/{password_toggler.js → lib/password_toggler.js} +0 -0
  16. data/app/packs/src/decidim/friendly_signup/setup_confirmations.js +57 -0
  17. data/app/packs/src/decidim/friendly_signup/{password_helper.js → setup_password.js} +1 -1
  18. data/app/packs/src/decidim/friendly_signup/setup_validations.js +7 -0
  19. data/app/packs/stylesheets/decidim/friendly_signup/_confirmation-codes.scss +23 -0
  20. data/app/views/decidim/devise/registrations/new.html.erb +10 -8
  21. data/app/views/decidim/friendly_signup/confirmation_codes/index.html.erb +38 -0
  22. data/app/views/decidim/friendly_signup/confirmation_codes_mailer/confirmation_instructions.html.erb +13 -0
  23. data/app/views/decidim/friendly_signup/shared/_password_fields.html.erb +1 -1
  24. data/config/locales/en.yml +79 -0
  25. data/lib/decidim/friendly_signup/engine.rb +21 -1
  26. data/lib/decidim/friendly_signup/user_attribute_validator.rb +49 -0
  27. data/lib/decidim/friendly_signup/version.rb +1 -1
  28. data/lib/decidim/friendly_signup.rb +32 -0
  29. data/package-lock.json +2 -2
  30. data/package.json +1 -1
  31. metadata +19 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f2ae2b38da8d5bfc15a20685f71f03c57874127d6017924d475a142f60d02cf
4
- data.tar.gz: 929035be31dd91ac0e725b849f83385164966b2be0f47ed103a8d3b5c3cbc149
3
+ metadata.gz: df7a2d3137cd72541b5d7e63878f022eebb44e0b115044425e727c4f0fe68d59
4
+ data.tar.gz: dd63fddf8a154b335369dcf87cae2aaff1c9b28c9bd58ad1910a97088283b75f
5
5
  SHA512:
6
- metadata.gz: ed74b6aaba9d5bf1bd4187f9199e1caba5c7ee2684be73eb271274734bced92119ead384a77a9434d6de9b65126e84b6799789e8d9286b0e25b4f573721ff4b3
7
- data.tar.gz: 28a1e3a53faf886766638d2765533fd3d395401c291f60d7b25066386682be00fcd581585e925c007dd0ad404d0db94794b73c188d24fe00e79e3934767c28d7
6
+ metadata.gz: a03470e2cb20fec61fe7106cd59d96293d9c9e490cd132570aa514a841c43587cbcf675ba25bfd272c5dec497db6c3f6a1922c170effb21f1ddeadeec252f331
7
+ data.tar.gz: 36919447d408e1a91f22b8d650b12db69ac00c08282fd5b07862bc074a344e9d99075d53fe53436af12ba80dc209b25e9085d3ca2bc0a219a1f47061d91566be
data/README.md CHANGED
@@ -1,22 +1,26 @@
1
1
  # Decidim::FriendlySignup
2
2
 
3
+ [![[CI] Lint](https://github.com/OpenSourcePolitics/decidim-module-friendly_signup/actions/workflows/lint.yml/badge.svg)](https://github.com/OpenSourcePolitics/decidim-module-friendly_signup/actions/workflows/lint.yml)
3
4
  [![[CI] Test](https://github.com/OpenSourcePolitics/decidim-module-friendly_signup/actions/workflows/test.yml/badge.svg)](https://github.com/OpenSourcePolitics/decidim-module-friendly_signup/actions/workflows/test.yml)
4
5
  [![Maintainability](https://api.codeclimate.com/v1/badges/46c261f70f7f49a8f385/maintainability)](https://codeclimate.com/github/OpenSourcePolitics/decidim-module-friendly_signup/maintainability)
5
6
  [![Test Coverage](https://codecov.io/gh/OpenSourcePolitics/decidim-module-friendly_signup/branch/main/graph/badge.svg?token=1lrOiLdy9P)](https://codecov.io/gh/OpenSourcePolitics/decidim-module-friendly_signup)
7
+ [![Gem Version](https://badge.fury.io/rb/decidim-friendly_signup.svg)](https://badge.fury.io/rb/decidim-friendly_signup)
8
+
6
9
  ---
7
- A more user friendly approach for the user registration process.
10
+
11
+ A more user friendly approach for the user registration process in [Decidim](https://github.com/decidim/decidim).
8
12
 
9
13
  ## Usage
10
14
 
11
15
  This module simply substitutes some pages to ease up the registration process in Decidim.
12
16
 
13
- Features:
17
+ ### Features:
14
18
 
15
19
  - [x] Simplify the password field and add a button with a "show password". ![Show/hide password](examples/passwords.png)
16
20
 
17
- - [ ] Remove the nickname field from the registration process and automatically create one on registering
18
- - [ ] Instant validate parameters when registering without having to send it for backend validation
19
- - [ ] Use checkout codes to validate the email instead of a link
21
+ - [x] Remove the nickname field from the registration process and automatically create one on registering. ![Hide nickname](examples/nickname.png)
22
+ - [x] Instant validate parameters when registering without having to send it for backend validation. ![Instant validation](examples/instant_validation.png)
23
+ - [x] Use numeric, confirmation codes to validate the email instead of a link. ![Confirmation codes](examples/confirmation_codes.png)
20
24
 
21
25
  ## Installation
22
26
 
@@ -38,9 +42,35 @@ And then execute:
38
42
  bundle
39
43
  ```
40
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
+
57
+ **Note:**
58
+
59
+ The correct version of FriendlySignup should resolved automatically by the Bundler.
60
+ However you can force some specific version using `gem "decidim-friendly_signup", "~> 0.1.0"` in the Gemfile.
61
+
62
+ Depending on your Decidim version, choose the corresponding FriendlySignup version to ensure compatibility:
63
+
64
+ | FriendlySignup version | Compatible Decidim versions |
65
+ |---|---|
66
+ | 0.4.x | 0.26.x |
67
+ | 0.3.x | 0.26.x |
68
+ | 0.2.x | 0.26.x |
69
+ | 0.1.x | 0.26.x |
70
+
41
71
  ## Configuration
42
72
 
43
- Customize your integration by creating an initializer (ie: `config/initializes/friendly_signup.rb`) and set some of the variables:
73
+ Customize your integration by creating an initializer (ie: `config/initializes/friendly_signup.rb`) and set some of the variables (you don't need to do this if you want all features enabled):
44
74
 
45
75
  ```ruby
46
76
  # config/initializers/friendly_signup.rb
@@ -48,14 +78,29 @@ Customize your integration by creating an initializer (ie: `config/initializes/f
48
78
  Decidim::FriendlySignup.configure do |config|
49
79
  # Override password views or leave the originals (default is true):
50
80
  config.override_passwords = false
51
- end
52
81
 
82
+ # Automatically validate user inputs in the register form (default is true):
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
90
+ end
53
91
  ```
54
92
 
55
93
  ## Contributing
56
94
 
57
95
  Bug reports and pull requests are welcome on GitHub at https://github.com/OpenSourcePolitics/decidim-module-friendly_signup.
58
96
 
97
+ ### Localization
98
+
99
+ If you would like to see this module in your own language, you can help with its
100
+ translation at Crowdin:
101
+
102
+ https://crowdin.com/project/decidim-friendly-signup
103
+
59
104
  ### Developing
60
105
 
61
106
  To start contributing to this project, first:
@@ -133,23 +178,13 @@ commands shown above.
133
178
 
134
179
  ### Test code coverage
135
180
 
136
- If you want to generate the code coverage report for the tests, you can use
137
- the `SIMPLECOV=1` environment variable in the rspec command as follows:
181
+ Test coverage should be generated automatically in the folder "coverage" once any test is run:
138
182
 
139
183
  ```bash
140
- $ SIMPLECOV=1 bundle exec rspec
184
+ $ bundle exec rspec
185
+ $ firefox coverage/index.html
141
186
  ```
142
187
 
143
- This will generate a folder named `coverage` in the project root which contains
144
- the code coverage report.
145
-
146
- ### Localization
147
-
148
- If you would like to see this module in your own language, you can help with its
149
- translation at Crowdin:
150
-
151
- https://crowdin.com/project/decidim-friendly-signup
152
-
153
188
  ## License
154
189
 
155
190
  See [LICENSE-AGPLv3.txt](LICENSE-AGPLv3.txt).
@@ -23,11 +23,8 @@ module Decidim
23
23
  @snippets
24
24
  end
25
25
 
26
- def friendly_override_activated?(type)
27
- case type
28
- when :override_passwords
29
- Decidim::FriendlySignup.override_passwords.present?
30
- end
26
+ def friendly_override_activated?(feat)
27
+ Decidim::FriendlySignup.send(feat.to_s).present?
31
28
  end
32
29
  end
33
30
  end
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module FriendlySignup
5
+ class ApplicationController < Decidim::ApplicationController
6
+ end
7
+ end
8
+ 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,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module FriendlySignup
5
+ class ValidatorController < ApplicationController
6
+ include Decidim::FormFactory
7
+
8
+ def validate
9
+ @form = form(Decidim::RegistrationForm).from_params(params)
10
+ validator = UserAttributeValidator.new(form: @form, attribute: params[:attribute])
11
+ render json: {
12
+ valid: validator.valid?,
13
+ error: validator.error
14
+ }
15
+ end
16
+ end
17
+ end
18
+ 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,4 +1,6 @@
1
- import "src/decidim/friendly_signup/password_helper"
1
+ import "src/decidim/friendly_signup/setup_password"
2
+ import "src/decidim/friendly_signup/setup_validations"
3
+ import "src/decidim/friendly_signup/setup_confirmations"
2
4
 
3
5
  // CSS
4
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";
@@ -0,0 +1,99 @@
1
+ /* eslint-disable line-comment-position, no-ternary, no-inline-comments */
2
+
3
+ // Instant, server-side validation
4
+ // compatible with abide classes https://get.foundation/sites/docs/abide.html
5
+ export default class InstantValidator {
6
+ // ms before xhr check
7
+ static get TIMEOUT() {
8
+ return 150;
9
+ }
10
+
11
+ constructor($form) {
12
+ this.$form = $form;
13
+ this.$inputs = $form.find("[data-instant-attribute]");
14
+ this.url = this.$form.data("validationUrl");
15
+ }
16
+
17
+ init() {
18
+ if (!this.url || !this.$form.length) {
19
+ return;
20
+ }
21
+ this.$form.foundation("disableValidation");
22
+ // this final validation prevents abide from resetting the field when user loses focus
23
+ this.$inputs.on("blur", (evt) => {
24
+ this.validate($(evt.currentTarget));
25
+ });
26
+ this.$inputs.on("keyup", (evt) => {
27
+ let $input = $(evt.currentTarget);
28
+ let checkTimeout = $input.data("checkTimeout");
29
+ // Trigger live validation with a delay to avoid throttling
30
+ if (checkTimeout) {
31
+ clearTimeout(checkTimeout);
32
+ }
33
+ $input.data("checkTimeout", setTimeout(() => {
34
+ this.validate($input);
35
+ }, this.TIMEOUT)
36
+ );
37
+ });
38
+ }
39
+
40
+ value($input) {
41
+ return $input.val().trim();
42
+ }
43
+
44
+ attribute($input) {
45
+ return $input.data("instantAttribute");
46
+ }
47
+
48
+ target($input) {
49
+ const $target = this.$form.find($input.data("instantTarget"));
50
+ return $target.length
51
+ ? $target
52
+ : $input;
53
+ }
54
+
55
+ validate($input) {
56
+ this.tamper($input);
57
+ this.post($input).done((response) => {
58
+ this.setFeedback(response, $input);
59
+ });
60
+ }
61
+
62
+ setFeedback(data, $input) {
63
+ if (data.valid) {
64
+ this.clearErrors($input);
65
+ } else {
66
+ this.addErrors(this.target($input), data.error);
67
+ }
68
+ }
69
+
70
+ tamper($dest) {
71
+ $dest.data("tampered", $dest.val().trim() !== "");
72
+ }
73
+
74
+ isTampered($dest) {
75
+ return $dest.data("tampered");
76
+ }
77
+
78
+ addErrors($dest, msg) {
79
+ if ($dest.closest("label").find(".form-error").length > 1) {
80
+ // Decidim may add and additional error class that does not play well with abide
81
+ $dest.closest("label").find(".form-error:last").remove();
82
+ }
83
+ this.$form.foundation("addErrorClasses", $dest);
84
+ if (msg) {
85
+ $dest.closest("label").find(".form-error").text(msg);
86
+ }
87
+ }
88
+
89
+ clearErrors($dest) {
90
+ this.$form.foundation("removeErrorClasses", $dest);
91
+ }
92
+
93
+ post($input) {
94
+ return $.ajax(this.url, {
95
+ method: "POST",
96
+ data: `${this.$form.serialize()}&attribute=${this.attribute($input)}`
97
+ });
98
+ }
99
+ }
@@ -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
+ });
@@ -1,4 +1,4 @@
1
- import PasswordToggler from "src/decidim/friendly_signup/password_toggler";
1
+ import PasswordToggler from "src/decidim/friendly_signup/lib/password_toggler";
2
2
 
3
3
  $(() => {
4
4
  window.Decidim = window.Decidim || {};
@@ -0,0 +1,7 @@
1
+ import InstantValidator from "src/decidim/friendly_signup/lib/instant_validator";
2
+
3
+ $(() => {
4
+ window.Decidim = window.Decidim || {};
5
+ window.Decidim.instantValidator = new InstantValidator($("form.instant-validation"));
6
+ window.Decidim.instantValidator.init();
7
+ });
@@ -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
+ }
@@ -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", id: "register-form" }) 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") %>
37
+ <%= f.text_field :name, help_text: t(".username_help"), autocomplete: "off", data: { "instant-attribute": "nickname" } %>
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 } %>
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" } %>
45
+ </div>
44
46
  </div>
45
- </div>
47
+ <% end %>
46
48
 
47
49
  <div class="field">
48
- <%= f.email_field :email %>
50
+ <%= f.email_field :email, autocomplete: "email", data: { "instant-attribute": "email" } %>
49
51
  </div>
50
52
 
51
- <%= render("decidim/friendly_signup/shared/password_fields", form: f, options: { help_text: t(".password_help", minimun_characters: ::PasswordValidator::MINIMUM_LENGTH), autocomplete: "off" }) %>
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,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
+ <% if @expires_at %>
8
+ <p class="email-small text-center"><%= t(".expires_in", time: distance_of_time_in_words(Time.now, @expires_at, scope: "decidim.friendly_signup.datetime.distance_in_words")) %></p>
9
+ <% end %>
10
+
11
+ <p class="stat text-center" style="margin:1em 0;"><b style="font-size: 2em"><%= @code %></b></p>
12
+
13
+ <p class="email-small email-closing text-center"><%= t(".ignore").html_safe %></p>
@@ -11,7 +11,7 @@
11
11
 
12
12
  <div class="user-password-confirmation">
13
13
  <div class="field">
14
- <%= form.password_field :password_confirmation, options.except(:help_text, :autocomplete) %>
14
+ <%= form.password_field :password_confirmation %>
15
15
  </div>
16
16
  </div>
17
17
  <% else %>
@@ -1,10 +1,89 @@
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"
5
77
  shared:
6
78
  password_fields:
7
79
  hidden_password: Your password is hidden
8
80
  hide_password: Hide password
9
81
  show_password: Show password
10
82
  shown_password: Your password is shown
83
+ devise:
84
+ confirmations:
85
+ signed_up_but_code_required: A message with a code has been sent to your email
86
+ address. Please copy and paste the received code in this page.
87
+ registrations:
88
+ signed_up_but_code_required: A message with a code has been sent to your email
89
+ 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
@@ -9,17 +10,36 @@ module Decidim
9
10
  class Engine < ::Rails::Engine
10
11
  isolate_namespace Decidim::FriendlySignup
11
12
 
13
+ routes do
14
+ devise_scope :user do
15
+ resources :confirmation_codes, only: [:index, :create]
16
+ end
17
+ post :validate, to: "validator#validate"
18
+ end
19
+
12
20
  # Prepare a zone to create overrides
13
21
  # https://edgeguides.rubyonrails.org/engines.html#overriding-models-and-controllers
14
22
  # overrides
15
23
  config.after_initialize do
16
24
  Decidim::Devise::RegistrationsController.include(Decidim::FriendlySignup::NeedsHeaderSnippets)
25
+ Decidim::Devise::RegistrationsController.include(Decidim::FriendlySignup::RegistrationsRedirect)
26
+ Decidim::Devise::ConfirmationsController.include(Decidim::FriendlySignup::RegistrationsRedirect)
17
27
  Decidim::Devise::InvitationsController.include(Decidim::FriendlySignup::NeedsHeaderSnippets)
18
28
  Decidim::Devise::PasswordsController.include(Decidim::FriendlySignup::NeedsHeaderSnippets)
19
29
  Decidim::AccountController.include(Decidim::FriendlySignup::NeedsHeaderSnippets)
30
+ Decidim::RegistrationForm.include(Decidim::FriendlySignup::AutoNickname)
31
+ Decidim::User.include(Decidim::FriendlySignup::NeedsRegistrationCodes)
32
+ end
33
+
34
+ initializer "friendly_signup.confirmation_throttling" do
35
+ # Throttle confirmation attempts for a given code parameter to 6 reqs/minute
36
+ # Return the confirmation_token as a discriminator on POST /users/sign_in requests
37
+ Rack::Attack.throttle("limit confirmations attempts per code", limit: 5, period: 60.seconds) do |request|
38
+ request.params["confirmation_token"] if request.path == "/friendly_signup/confirmation_codes" && request.post?
39
+ end
20
40
  end
21
41
 
22
- initializer "FriendlySignup.webpacker.assets_path" do
42
+ initializer "friendly_signup.webpacker.assets_path" do
23
43
  Decidim.register_assets_path File.expand_path("app/packs", root)
24
44
  end
25
45
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module FriendlySignup
5
+ class UserAttributeValidator
6
+ def initialize(attribute:, form:)
7
+ @attribute = attribute
8
+ @form = form
9
+ end
10
+
11
+ delegate :current_organization, to: :form
12
+ attr_reader :attribute, :form
13
+
14
+ def valid?
15
+ @valid ||= begin
16
+ form.validate
17
+ # we don't validate the form but the attribute alone
18
+ errors.blank?
19
+ end
20
+ end
21
+
22
+ def input
23
+ @input ||= form.public_send(attribute).to_s.dup if valid_attribute?
24
+ end
25
+
26
+ def errors
27
+ @errors ||= valid_attribute? ? form.errors[attribute] : ["Invalid attribute"]
28
+ end
29
+
30
+ def error
31
+ errors.flatten.map(&:upcase_first).join(". ") unless valid?
32
+ end
33
+
34
+ private
35
+
36
+ def valid_attribute?
37
+ %w(nickname email name password).include? attribute.to_s
38
+ end
39
+
40
+ def valid_suggestor?
41
+ ["nickname"].include? attribute.to_s
42
+ end
43
+
44
+ def valid_users
45
+ Decidim::UserBaseEntity.where(invitation_token: nil, organization: current_organization)
46
+ end
47
+ end
48
+ end
49
+ 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.1"
8
+ VERSION = "0.4"
9
9
  end
10
10
  end
@@ -1,14 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "decidim/friendly_signup/version"
3
4
  require "decidim/friendly_signup/engine"
4
5
 
5
6
  module Decidim
6
7
  module FriendlySignup
7
8
  include ActiveSupport::Configurable
8
9
 
10
+ autoload :UserAttributeValidator, "decidim/friendly_signup/user_attribute_validator"
11
+
9
12
  # Whether to override passwords boxes or not
10
13
  config_accessor :override_passwords do
11
14
  true
12
15
  end
16
+
17
+ # Whether to use instant validation or not
18
+ config_accessor :use_instant_validation do
19
+ true
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
13
38
  end
14
39
  end
40
+
41
+ # Engines to handle logic unrelated to participatory spaces or components
42
+ Decidim.register_global_engine(
43
+ :decidim_friendly_signup, # this is the name of the global method to access engine routes
44
+ ::Decidim::FriendlySignup::Engine,
45
+ at: "/friendly_signup"
46
+ )
data/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "decidim-module-friendly_signup",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "decidim-module-friendly_signup",
9
- "version": "0.1.0",
9
+ "version": "0.2.0",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "devDependencies": {
12
12
  "eslint": "^7.25.0",
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decidim-module-friendly_signup",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A better signup process for Decidim",
5
5
  "main": "index.js",
6
6
  "directories": {
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.1'
4
+ version: '0.4'
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-12 00:00:00.000000000 Z
11
+ date: 2022-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: decidim-core
@@ -49,22 +49,37 @@ 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
53
+ - app/controllers/decidim/friendly_signup/application_controller.rb
54
+ - app/controllers/decidim/friendly_signup/confirmation_codes_controller.rb
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
52
60
  - app/packs/entrypoints/decidim_friendly_signup.js
53
61
  - app/packs/entrypoints/decidim_friendly_signup.scss
54
62
  - app/packs/images/decidim/friendly_signup/icon.svg
55
- - app/packs/src/decidim/friendly_signup/password_helper.js
56
- - app/packs/src/decidim/friendly_signup/password_toggler.js
63
+ - app/packs/src/decidim/friendly_signup/lib/instant_validator.js
64
+ - app/packs/src/decidim/friendly_signup/lib/password_toggler.js
65
+ - app/packs/src/decidim/friendly_signup/setup_confirmations.js
66
+ - app/packs/src/decidim/friendly_signup/setup_password.js
67
+ - app/packs/src/decidim/friendly_signup/setup_validations.js
68
+ - app/packs/stylesheets/decidim/friendly_signup/_confirmation-codes.scss
57
69
  - app/packs/stylesheets/decidim/friendly_signup/_input-groups.scss
58
70
  - app/views/decidim/account/_password_fields.html.erb
59
71
  - app/views/decidim/devise/invitations/edit.html.erb
60
72
  - app/views/decidim/devise/passwords/edit.html.erb
61
73
  - app/views/decidim/devise/registrations/new.html.erb
74
+ - app/views/decidim/friendly_signup/confirmation_codes/index.html.erb
75
+ - app/views/decidim/friendly_signup/confirmation_codes_mailer/confirmation_instructions.html.erb
62
76
  - app/views/decidim/friendly_signup/shared/_password_fields.html.erb
63
77
  - config/assets.rb
64
78
  - config/i18n-tasks.yml
65
79
  - config/locales/en.yml
66
80
  - lib/decidim/friendly_signup.rb
67
81
  - lib/decidim/friendly_signup/engine.rb
82
+ - lib/decidim/friendly_signup/user_attribute_validator.rb
68
83
  - lib/decidim/friendly_signup/version.rb
69
84
  - package-lock.json
70
85
  - package.json