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.
- checksums.yaml +4 -4
- data/README.md +52 -1
- data/app/controllers/concerns/decidim/friendly_signup/registrations_redirect.rb +37 -0
- data/app/controllers/decidim/friendly_signup/confirmation_codes_controller.rb +50 -0
- data/app/forms/decidim/friendly_signup/confirmation_code_form.rb +33 -0
- data/app/mailers/decidim/friendly_signup/confirmation_codes_mailer.rb +22 -0
- data/app/models/concerns/decidim/friendly_signup/needs_registration_codes.rb +24 -0
- data/app/packs/entrypoints/decidim_friendly_signup.js +1 -0
- data/app/packs/entrypoints/decidim_friendly_signup.scss +1 -0
- data/app/packs/src/decidim/friendly_signup/lib/instant_validator.js +10 -1
- data/app/packs/src/decidim/friendly_signup/setup_confirmations.js +57 -0
- data/app/packs/stylesheets/decidim/friendly_signup/_confirmation-codes.scss +23 -0
- data/app/views/decidim/devise/invitations/edit.html.erb +8 -6
- data/app/views/decidim/devise/passwords/edit.html.erb +2 -2
- data/app/views/decidim/devise/registrations/new.html.erb +5 -5
- data/app/views/decidim/devise/sessions/new.html.erb +58 -0
- data/app/views/decidim/friendly_signup/confirmation_codes/index.html.erb +38 -0
- data/app/views/decidim/friendly_signup/confirmation_codes_mailer/confirmation_instructions.html.erb +13 -0
- data/app/views/decidim/friendly_signup/shared/_password_fields.html.erb +13 -7
- data/config/locales/en.yml +103 -0
- data/lib/decidim/friendly_signup/engine.rb +19 -2
- data/lib/decidim/friendly_signup/user_attribute_validator.rb +34 -5
- data/lib/decidim/friendly_signup/version.rb +1 -1
- data/lib/decidim/friendly_signup.rb +12 -0
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 156a94366f47dc7f364338feca03ba824d6e9d38a16b9c8f8a6fbac4774ccbd1
|
4
|
+
data.tar.gz: 131f4becfb5b981f0f026a970b0b38cf98fdb2d5970bc43873ad942d360df76d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
- [
|
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
|
@@ -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").
|
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
|
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
|
-
|
23
|
-
<div class="
|
24
|
-
|
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
|
-
|
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: "
|
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)
|
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"
|
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"
|
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"
|
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"
|
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>
|
data/app/views/decidim/friendly_signup/confirmation_codes_mailer/confirmation_instructions.html.erb
ADDED
@@ -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
|
-
|
13
|
-
<div class="
|
14
|
-
|
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
|
-
|
18
|
+
<% end %>
|
19
|
+
|
17
20
|
<% else %>
|
18
21
|
<div class="field">
|
19
22
|
<%= form.password_field :password, options %>
|
20
23
|
</div>
|
21
24
|
|
22
|
-
|
23
|
-
|
24
|
-
|
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 %>
|
data/config/locales/en.yml
CHANGED
@@ -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 "
|
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
|
-
|
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
|
@@ -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:
|
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-
|
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
|