stimulus-password-strength 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ <div class="<%= container_class %>"
2
+ data-controller="password-strength"
3
+ data-password-strength-weak-label="<%= labels[:weak] %>"
4
+ data-password-strength-fair-label="<%= labels[:fair] %>"
5
+ data-password-strength-good-label="<%= labels[:good] %>"
6
+ data-password-strength-strong-label="<%= labels[:strong] %>"
7
+ data-password-strength-bar-base-class="<%= bar_base_class %>"
8
+ data-password-strength-text-base-class="<%= text_base_class %>"
9
+ data-password-strength-weak-bar-color="<%= bar_colors[:weak] %>"
10
+ data-password-strength-fair-bar-color="<%= bar_colors[:fair] %>"
11
+ data-password-strength-good-bar-color="<%= bar_colors[:good] %>"
12
+ data-password-strength-strong-bar-color="<%= bar_colors[:strong] %>"
13
+ data-password-strength-weak-text-color="<%= text_colors[:weak] %>"
14
+ data-password-strength-fair-text-color="<%= text_colors[:fair] %>"
15
+ data-password-strength-good-text-color="<%= text_colors[:good] %>"
16
+ data-password-strength-strong-text-color="<%= text_colors[:strong] %>">
17
+ <% if label.present? %>
18
+ <div class="<%= label_row_class %>">
19
+ <%= form.label attribute, label, class: label_class %>
20
+
21
+ <div class="<%= header_aux_class %>">
22
+ <% if requirements.any? %>
23
+ <div class="<%= requirements_class %>" style="<%= requirements_style %>">
24
+ <% requirements.each do |requirement| %>
25
+ <p data-password-strength-target="requirement"
26
+ data-rule="<%= requirement[:rule] %>"
27
+ data-value="<%= requirement[:value] %>"
28
+ data-label="<%= requirement[:label] %>"
29
+ data-remaining-singular="<%= requirement[:remaining_singular] %>"
30
+ data-remaining-plural="<%= requirement[:remaining_plural] %>"
31
+ data-met-label="<%= requirement[:met_label] %>"
32
+ data-pending-style="<%= requirement_pending_style %>"
33
+ data-met-style="<%= requirement_met_style %>"
34
+ data-unmet-style="<%= requirement_unmet_style %>"
35
+ class="<%= requirement_class %>"
36
+ style="<%= requirement_pending_style %>"><%= requirement[:label] %></p>
37
+ <% end %>
38
+ </div>
39
+ <% end %>
40
+ </div>
41
+ </div>
42
+
43
+ <div data-password-strength-target="statusRow" class="<%= status_row_class %>" style="<%= status_row_style %>; visibility: hidden;">
44
+ <div data-password-strength-target="strengthTrack" class="<%= bar_track_class %>" style="visibility: hidden;">
45
+ <div data-password-strength-target="strengthBar" class="<%= bar_base_class %>" style="width: 0%"></div>
46
+ </div>
47
+
48
+ <p data-password-strength-target="strengthText" class="<%= text_base_class %>" style="<%= text_style %>" aria-live="polite"></p>
49
+ </div>
50
+ <% else %>
51
+ <% if requirements.any? %>
52
+ <div class="<%= requirements_class %>" style="<%= requirements_style %>">
53
+ <% requirements.each do |requirement| %>
54
+ <p data-password-strength-target="requirement"
55
+ data-rule="<%= requirement[:rule] %>"
56
+ data-value="<%= requirement[:value] %>"
57
+ data-label="<%= requirement[:label] %>"
58
+ data-remaining-singular="<%= requirement[:remaining_singular] %>"
59
+ data-remaining-plural="<%= requirement[:remaining_plural] %>"
60
+ data-met-label="<%= requirement[:met_label] %>"
61
+ data-pending-style="<%= requirement_pending_style %>"
62
+ data-met-style="<%= requirement_met_style %>"
63
+ data-unmet-style="<%= requirement_unmet_style %>"
64
+ class="<%= requirement_class %>"
65
+ style="<%= requirement_pending_style %>"><%= requirement[:label] %></p>
66
+ <% end %>
67
+ </div>
68
+ <% end %>
69
+
70
+ <div data-password-strength-target="statusRow" class="<%= status_row_class %>" style="<%= status_row_style %>; visibility: hidden;">
71
+ <div data-password-strength-target="strengthTrack" class="<%= bar_track_class %>" style="visibility: hidden;">
72
+ <div data-password-strength-target="strengthBar" class="<%= bar_base_class %>" style="width: 0%"></div>
73
+ </div>
74
+
75
+ <p data-password-strength-target="strengthText" class="<%= text_base_class %>" style="<%= text_style %>" aria-live="polite"></p>
76
+ </div>
77
+ <% end %>
78
+
79
+ <div class="relative">
80
+ <%= form.password_field attribute,
81
+ required: required,
82
+ autocomplete: autocomplete,
83
+ placeholder: placeholder,
84
+ class: input_class,
85
+ data: {
86
+ password_strength_target: "input",
87
+ action: "input->password-strength#evaluate"
88
+ } %>
89
+
90
+ <button type="button"
91
+ data-password-strength-target="toggle"
92
+ aria-label="<%= toggle_labels[:show] %>"
93
+ title="<%= toggle_labels[:show] %>"
94
+ data-show-label="<%= toggle_labels[:show] %>"
95
+ data-hide-label="<%= toggle_labels[:hide] %>"
96
+ data-action="click->password-strength#toggle"
97
+ class="<%= toggle_class %>">
98
+ <svg data-password-strength-target="showIcon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" style="width: 1rem; height: 1rem;" aria-hidden="true">
99
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.27 2.943 9.542 7-1.273 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7Z" />
100
+ <circle cx="12" cy="12" r="3" />
101
+ </svg>
102
+ <svg data-password-strength-target="hideIcon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" style="width: 1rem; height: 1rem;" aria-hidden="true" hidden>
103
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 3l18 18" />
104
+ <path stroke-linecap="round" stroke-linejoin="round" d="M10.584 10.587A2 2 0 0 0 13.414 13.4" />
105
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.364 5.365A10.45 10.45 0 0 1 12 5c4.478 0 8.27 2.943 9.542 7a10.46 10.46 0 0 1-4.132 5.411" />
106
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6.657 6.658C4.798 7.936 3.423 9.79 2.458 12c1.273 4.057 5.064 7 9.542 7 1.536 0 2.992-.346 4.293-.965" />
107
+ </svg>
108
+ </button>
109
+ </div>
110
+
111
+ <% if hint.present? %>
112
+ <p class="<%= hint_class %>"><%= hint %></p>
113
+ <% end %>
114
+ </div>
@@ -0,0 +1,2 @@
1
+ pin "stimulus-password-strength", to: "stimulus_password_strength_controller.js", preload: false
2
+ pin "zxcvbn", to: "zxcvbn.js", preload: false
@@ -0,0 +1,8 @@
1
+ en:
2
+ stimulus_password_strength:
3
+ show: "Show"
4
+ hide: "Hide"
5
+ weak: "Weak"
6
+ fair: "Fair"
7
+ good: "Good"
8
+ strong: "Strong"
@@ -0,0 +1,8 @@
1
+ pl:
2
+ stimulus_password_strength:
3
+ show: "Pokaż"
4
+ hide: "Ukryj"
5
+ weak: "Słabe"
6
+ fair: "Przeciętne"
7
+ good: "Dobre"
8
+ strong: "Silne"
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPasswordStrength
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def add_importmap_pins
9
+ importmap_path = "config/importmap.rb"
10
+ return unless destination_file?(importmap_path)
11
+
12
+ add_pin(importmap_path, 'pin "stimulus-password-strength", to: "stimulus_password_strength_controller.js", preload: false')
13
+ add_pin(importmap_path, 'pin "zxcvbn", to: "zxcvbn.js", preload: false')
14
+ end
15
+
16
+ def register_stimulus_controller
17
+ index_path = "app/javascript/controllers/index.js"
18
+ return unless destination_file?(index_path)
19
+
20
+ content = read_destination(index_path)
21
+ import_line = 'import PasswordStrengthController from "stimulus-password-strength"'
22
+ register_line = 'application.register("password-strength", PasswordStrengthController)'
23
+
24
+ append_to_file(index_path, "\n#{import_line}\n") unless content.include?(import_line)
25
+ append_to_file(index_path, "#{register_line}\n") unless read_destination(index_path).include?(register_line)
26
+ end
27
+
28
+ def create_initializer
29
+ template "stimulus_password_strength.rb.tt", "config/initializers/stimulus_password_strength.rb"
30
+ end
31
+
32
+ def create_password_policy
33
+ policy_path = "app/lib/password_policy.rb"
34
+ return if destination_file?(policy_path)
35
+
36
+ template "password_policy.rb.tt", policy_path
37
+ end
38
+
39
+ private
40
+
41
+ def add_pin(path, line)
42
+ content = read_destination(path)
43
+ append_to_file(path, "#{line}\n") unless content.include?(line)
44
+ end
45
+
46
+ def destination_file?(path)
47
+ File.exist?(File.join(destination_root, path))
48
+ end
49
+
50
+ def read_destination(path)
51
+ File.read(File.join(destination_root, path))
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PasswordPolicy
4
+ MIN_LENGTH = 12
5
+
6
+ REQUIREMENTS = [
7
+ {
8
+ rule: :min_length,
9
+ value: MIN_LENGTH,
10
+ label: "At least #{MIN_LENGTH} characters",
11
+ remaining_singular: "Type 1 more character",
12
+ remaining_plural: "Type %{count} more characters",
13
+ met_label: "#{MIN_LENGTH}+ chars"
14
+ }
15
+ ].freeze
16
+
17
+ def self.requirements
18
+ REQUIREMENTS
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ StimulusPasswordStrength.configure do |config|
4
+ # Example customization:
5
+ # config.input_class = "w-full rounded-md border px-3 py-2 pr-16"
6
+ # config.text_style = "min-width: 2.5rem; text-align: right; white-space: nowrap;"
7
+ # config.status_row_class = "flex min-h-5 items-center gap-2"
8
+ # config.status_row_style = "min-height: 1rem;"
9
+ # config.requirements_style = "min-height: 1rem;"
10
+ # config.requirement_pending_style = "color: #6b7280;"
11
+ # config.requirement_met_style = "color: #047857;"
12
+ # config.requirement_unmet_style = "color: #b91c1c;"
13
+ # config.bar_colors = {
14
+ # weak: "#f87171",
15
+ # fair: "#fbbf24",
16
+ # good: "#22c55e",
17
+ # strong: "#059669"
18
+ # }
19
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../stimulus_password_strength"
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPasswordStrength
4
+ class Configuration
5
+ attr_accessor :container_class,
6
+ :label_row_class,
7
+ :label_class,
8
+ :header_aux_class,
9
+ :status_row_class,
10
+ :status_row_style,
11
+ :requirements_class,
12
+ :requirements_style,
13
+ :requirement_class,
14
+ :input_class,
15
+ :toggle_class,
16
+ :bar_track_class,
17
+ :bar_base_class,
18
+ :text_base_class,
19
+ :text_style,
20
+ :hint_class,
21
+ :requirement_pending_style,
22
+ :requirement_met_style,
23
+ :requirement_unmet_style,
24
+ :bar_colors,
25
+ :text_colors
26
+
27
+ def initialize
28
+ @container_class = "space-y-1"
29
+ @label_row_class = "flex items-end justify-between gap-3"
30
+ @label_class = "block text-sm font-medium text-gray-700"
31
+ @header_aux_class = "flex justify-end"
32
+ @status_row_class = "flex min-h-5 items-center gap-2"
33
+ @status_row_style = "min-height: 1rem;"
34
+ @requirements_class = "flex justify-end gap-2"
35
+ @requirements_style = "min-height: 1rem;"
36
+ @requirement_class = "text-xs text-right leading-tight"
37
+ @input_class = "w-full rounded-xl border border-gray-300 px-4 py-3 pr-16 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
38
+ @toggle_class = "absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer text-xs font-medium text-gray-500 hover:text-gray-700"
39
+ @bar_track_class = "h-1.5 w-20 overflow-hidden rounded-full bg-gray-100"
40
+ @bar_base_class = "h-full rounded-full transition-all duration-300"
41
+ @text_base_class = "text-xs"
42
+ @text_style = "min-width: 2.5rem; text-align: right; white-space: nowrap;"
43
+ @hint_class = "mt-1 text-xs text-gray-500"
44
+ @requirement_pending_style = "color: #6b7280;"
45
+ @requirement_met_style = "color: #047857;"
46
+ @requirement_unmet_style = "color: #b91c1c;"
47
+
48
+ @bar_colors = {
49
+ weak: "#f87171",
50
+ fair: "#fbbf24",
51
+ good: "#22c55e",
52
+ strong: "#059669"
53
+ }
54
+
55
+ @text_colors = {
56
+ weak: "#ef4444",
57
+ fair: "#d97706",
58
+ good: "#16a34a",
59
+ strong: "#047857"
60
+ }
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPasswordStrength
4
+ class Engine < ::Rails::Engine
5
+ initializer "stimulus_password_strength.assets" do |app|
6
+ next unless app.config.respond_to?(:assets)
7
+
8
+ app.config.assets.paths << root.join("app/javascript")
9
+ app.config.assets.paths << root.join("vendor/javascript")
10
+ end
11
+
12
+ initializer "stimulus_password_strength.importmap", before: "importmap" do |app|
13
+ next unless app.config.respond_to?(:importmap)
14
+
15
+ app.config.importmap.paths << root.join("config/importmap.rb")
16
+ end
17
+
18
+ initializer "stimulus_password_strength.helpers" do
19
+ ActiveSupport.on_load(:action_view) do
20
+ include StimulusPasswordStrength::Helper
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPasswordStrength
4
+ module Helper
5
+ def password_strength_field(form, attribute, **options)
6
+ config = StimulusPasswordStrength.configuration
7
+
8
+ placeholder = options.delete(:placeholder)
9
+ hint = options.delete(:hint)
10
+ label = options.delete(:label)
11
+ required = options.delete(:required)
12
+ autocomplete = options.delete(:autocomplete) || "new-password"
13
+ requirements = normalize_requirements(options.delete(:requirements) || [])
14
+ input_class = options.delete(:input_class) || config.input_class
15
+ container_class = options.delete(:container_class) || config.container_class
16
+ label_row_class = options.delete(:label_row_class) || config.label_row_class
17
+ label_class = options.delete(:label_class) || config.label_class
18
+ header_aux_class = options.delete(:header_aux_class) || config.header_aux_class
19
+ status_row_class = options.delete(:status_row_class) || config.status_row_class
20
+ status_row_style = options.delete(:status_row_style) || config.status_row_style
21
+ requirements_class = options.delete(:requirements_class) || config.requirements_class
22
+ requirements_style = options.delete(:requirements_style) || config.requirements_style
23
+ requirement_class = options.delete(:requirement_class) || config.requirement_class
24
+ toggle_class = options.delete(:toggle_class) || config.toggle_class
25
+ bar_track_class = options.delete(:bar_track_class) || config.bar_track_class
26
+ bar_base_class = options.delete(:bar_base_class) || config.bar_base_class
27
+ text_base_class = options.delete(:text_base_class) || config.text_base_class
28
+ text_style = options.delete(:text_style) || config.text_style
29
+ hint_class = options.delete(:hint_class) || config.hint_class
30
+
31
+ labels = i18n_labels(options.delete(:strength_labels) || {})
32
+ toggle_labels = i18n_toggle_labels(options.delete(:toggle_labels) || {})
33
+
34
+ render(
35
+ "stimulus_password_strength/field",
36
+ form: form,
37
+ attribute: attribute,
38
+ label: label,
39
+ placeholder: placeholder,
40
+ hint: hint,
41
+ required: required,
42
+ autocomplete: autocomplete,
43
+ requirements: requirements,
44
+ input_class: input_class,
45
+ container_class: container_class,
46
+ label_row_class: label_row_class,
47
+ label_class: label_class,
48
+ header_aux_class: header_aux_class,
49
+ status_row_class: status_row_class,
50
+ status_row_style: status_row_style,
51
+ requirements_class: requirements_class,
52
+ requirements_style: requirements_style,
53
+ requirement_class: requirement_class,
54
+ toggle_class: toggle_class,
55
+ bar_track_class: bar_track_class,
56
+ bar_base_class: bar_base_class,
57
+ text_base_class: text_base_class,
58
+ text_style: text_style,
59
+ hint_class: hint_class,
60
+ requirement_pending_style: config.requirement_pending_style,
61
+ requirement_met_style: config.requirement_met_style,
62
+ requirement_unmet_style: config.requirement_unmet_style,
63
+ labels: labels,
64
+ toggle_labels: toggle_labels,
65
+ bar_colors: config.bar_colors,
66
+ text_colors: config.text_colors
67
+ )
68
+ end
69
+
70
+ private
71
+
72
+ def i18n_labels(override_labels)
73
+ {
74
+ weak: override_labels[:weak] || I18n.t("stimulus_password_strength.weak", default: "Weak"),
75
+ fair: override_labels[:fair] || I18n.t("stimulus_password_strength.fair", default: "Fair"),
76
+ good: override_labels[:good] || I18n.t("stimulus_password_strength.good", default: "Good"),
77
+ strong: override_labels[:strong] || I18n.t("stimulus_password_strength.strong", default: "Strong")
78
+ }
79
+ end
80
+
81
+ def i18n_toggle_labels(override_labels)
82
+ {
83
+ show: override_labels[:show] || I18n.t("stimulus_password_strength.show", default: "Show"),
84
+ hide: override_labels[:hide] || I18n.t("stimulus_password_strength.hide", default: "Hide")
85
+ }
86
+ end
87
+
88
+ def normalize_requirements(raw_requirements)
89
+ raw_requirements.map do |requirement|
90
+ item = requirement.to_h.transform_keys(&:to_sym)
91
+ rule = item.fetch(:rule).to_sym
92
+ label = item.fetch(:label).to_s
93
+
94
+ raise ArgumentError, "Requirement label must be present" if label.strip.empty?
95
+
96
+ case rule
97
+ when :min_length
98
+ {
99
+ rule: rule.to_s,
100
+ value: Integer(item.fetch(:value)),
101
+ label: label,
102
+ remaining_singular: item[:remaining_singular].to_s,
103
+ remaining_plural: item[:remaining_plural].to_s,
104
+ met_label: item[:met_label].to_s
105
+ }
106
+ when :uppercase
107
+ {
108
+ rule: rule.to_s,
109
+ value: "",
110
+ label: label,
111
+ remaining_singular: "",
112
+ remaining_plural: "",
113
+ met_label: item[:met_label].to_s
114
+ }
115
+ else
116
+ raise ArgumentError, "Unsupported requirement rule: #{rule}"
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPasswordStrength
4
+ VERSION = "0.1.2"
5
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stimulus_password_strength/version"
4
+ require_relative "stimulus_password_strength/configuration"
5
+ require_relative "stimulus_password_strength/helper"
6
+ require_relative "stimulus_password_strength/engine" if defined?(Rails)
7
+
8
+ module StimulusPasswordStrength
9
+ class << self
10
+ def configuration
11
+ @configuration ||= Configuration.new
12
+ end
13
+
14
+ def configure
15
+ yield(configuration)
16
+ end
17
+ end
18
+ end