better_authy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +123 -0
  4. data/Rakefile +3 -0
  5. data/app/controllers/better_authy/base_controller.rb +39 -0
  6. data/app/controllers/better_authy/passwords_controller.rb +92 -0
  7. data/app/controllers/better_authy/registrations_controller.rb +27 -0
  8. data/app/controllers/better_authy/sessions_controller.rb +48 -0
  9. data/app/helpers/better_authy/application_helper.rb +7 -0
  10. data/app/mailers/better_authy/application_mailer.rb +13 -0
  11. data/app/mailers/better_authy/password_reset_mailer.rb +20 -0
  12. data/app/models/better_authy/forgot_password_form.rb +12 -0
  13. data/app/models/better_authy/reset_password_form.rb +15 -0
  14. data/app/models/better_authy/session_form.rb +15 -0
  15. data/app/views/better_authy/password_reset_mailer/reset_password_instructions.html.erb +13 -0
  16. data/app/views/better_authy/password_reset_mailer/reset_password_instructions.text.erb +9 -0
  17. data/app/views/better_authy/passwords/edit.html.erb +55 -0
  18. data/app/views/better_authy/passwords/new.html.erb +61 -0
  19. data/app/views/better_authy/registrations/new.html.erb +59 -0
  20. data/app/views/better_authy/sessions/new.html.erb +80 -0
  21. data/app/views/layouts/better_authy/application.html.erb +20 -0
  22. data/config/locales/en.yml +48 -0
  23. data/config/locales/it.yml +48 -0
  24. data/config/routes.rb +23 -0
  25. data/lib/better_authy/configuration.rb +43 -0
  26. data/lib/better_authy/controller_helpers.rb +97 -0
  27. data/lib/better_authy/engine.rb +11 -0
  28. data/lib/better_authy/errors.rb +6 -0
  29. data/lib/better_authy/model_extensions.rb +23 -0
  30. data/lib/better_authy/models/authenticable.rb +118 -0
  31. data/lib/better_authy/scope_configuration.rb +30 -0
  32. data/lib/better_authy/version.rb +5 -0
  33. data/lib/better_authy.rb +37 -0
  34. metadata +137 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2cd343402586a23dd2212844efaa3a7007be3a0777b5012264d9380852b060c8
4
+ data.tar.gz: b0bde974d6e6c64172d8453193fe4c0727cd3d101b9548093928ab7a33259a0d
5
+ SHA512:
6
+ metadata.gz: 7f58ca868ea2d945e6f2ec0e22b4c255295efa4354a277648c32d9547d3525e8515f705d595a0c4671225cdf77e4ae070891832dfb96fb1d95fefce52a267b94
7
+ data.tar.gz: df65aa3eb2b1afb41e9c386d92e045e8f9deb3763c512d5d381d9e7256becefc1dcfe8a08fb16a8671fbfa12bf995948096e04b0c6816eb9ce30abee277ea50a
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Umberto Peserico
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # BetterAuthy
2
+
3
+ A flexible authentication engine for Rails 8.0+ with multi-scope support. Enables multiple authenticatable models (users, accounts, admins) through a scope-based configuration system.
4
+
5
+ ## Features
6
+
7
+ - Multi-scope authentication (multiple user types)
8
+ - Session-based authentication with encrypted cookies
9
+ - Remember me functionality
10
+ - Password reset via email
11
+ - Sign-in tracking (IP, timestamp, count)
12
+
13
+ ## Quick Start
14
+
15
+ ### 1. Add to Gemfile
16
+
17
+ ```ruby
18
+ gem "better_authy", "~> 0.1.0"
19
+ ```
20
+
21
+ ### 2. Configure scope
22
+
23
+ ```ruby
24
+ # config/initializers/better_authy.rb
25
+ BetterAuthy.configure do |config|
26
+ config.scope :user do |scope|
27
+ scope.model_name = "User"
28
+ end
29
+ end
30
+ ```
31
+
32
+ ### 3. Add to model
33
+
34
+ ```ruby
35
+ class User < ApplicationRecord
36
+ better_authy_authenticable :user
37
+ end
38
+ ```
39
+
40
+ ### 4. Include controller helpers
41
+
42
+ ```ruby
43
+ class ApplicationController < ActionController::Base
44
+ include BetterAuthy::ControllerHelpers
45
+ end
46
+ ```
47
+
48
+ ### 5. Mount engine
49
+
50
+ ```ruby
51
+ Rails.application.routes.draw do
52
+ mount BetterAuthy::Engine => "/auth"
53
+ end
54
+ ```
55
+
56
+ ### 6. Create migration
57
+
58
+ ```ruby
59
+ class CreateUsers < ActiveRecord::Migration[8.0]
60
+ def change
61
+ create_table :users, id: :uuid do |t|
62
+ t.timestamps null: false
63
+ t.string :email, null: false
64
+ t.string :password_digest, null: false
65
+ t.string :remember_token_digest
66
+ t.datetime :remember_created_at
67
+ t.string :password_reset_token_digest
68
+ t.datetime :password_reset_sent_at
69
+ t.integer :sign_in_count, default: 0
70
+ t.datetime :current_sign_in_at, :last_sign_in_at
71
+ t.string :current_sign_in_ip, :last_sign_in_ip
72
+ t.index :email, unique: true
73
+ end
74
+ end
75
+ end
76
+ ```
77
+
78
+ ## Usage
79
+
80
+ ```ruby
81
+ # In controllers
82
+ before_action :authenticate_user!
83
+ current_user
84
+ user_signed_in?
85
+
86
+ # Manual sign in/out
87
+ sign_in_user(user, remember: true)
88
+ sign_out_user
89
+ ```
90
+
91
+ ## Routes
92
+
93
+ | Path | Description |
94
+ |------|-------------|
95
+ | `/auth/user/login` | Login |
96
+ | `/auth/user/logout` | Logout |
97
+ | `/auth/user/signup` | Registration |
98
+ | `/auth/user/password/new` | Forgot password |
99
+ | `/auth/user/password/edit` | Reset password |
100
+
101
+ ## Documentation
102
+
103
+ See the [docs/](docs/) folder for detailed guides:
104
+
105
+ - [Installation](docs/installation.md) - Complete setup instructions
106
+ - [Configuration](docs/configuration.md) - All configuration options
107
+ - [Models](docs/models.md) - Authenticable model setup
108
+ - [Controller Helpers](docs/controller-helpers.md) - Authentication methods
109
+ - [Routes](docs/routes.md) - Route helpers and customization
110
+ - [Password Reset](docs/password-reset.md) - Password reset flow
111
+ - [Multi-Scope](docs/multi-scope.md) - Multiple user types
112
+ - [Testing](docs/testing.md) - Test setup and patterns
113
+
114
+ ## Dependencies
115
+
116
+ - Rails >= 8.0
117
+ - bcrypt ~> 3.1
118
+ - view_component ~> 4.0
119
+ - better_ui ~> 0.7
120
+
121
+ ## License
122
+
123
+ MIT License
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuthy
4
+ class BaseController < ::ApplicationController
5
+ helper BetterAuthy::ApplicationHelper
6
+
7
+ layout :resolve_layout
8
+ protect_from_forgery with: :exception
9
+
10
+ private
11
+
12
+ def scope_name
13
+ # Scope is injected into params via routes defaults: { scope: scope_name }
14
+ params[:scope]
15
+ end
16
+
17
+ def scope_config
18
+ @scope_config ||= BetterAuthy.scope_for!(scope_name)
19
+ end
20
+
21
+ def redirect_if_signed_in
22
+ return unless send(:"#{scope_name}_signed_in?")
23
+
24
+ redirect_to scope_config.after_sign_in_path
25
+ end
26
+
27
+ def resolve_layout
28
+ return default_layout if scope_name.blank?
29
+
30
+ scope_config.layout
31
+ rescue BetterAuthy::ConfigurationError
32
+ default_layout
33
+ end
34
+
35
+ def default_layout
36
+ "better_authy/application"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuthy
4
+ class PasswordsController < BaseController
5
+ before_action :redirect_if_signed_in, only: %i[new create edit update]
6
+ before_action :find_resource_by_token, only: %i[edit update]
7
+
8
+ def new
9
+ @forgot_password_form = BetterAuthy::ForgotPasswordForm.new
10
+ end
11
+
12
+ def create
13
+ @forgot_password_form = BetterAuthy::ForgotPasswordForm.new(forgot_password_params)
14
+
15
+ if @forgot_password_form.valid?
16
+ resource = find_resource_by_email
17
+ if resource
18
+ token = resource.generate_password_reset_token!
19
+ BetterAuthy::PasswordResetMailer.reset_password_instructions(
20
+ resource, token, scope_name
21
+ ).deliver_later
22
+ end
23
+ # Always show generic message to prevent email enumeration
24
+ redirect_to send(:"#{scope_name}_login_path"),
25
+ notice: I18n.t("better_authy.passwords.send_instructions")
26
+ else
27
+ render :new, status: :unprocessable_content
28
+ end
29
+ end
30
+
31
+ def edit
32
+ @reset_password_form = BetterAuthy::ResetPasswordForm.new(token: params[:token])
33
+ end
34
+
35
+ def update
36
+ @reset_password_form = BetterAuthy::ResetPasswordForm.new(reset_password_params)
37
+
38
+ if @reset_password_form.valid? && @resource.reset_password!(
39
+ @reset_password_form.password,
40
+ @reset_password_form.password_confirmation
41
+ )
42
+ redirect_to send(:"#{scope_name}_login_path"),
43
+ notice: I18n.t("better_authy.passwords.updated")
44
+ else
45
+ @resource.errors.each do |error|
46
+ @reset_password_form.errors.add(error.attribute, error.message)
47
+ end
48
+ render :edit, status: :unprocessable_content
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def forgot_password_params
55
+ params.require(:forgot_password).permit(:email)
56
+ end
57
+
58
+ def reset_password_params
59
+ params.require(:reset_password).permit(:token, :password, :password_confirmation)
60
+ end
61
+
62
+ def find_resource_by_email
63
+ email = forgot_password_params[:email].to_s.strip.downcase
64
+ scope_config.model_class.find_by(email: email)
65
+ end
66
+
67
+ def find_resource_by_token
68
+ token = params[:token]
69
+ token ||= params.dig(:reset_password, :token)
70
+
71
+ if token.blank?
72
+ redirect_to send(:"new_#{scope_name}_password_path"),
73
+ alert: I18n.t("better_authy.passwords.no_token")
74
+ return
75
+ end
76
+
77
+ @resource = find_resource_with_valid_token(token)
78
+
79
+ unless @resource
80
+ redirect_to send(:"new_#{scope_name}_password_path"),
81
+ alert: I18n.t("better_authy.passwords.invalid_token")
82
+ end
83
+ end
84
+
85
+ def find_resource_with_valid_token(token)
86
+ scope_config.model_class.find_each do |resource|
87
+ return resource if resource.password_reset_token_valid?(token)
88
+ end
89
+ nil
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuthy
4
+ class RegistrationsController < BaseController
5
+ before_action :redirect_if_signed_in, only: %i[new create]
6
+
7
+ def new
8
+ @resource = scope_config.model_class.new
9
+ end
10
+
11
+ def create
12
+ @resource = scope_config.model_class.new(resource_params)
13
+ if @resource.save
14
+ send(:"sign_in_#{scope_name}", @resource)
15
+ redirect_to scope_config.after_sign_in_path
16
+ else
17
+ render :new, status: :unprocessable_content
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def resource_params
24
+ params.require(scope_name).permit(:email, :password, :password_confirmation)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuthy
4
+ class SessionsController < BaseController
5
+ before_action :redirect_if_signed_in, only: %i[new create]
6
+
7
+ def new
8
+ @session_form = BetterAuthy::SessionForm.new(email: params.dig(:session, :email))
9
+ end
10
+
11
+ def create
12
+ @session_form = BetterAuthy::SessionForm.new(session_params)
13
+
14
+ # debugger
15
+
16
+ resource = find_resource_by_email
17
+ if resource&.authenticate(session_params[:password])
18
+ remember = @session_form.remember_me
19
+ send(:"sign_in_#{scope_name}", resource, remember: remember)
20
+ redirect_to scope_config.after_sign_in_path
21
+ else
22
+ render_invalid_credentials
23
+ end
24
+ end
25
+
26
+ def destroy
27
+ send(:"sign_out_#{scope_name}")
28
+ redirect_to scope_config.after_sign_in_path
29
+ end
30
+
31
+ private
32
+
33
+ def session_params
34
+ params.require(:session).permit(:email, :password, :remember_me)
35
+ end
36
+
37
+ def find_resource_by_email
38
+ email = session_params[:email].to_s.strip.downcase
39
+ scope_config.model_class.find_by(email: email)
40
+ end
41
+
42
+ def render_invalid_credentials
43
+ flash.now[:alert] = I18n.t("better_authy.sessions.invalid_credentials",
44
+ default: "Invalid email or password")
45
+ render :new, status: :unprocessable_content
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuthy
4
+ module ApplicationHelper
5
+ include BetterUi::ApplicationHelper
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuthy
4
+ class ApplicationMailer < ::ActionMailer::Base
5
+ default from: -> { default_from_address }
6
+
7
+ private
8
+
9
+ def default_from_address
10
+ ENV.fetch("BETTER_AUTHY_MAILER_FROM", "noreply@example.com")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuthy
4
+ class PasswordResetMailer < ApplicationMailer
5
+ include BetterAuthy::Engine.routes.url_helpers
6
+
7
+ def reset_password_instructions(resource, token, scope_name)
8
+ @resource = resource
9
+ @token = token
10
+ @scope_name = scope_name
11
+ @scope_config = BetterAuthy.scope_for!(scope_name)
12
+ @reset_url = send(:"edit_#{scope_name}_password_url", token: token)
13
+
14
+ mail(
15
+ to: resource.email,
16
+ subject: I18n.t("better_authy.passwords.mailer.subject")
17
+ )
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuthy
4
+ class ForgotPasswordForm
5
+ include ActiveModel::Model
6
+ include ActiveModel::Attributes
7
+
8
+ attribute :email, :string
9
+
10
+ validates :email, presence: true
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuthy
4
+ class ResetPasswordForm
5
+ include ActiveModel::Model
6
+ include ActiveModel::Attributes
7
+
8
+ attribute :token, :string
9
+ attribute :password, :string
10
+ attribute :password_confirmation, :string
11
+
12
+ validates :password, presence: true, length: { minimum: 8 }
13
+ validates :password_confirmation, presence: true
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuthy
4
+ class SessionForm
5
+ include ActiveModel::Model
6
+ include ActiveModel::Attributes
7
+
8
+ attribute :email, :string
9
+ attribute :password, :string
10
+ attribute :remember_me, :boolean, default: false
11
+
12
+ validates :email, presence: true
13
+ validates :password, presence: true
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ <h1><%= t("better_authy.passwords.mailer.greeting", email: @resource.email) %></h1>
2
+
3
+ <p><%= t("better_authy.passwords.mailer.instruction") %></p>
4
+
5
+ <p>
6
+ <a href="<%= @reset_url %>">
7
+ <%= t("better_authy.passwords.mailer.action") %>
8
+ </a>
9
+ </p>
10
+
11
+ <p><%= t("better_authy.passwords.mailer.ignore") %></p>
12
+
13
+ <p><%= t("better_authy.passwords.mailer.expiration", hours: (@scope_config.password_reset_within / 1.hour).to_i) %></p>
@@ -0,0 +1,9 @@
1
+ <%= t("better_authy.passwords.mailer.greeting", email: @resource.email) %>
2
+
3
+ <%= t("better_authy.passwords.mailer.instruction") %>
4
+
5
+ <%= @reset_url %>
6
+
7
+ <%= t("better_authy.passwords.mailer.ignore") %>
8
+
9
+ <%= t("better_authy.passwords.mailer.expiration", hours: (@scope_config.password_reset_within / 1.hour).to_i) %>
@@ -0,0 +1,55 @@
1
+ <% content_for :title, t("better_authy.passwords.reset_title") %>
2
+
3
+ <div class="w-full max-w-md">
4
+ <% if flash[:alert].present? %>
5
+ <%= bui_action_messages([flash[:alert]],
6
+ variant: :danger,
7
+ style: :soft,
8
+ dismissible: true,
9
+ container_classes: "mb-6"
10
+ ) %>
11
+ <% end %>
12
+
13
+ <%= bui_card(
14
+ variant: :light,
15
+ style: :bordered,
16
+ size: :lg,
17
+ shadow: true
18
+ ) do |card| %>
19
+ <% card.with_header do %>
20
+ <h1 class="text-2xl font-bold text-center text-grayscale-900">
21
+ <%= t("better_authy.passwords.reset_title") %>
22
+ </h1>
23
+ <% end %>
24
+
25
+ <% card.with_body do %>
26
+ <%= form_with model: @reset_password_form, scope: :reset_password, url: send(:"#{params[:scope]}_password_path"), method: :patch, builder: BetterUi::UiFormBuilder, class: "space-y-6" do |f| %>
27
+ <%= f.hidden_field :token %>
28
+
29
+ <%= f.bui_password_input :password,
30
+ label: t("better_authy.attributes.password"),
31
+ placeholder: t("better_authy.placeholders.password"),
32
+ hint: t("better_authy.hints.password"),
33
+ autocomplete: "new-password" %>
34
+
35
+ <%= f.bui_password_input :password_confirmation,
36
+ label: t("better_authy.attributes.password_confirmation"),
37
+ placeholder: t("better_authy.placeholders.password_confirmation"),
38
+ autocomplete: "new-password" %>
39
+
40
+ <div>
41
+ <%= bui_button(
42
+ variant: :primary,
43
+ style: :solid,
44
+ size: :lg,
45
+ type: :submit,
46
+ show_loader_on_click: true,
47
+ container_classes: "w-full"
48
+ ) do %>
49
+ <%= t("better_authy.passwords.reset_submit") %>
50
+ <% end %>
51
+ </div>
52
+ <% end %>
53
+ <% end %>
54
+ <% end %>
55
+ </div>
@@ -0,0 +1,61 @@
1
+ <% content_for :title, t("better_authy.passwords.forgot_title") %>
2
+
3
+ <div class="w-full max-w-md">
4
+ <% if flash[:alert].present? %>
5
+ <%= bui_action_messages([flash[:alert]],
6
+ variant: :danger,
7
+ style: :soft,
8
+ dismissible: true,
9
+ container_classes: "mb-6"
10
+ ) %>
11
+ <% end %>
12
+
13
+ <%= bui_card(
14
+ variant: :light,
15
+ style: :bordered,
16
+ size: :lg,
17
+ shadow: true
18
+ ) do |card| %>
19
+ <% card.with_header do %>
20
+ <h1 class="text-2xl font-bold text-center text-grayscale-900">
21
+ <%= t("better_authy.passwords.forgot_title") %>
22
+ </h1>
23
+ <% end %>
24
+
25
+ <% card.with_body do %>
26
+ <p class="text-sm text-grayscale-600 mb-6">
27
+ <%= t("better_authy.passwords.forgot_instruction") %>
28
+ </p>
29
+
30
+ <%= form_with model: @forgot_password_form, scope: :forgot_password, url: send(:"#{params[:scope]}_password_path"), builder: BetterUi::UiFormBuilder, class: "space-y-6" do |f| %>
31
+ <%= f.bui_text_input :email,
32
+ label: t("better_authy.attributes.email"),
33
+ placeholder: t("better_authy.placeholders.email"),
34
+ type: "email",
35
+ autocomplete: "email" %>
36
+
37
+ <div>
38
+ <%= bui_button(
39
+ variant: :primary,
40
+ style: :solid,
41
+ size: :lg,
42
+ type: :submit,
43
+ show_loader_on_click: true,
44
+ container_classes: "w-full"
45
+ ) do %>
46
+ <%= t("better_authy.passwords.forgot_submit") %>
47
+ <% end %>
48
+ </div>
49
+ <% end %>
50
+ <% end %>
51
+
52
+ <% card.with_footer do %>
53
+ <p class="text-center text-sm text-grayscale-600">
54
+ <%= t("better_authy.passwords.remembered") %>
55
+ <%= link_to t("better_authy.registrations.login_link"),
56
+ send(:"#{params[:scope]}_login_path"),
57
+ class: "font-medium text-primary-600 hover:text-primary-500" %>
58
+ </p>
59
+ <% end %>
60
+ <% end %>
61
+ </div>
@@ -0,0 +1,59 @@
1
+ <% content_for :title, t("better_authy.registrations.title") %>
2
+
3
+ <div class="w-full max-w-md">
4
+ <%= bui_card(
5
+ variant: :light,
6
+ style: :bordered,
7
+ size: :lg,
8
+ shadow: true
9
+ ) do |card| %>
10
+ <% card.with_header do %>
11
+ <h1 class="text-2xl font-bold text-center text-grayscale-900">
12
+ <%= t("better_authy.registrations.title") %>
13
+ </h1>
14
+ <% end %>
15
+
16
+ <% card.with_body do %>
17
+ <%= form_with model: @resource, scope: params[:scope], url: request.path, builder: BetterUi::UiFormBuilder, class: "space-y-6" do |f| %>
18
+ <%= f.bui_text_input :email,
19
+ label: t("better_authy.attributes.email"),
20
+ placeholder: t("better_authy.placeholders.email"),
21
+ type: "email",
22
+ autocomplete: "email" %>
23
+
24
+ <%= f.bui_password_input :password,
25
+ label: t("better_authy.attributes.password"),
26
+ placeholder: t("better_authy.placeholders.password"),
27
+ hint: t("better_authy.hints.password"),
28
+ autocomplete: "new-password" %>
29
+
30
+ <%= f.bui_password_input :password_confirmation,
31
+ label: t("better_authy.attributes.password_confirmation"),
32
+ placeholder: t("better_authy.placeholders.password_confirmation"),
33
+ autocomplete: "new-password" %>
34
+
35
+ <div>
36
+ <%= bui_button(
37
+ variant: :primary,
38
+ style: :solid,
39
+ size: :lg,
40
+ type: :submit,
41
+ show_loader_on_click: true,
42
+ container_classes: "w-full"
43
+ ) do %>
44
+ <%= t("better_authy.registrations.submit") %>
45
+ <% end %>
46
+ </div>
47
+ <% end %>
48
+ <% end %>
49
+
50
+ <% card.with_footer do %>
51
+ <p class="text-center text-sm text-grayscale-600">
52
+ <%= t("better_authy.registrations.has_account") %>
53
+ <%= link_to t("better_authy.registrations.login_link"),
54
+ send(:"#{params[:scope]}_login_path"),
55
+ class: "font-medium text-primary-600 hover:text-primary-500" %>
56
+ </p>
57
+ <% end %>
58
+ <% end %>
59
+ </div>