kaze 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/kaze/commands/install_command.rb +3 -1
- data/lib/kaze/commands/installs_hotwire_stack.rb +2 -2
- data/lib/kaze/commands/installs_inertia_stacks.rb +7 -7
- data/lib/kaze/version.rb +1 -1
- data/stubs/default/app/mailers/application_mailer.rb +1 -1
- data/stubs/default/app/mailers/user_mailer.rb +16 -1
- data/stubs/default/app/models/concerns/can_reset_password.rb +1 -1
- data/stubs/default/app/models/concerns/must_verify_email.rb +15 -0
- data/stubs/default/app/models/user.rb +1 -0
- data/stubs/default/app/views/user_mailer/reset_password.html.erb +2 -2
- data/stubs/default/app/views/user_mailer/verify_email.html.erb +33 -0
- data/stubs/default/config/routes.rb +4 -0
- data/stubs/default/db/migrate/20240101000000_create_users.rb +1 -0
- data/stubs/default/test/factories/users.rb +5 -0
- data/stubs/default/test/integration/auth/authentication_test.rb +4 -6
- data/stubs/default/test/integration/auth/email_verification_test.rb +40 -0
- data/stubs/default/test/integration/auth/password_reset_test.rb +3 -3
- data/stubs/default/test/integration/password_update_test.rb +2 -2
- data/stubs/default/test/integration/profile_test.rb +4 -4
- data/stubs/hotwire/app/components/modal_component.rb +5 -5
- data/stubs/hotwire/app/controllers/auth/authenticated_session_controller.rb +3 -2
- data/stubs/hotwire/app/controllers/auth/email_verification_notification_controller.rb +21 -0
- data/stubs/hotwire/app/controllers/auth/new_password_controller.rb +2 -2
- data/stubs/hotwire/app/controllers/auth/password_reset_link_controller.rb +1 -1
- data/stubs/hotwire/app/controllers/auth/registered_user_controller.rb +6 -2
- data/stubs/hotwire/app/controllers/auth/verified_email_controller.rb +23 -0
- data/stubs/hotwire/app/controllers/concerns/authenticate.rb +10 -0
- data/stubs/hotwire/app/controllers/concerns/validate_signature.rb +17 -0
- data/stubs/hotwire/app/controllers/password_controller.rb +1 -1
- data/stubs/hotwire/app/controllers/profile_controller.rb +2 -2
- data/stubs/hotwire/app/views/auth/verify_email.html.erb +23 -0
- data/stubs/hotwire/app/views/profile/partials/_delete_user_form.html.erb +1 -1
- data/stubs/inertia-common/app/controllers/auth/authenticated_session_controller.rb +3 -3
- data/stubs/inertia-common/app/controllers/auth/email_verification_notification_controller.rb +21 -0
- data/stubs/inertia-common/app/controllers/auth/new_password_controller.rb +2 -3
- data/stubs/inertia-common/app/controllers/auth/password_reset_link_controller.rb +3 -3
- data/stubs/inertia-common/app/controllers/auth/registered_user_controller.rb +6 -2
- data/stubs/inertia-common/app/controllers/auth/verified_email_controller.rb +23 -0
- data/stubs/inertia-common/app/controllers/concerns/authenticate.rb +10 -0
- data/stubs/inertia-common/app/controllers/concerns/validate_signature.rb +17 -0
- data/stubs/inertia-common/app/controllers/password_controller.rb +1 -1
- data/stubs/inertia-common/app/controllers/profile_controller.rb +2 -2
- data/stubs/inertia-common/test/integration/password_update_test.rb +2 -2
- data/stubs/inertia-common/test/integration/profile_test.rb +4 -4
- data/stubs/inertia-react-ts/app/javascript/Components/Checkbox.tsx +1 -4
- data/stubs/inertia-react-ts/app/javascript/Components/DangerButton.tsx +3 -5
- data/stubs/inertia-react-ts/app/javascript/Components/Dropdown.tsx +4 -25
- data/stubs/inertia-react-ts/app/javascript/Components/InputError.tsx +1 -4
- data/stubs/inertia-react-ts/app/javascript/Components/InputLabel.tsx +1 -4
- data/stubs/inertia-react-ts/app/javascript/Components/NavLink.tsx +3 -5
- data/stubs/inertia-react-ts/app/javascript/Components/PrimaryButton.tsx +3 -5
- data/stubs/inertia-react-ts/app/javascript/Components/SecondaryButton.tsx +3 -5
- data/stubs/inertia-react-ts/app/javascript/Components/TextInput.tsx +1 -7
- data/stubs/inertia-react-ts/app/javascript/Layouts/AuthenticatedLayout.tsx +14 -52
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ForgotPassword.tsx +3 -6
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Login.tsx +9 -23
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Register.tsx +1 -4
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ResetPassword.tsx +1 -4
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/VerifyEmail.tsx +47 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Dashboard.tsx +2 -8
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Edit.tsx +1 -5
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.tsx +7 -19
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.tsx +7 -15
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.tsx +7 -16
- data/stubs/inertia-react-ts/app/javascript/Pages/Welcome.tsx +1 -2
- data/stubs/inertia-react-ts/app/javascript/entrypoints/application.tsx +1 -5
- data/stubs/inertia-react-ts/config/tailwind.config.js +1 -6
- data/stubs/inertia-vue-ts/app/javascript/Components/ApplicationLogo.vue +3 -1
- data/stubs/inertia-vue-ts/app/javascript/Components/Dropdown.vue +1 -4
- data/stubs/inertia-vue-ts/app/javascript/Components/Modal.vue +3 -13
- data/stubs/inertia-vue-ts/app/javascript/Layouts/AuthenticatedLayout.vue +10 -45
- data/stubs/inertia-vue-ts/app/javascript/Layouts/GuestLayout.vue +2 -6
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ForgotPassword.vue +4 -11
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Login.vue +2 -10
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Register.vue +1 -5
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ResetPassword.vue +1 -4
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/VerifyEmail.vue +50 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Dashboard.vue +3 -11
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Edit.vue +2 -10
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.vue +5 -9
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.vue +2 -9
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.vue +3 -13
- data/stubs/inertia-vue-ts/app/javascript/Pages/Welcome.vue +2 -5
- data/stubs/inertia-vue-ts/config/tailwind.config.js +1 -6
- metadata +14 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 53f603a627b691ff3fee85613cd44327e85799c85c3b75557ee10c9757dba9e7
|
4
|
+
data.tar.gz: ca7677bad4b8446b1288a1d3a7d4db40edb6a77f851140a37b86e3607223c63d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 979b1c79b3e9c447a4cf5adf3dfb1661b166f603114ce2e107c61719180c8a8102d2f5614de513ecef3b5f8847664f4618ea8e5bc6689f624d2646b76596ae3e
|
7
|
+
data.tar.gz: 4cb564c4b6ffd71e46adae28a2aeb903774bf7bd30db4c5375a0e649a91f77b4fd24ef8ddd79856513ef80d5b5f7b8c9e0f8aed8752cd9c637a65c589289a95b
|
@@ -9,6 +9,8 @@ class Kaze::Commands::InstallCommand < Thor
|
|
9
9
|
|
10
10
|
desc 'install [STACK]', 'Install the Kaze controllers and resources. Supported stacks: hotwire, react, vue.'
|
11
11
|
def install(stack = 'hotwire')
|
12
|
+
return say 'Kaze must be run in a new Rails application.', :red unless File.exist?("#{Dir.pwd}/bin/rails")
|
13
|
+
|
12
14
|
if stack == 'hotwire'
|
13
15
|
return install_hotwire_stack
|
14
16
|
end
|
@@ -53,7 +55,7 @@ class Kaze::Commands::InstallCommand < Thor
|
|
53
55
|
def install_migrations
|
54
56
|
ensure_directory_exists("#{Dir.pwd}/db/migrate")
|
55
57
|
FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/db/migrate", "#{Dir.pwd}/db/migrate")
|
56
|
-
stdin, _ = Open3.capture3(
|
58
|
+
stdin, _ = Open3.capture3("#{Dir.pwd}/bin/rails version")
|
57
59
|
versions = stdin.gsub!('Rails ', '').split('.')
|
58
60
|
railsVersion = [ versions[0], versions[1] ].join('.')
|
59
61
|
Dir.children("#{Dir.pwd}/db/migrate").each do |file|
|
@@ -32,7 +32,7 @@ module Kaze::Commands::InstallsHotwireStack
|
|
32
32
|
FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/hotwire/app/views", "#{Dir.pwd}/app/views")
|
33
33
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.html.erb", "#{Dir.pwd}/app/views/layouts/mailer.html.erb")
|
34
34
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.text.erb", "#{Dir.pwd}/app/views/layouts/mailer.text.erb")
|
35
|
-
FileUtils.
|
35
|
+
FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/user_mailer", "#{Dir.pwd}/app/views/user_mailer")
|
36
36
|
|
37
37
|
# Components + Pages...
|
38
38
|
ensure_directory_exists("#{Dir.pwd}/app/components")
|
@@ -57,7 +57,7 @@ module Kaze::Commands::InstallsHotwireStack
|
|
57
57
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/hotwire/config/tailwind.config.js", "#{Dir.pwd}/config/tailwind.config.js")
|
58
58
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/dev", "#{Dir.pwd}/bin/dev")
|
59
59
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
|
60
|
-
run_command(
|
60
|
+
run_command("#{Dir.pwd}/bin/rails tailwindcss:build")
|
61
61
|
|
62
62
|
say ''
|
63
63
|
say 'Kaze scaffolding installed successfully.', :green
|
@@ -34,7 +34,7 @@ module Kaze::Commands::InstallsInertiaStacks
|
|
34
34
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-react-ts/app/views/layouts/application.html.erb", "#{Dir.pwd}/app/views/layouts/application.html.erb")
|
35
35
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.html.erb", "#{Dir.pwd}/app/views/layouts/mailer.html.erb")
|
36
36
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.text.erb", "#{Dir.pwd}/app/views/layouts/mailer.text.erb")
|
37
|
-
FileUtils.
|
37
|
+
FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/user_mailer", "#{Dir.pwd}/app/views/user_mailer")
|
38
38
|
|
39
39
|
# Components + Pages...
|
40
40
|
ensure_directory_exists("#{Dir.pwd}/app/javascript")
|
@@ -60,9 +60,9 @@ module Kaze::Commands::InstallsInertiaStacks
|
|
60
60
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/dev", "#{Dir.pwd}/bin/dev")
|
61
61
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/bin/vite", "#{Dir.pwd}/bin/vite")
|
62
62
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
|
63
|
-
File.write("#{Dir.pwd}/Procfile.dev", "#{File.read("#{Dir.pwd}/Procfile.dev")}\nvite: bin/vite dev\n")
|
64
|
-
run_command(
|
65
|
-
run_command(
|
63
|
+
File.write("#{Dir.pwd}/Procfile.dev", "#{File.read("#{Dir.pwd}/Procfile.dev")}\nvite: bundle exec rake js:routes:typescript && bin/vite dev\n")
|
64
|
+
run_command("#{Dir.pwd}/bin/rails generate js_routes:middleware")
|
65
|
+
run_command("#{Dir.pwd}/bin/rails tailwindcss:build")
|
66
66
|
|
67
67
|
say ''
|
68
68
|
say 'Installing and building Node dependencies.', :magenta
|
@@ -140,9 +140,9 @@ module Kaze::Commands::InstallsInertiaStacks
|
|
140
140
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/dev", "#{Dir.pwd}/bin/dev")
|
141
141
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/bin/vite", "#{Dir.pwd}/bin/vite")
|
142
142
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
|
143
|
-
File.write("#{Dir.pwd}/Procfile.dev", "#{File.read("#{Dir.pwd}/Procfile.dev")}\nvite: bin/vite dev\n")
|
144
|
-
run_command(
|
145
|
-
run_command(
|
143
|
+
File.write("#{Dir.pwd}/Procfile.dev", "#{File.read("#{Dir.pwd}/Procfile.dev")}\nvite: bundle exec rake js:routes:typescript && bin/vite dev\n")
|
144
|
+
run_command("#{Dir.pwd}/bin/rails generate js_routes:middleware")
|
145
|
+
run_command("#{Dir.pwd}/bin/rails tailwindcss:build")
|
146
146
|
|
147
147
|
say ''
|
148
148
|
say 'Installing and building Node dependencies.', :magenta
|
data/lib/kaze/version.rb
CHANGED
@@ -1,8 +1,23 @@
|
|
1
1
|
class UserMailer < ApplicationMailer
|
2
|
-
def reset_password
|
2
|
+
def reset_password(token)
|
3
|
+
@reset_url = password_reset_url(token: token)
|
4
|
+
|
3
5
|
mail(
|
4
6
|
to: params[:user].email,
|
5
7
|
subject: 'Reset Password Notification'
|
6
8
|
)
|
7
9
|
end
|
10
|
+
|
11
|
+
def verify_email
|
12
|
+
verifier = ActiveSupport::MessageVerifier.new(ENV.fetch('RAILS_MASTER_KEY', ''))
|
13
|
+
|
14
|
+
signature = verifier.generate(verification_verify_url(id: params[:user].id, hash: Digest::SHA1.hexdigest(params[:user].email)), expires_in: 60.minutes.from_now.to_i)
|
15
|
+
|
16
|
+
@verification_url = verification_verify_url(id: params[:user].id, hash: Digest::SHA1.hexdigest(params[:user].email), signature: signature)
|
17
|
+
|
18
|
+
mail(
|
19
|
+
to: params[:user].email,
|
20
|
+
subject: 'Verify Email Address'
|
21
|
+
)
|
22
|
+
end
|
8
23
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module MustVerifyEmail
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
def has_verified_email?
|
5
|
+
!email_verified_at.nil?
|
6
|
+
end
|
7
|
+
|
8
|
+
def mark_email_as_verified
|
9
|
+
self.update(email_verified_at: Time.now)
|
10
|
+
end
|
11
|
+
|
12
|
+
def send_email_verification_notification
|
13
|
+
UserMailer.with(user: self).verify_email.deliver_later
|
14
|
+
end
|
15
|
+
end
|
@@ -12,7 +12,7 @@
|
|
12
12
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation">
|
13
13
|
<tr>
|
14
14
|
<td>
|
15
|
-
<a href="<%=
|
15
|
+
<a href="<%= @reset_url %>" class="button button-primary" target="_blank" rel="noopener">Reset Password</a>
|
16
16
|
</td>
|
17
17
|
</tr>
|
18
18
|
</table>
|
@@ -30,5 +30,5 @@
|
|
30
30
|
<%= ENV.fetch("APP_NAME", "Rails") %></p>
|
31
31
|
<!-- Subcopy -->
|
32
32
|
<% content_for :subcopy do %>
|
33
|
-
<p>If you're having trouble clicking the "Reset Password" button, copy and paste the URL below into your web browser: <span class="break-all"><%= link_to
|
33
|
+
<p>If you're having trouble clicking the "Reset Password" button, copy and paste the URL below into your web browser: <span class="break-all"><%= link_to @reset_url, @reset_url %><span></p>
|
34
34
|
<% end %>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<!-- Greeting -->
|
2
|
+
<h1>Hello!</h1>
|
3
|
+
<!-- Intro Lines -->
|
4
|
+
<p>Please click the button below to verify your email address.</p>
|
5
|
+
<!-- Action Button -->
|
6
|
+
<table class="action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
7
|
+
<tr>
|
8
|
+
<td align="center">
|
9
|
+
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
|
10
|
+
<tr>
|
11
|
+
<td align="center">
|
12
|
+
<table border="0" cellpadding="0" cellspacing="0" role="presentation">
|
13
|
+
<tr>
|
14
|
+
<td>
|
15
|
+
<a href="<%= @verification_url %>" class="button button-primary" target="_blank" rel="noopener">Verify Email Address</a>
|
16
|
+
</td>
|
17
|
+
</tr>
|
18
|
+
</table>
|
19
|
+
</td>
|
20
|
+
</tr>
|
21
|
+
</table>
|
22
|
+
</td>
|
23
|
+
</tr>
|
24
|
+
</table>
|
25
|
+
<!-- Outro Lines -->
|
26
|
+
<p>If you did not create an account, no further action is required.</p>
|
27
|
+
<!-- Salutation -->
|
28
|
+
<p>Regards,<br>
|
29
|
+
<%= ENV.fetch("APP_NAME", "Rails") %></p>
|
30
|
+
<!-- Subcopy -->
|
31
|
+
<% content_for :subcopy do %>
|
32
|
+
<p>If you're having trouble clicking the "Verify Email Address" button, copy and paste the URL below into your web browser: <span class="break-all"><%= link_to @verification_url, @verification_url %><span></p>
|
33
|
+
<% end %>
|
@@ -15,6 +15,10 @@ Rails.application.routes.draw do
|
|
15
15
|
get 'reset-password/:token', to: 'auth/new_password#new', as: :password_reset
|
16
16
|
post 'reset-password', to: 'auth/new_password#create', as: :password_store
|
17
17
|
|
18
|
+
get 'verify-email', to: 'auth/email_verification_notification#new', as: :verification_notice
|
19
|
+
post 'email/verification-notification', to: 'auth/email_verification_notification#create', as: :verification_send
|
20
|
+
get 'verify-email/:id/:hash', to: 'auth/verified_email#create', as: :verification_verify
|
21
|
+
|
18
22
|
post 'logout', to: 'auth/authenticated_session#destroy', as: :logout
|
19
23
|
|
20
24
|
get 'dashboard', to: 'dashboard#index', as: :dashboard
|
@@ -8,7 +8,7 @@ class Auth::AuthenticationTest < ActionDispatch::IntegrationTest
|
|
8
8
|
end
|
9
9
|
|
10
10
|
test 'users can authenticate using the login screen' do
|
11
|
-
user = FactoryBot.create
|
11
|
+
user = FactoryBot.create(:user)
|
12
12
|
|
13
13
|
post login_path, params: {
|
14
14
|
email: user.email,
|
@@ -20,7 +20,7 @@ class Auth::AuthenticationTest < ActionDispatch::IntegrationTest
|
|
20
20
|
end
|
21
21
|
|
22
22
|
test 'users cannot authenticate with invalid password' do
|
23
|
-
user = FactoryBot.create
|
23
|
+
user = FactoryBot.create(:user)
|
24
24
|
|
25
25
|
post login_path, params: {
|
26
26
|
email: user.email,
|
@@ -31,11 +31,9 @@ class Auth::AuthenticationTest < ActionDispatch::IntegrationTest
|
|
31
31
|
end
|
32
32
|
|
33
33
|
test 'users can logout' do
|
34
|
-
user = FactoryBot.create
|
34
|
+
user = FactoryBot.create(:user)
|
35
35
|
|
36
|
-
acting_as
|
37
|
-
|
38
|
-
post logout_path
|
36
|
+
acting_as(user).post logout_path
|
39
37
|
|
40
38
|
assert_guest
|
41
39
|
assert_redirected_to '/'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class EmailVerificationTest < ActionDispatch::IntegrationTest
|
4
|
+
if User.include?(MustVerifyEmail)
|
5
|
+
test 'email verification screen can be rendered' do
|
6
|
+
user = FactoryBot.create(:user, :unverified)
|
7
|
+
|
8
|
+
acting_as(user).get verification_notice_path
|
9
|
+
|
10
|
+
assert_response :success
|
11
|
+
end
|
12
|
+
|
13
|
+
test 'email can be verified' do
|
14
|
+
user = FactoryBot.create(:user, :unverified)
|
15
|
+
|
16
|
+
acting_as(user).get verification_url(id: user.id, hash: Digest::SHA1.hexdigest(user.email))
|
17
|
+
|
18
|
+
assert user.reload.has_verified_email?
|
19
|
+
assert_redirected_to dashboard_path(verified: '1')
|
20
|
+
end
|
21
|
+
|
22
|
+
test 'email is not verified with invalid hash' do
|
23
|
+
user = FactoryBot.create(:user, :unverified)
|
24
|
+
|
25
|
+
acting_as(user).get verification_url(id: user.id, hash: Digest::SHA1.hexdigest('wrong-email'))
|
26
|
+
|
27
|
+
assert_not user.reload.has_verified_email?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def verification_url(params)
|
34
|
+
verifier = ActiveSupport::MessageVerifier.new(ENV.fetch('RAILS_MASTER_KEY', ''))
|
35
|
+
|
36
|
+
signature = verifier.generate(verification_verify_url(params), expires_in: 60.minutes.from_now.to_i)
|
37
|
+
|
38
|
+
verification_verify_url(params.merge(signature: signature))
|
39
|
+
end
|
40
|
+
end
|
@@ -8,7 +8,7 @@ class Auth::PasswordResetTest < ActionDispatch::IntegrationTest
|
|
8
8
|
end
|
9
9
|
|
10
10
|
test 'reset password link can be requested' do
|
11
|
-
user = FactoryBot.create
|
11
|
+
user = FactoryBot.create(:user)
|
12
12
|
token = user.generate_token_for(:password_reset)
|
13
13
|
email = UserMailer.with(user: user, token: token).reset_password
|
14
14
|
|
@@ -20,7 +20,7 @@ class Auth::PasswordResetTest < ActionDispatch::IntegrationTest
|
|
20
20
|
end
|
21
21
|
|
22
22
|
test 'reset password screen can be rendered' do
|
23
|
-
user = FactoryBot.create
|
23
|
+
user = FactoryBot.create(:user)
|
24
24
|
|
25
25
|
get password_reset_path(token: user.generate_token_for(:password_reset))
|
26
26
|
|
@@ -28,7 +28,7 @@ class Auth::PasswordResetTest < ActionDispatch::IntegrationTest
|
|
28
28
|
end
|
29
29
|
|
30
30
|
test 'password can be reset_with_valid_token' do
|
31
|
-
user = FactoryBot.create
|
31
|
+
user = FactoryBot.create(:user)
|
32
32
|
|
33
33
|
post password_store_path, params: {
|
34
34
|
token: user.generate_token_for(:password_reset),
|
@@ -2,7 +2,7 @@ require 'test_helper'
|
|
2
2
|
|
3
3
|
class PasswordUpdateTest < ActionDispatch::IntegrationTest
|
4
4
|
test 'password can be updated' do
|
5
|
-
user = FactoryBot.create
|
5
|
+
user = FactoryBot.create(:user)
|
6
6
|
|
7
7
|
acting_as(user).put password_update_path, params: {
|
8
8
|
current_password: 'password',
|
@@ -15,7 +15,7 @@ class PasswordUpdateTest < ActionDispatch::IntegrationTest
|
|
15
15
|
end
|
16
16
|
|
17
17
|
test 'correct password must be provided to update password' do
|
18
|
-
user = FactoryBot.create
|
18
|
+
user = FactoryBot.create(:user)
|
19
19
|
|
20
20
|
acting_as(user).put password_update_path, params: {
|
21
21
|
current_password: 'wrong-password',
|
@@ -2,7 +2,7 @@ require 'test_helper'
|
|
2
2
|
|
3
3
|
class ProfileTest < ActionDispatch::IntegrationTest
|
4
4
|
test 'profile page is displayed' do
|
5
|
-
user = FactoryBot.create
|
5
|
+
user = FactoryBot.create(:user)
|
6
6
|
|
7
7
|
acting_as(user).get profile_edit_path
|
8
8
|
|
@@ -10,7 +10,7 @@ class ProfileTest < ActionDispatch::IntegrationTest
|
|
10
10
|
end
|
11
11
|
|
12
12
|
test 'profile information can be updated' do
|
13
|
-
user = FactoryBot.create
|
13
|
+
user = FactoryBot.create(:user)
|
14
14
|
|
15
15
|
acting_as(user).patch profile_edit_path, params: {
|
16
16
|
name: 'Test User',
|
@@ -26,7 +26,7 @@ class ProfileTest < ActionDispatch::IntegrationTest
|
|
26
26
|
end
|
27
27
|
|
28
28
|
test 'user can delete their account' do
|
29
|
-
user = FactoryBot.create
|
29
|
+
user = FactoryBot.create(:user)
|
30
30
|
|
31
31
|
acting_as(user).delete profile_destroy_path, params: {
|
32
32
|
password: 'password'
|
@@ -39,7 +39,7 @@ class ProfileTest < ActionDispatch::IntegrationTest
|
|
39
39
|
end
|
40
40
|
|
41
41
|
test 'correct password must be provided to delete account' do
|
42
|
-
user = FactoryBot.create
|
42
|
+
user = FactoryBot.create(:user)
|
43
43
|
|
44
44
|
acting_as(user).delete profile_destroy_path, params: {
|
45
45
|
password: 'wrong-password'
|
@@ -3,11 +3,11 @@ class ModalComponent < ViewComponent::Base
|
|
3
3
|
@name = attributes[:name]
|
4
4
|
@show = attributes[:show] || false
|
5
5
|
@max_width = {
|
6
|
-
:
|
7
|
-
:
|
8
|
-
:
|
9
|
-
:
|
10
|
-
'2xl'
|
6
|
+
sm: 'sm:max-w-sm',
|
7
|
+
md: 'sm:max-w-md',
|
8
|
+
lg: 'sm:max-w-lg',
|
9
|
+
xl: 'sm:max-w-xl',
|
10
|
+
'2xl': 'sm:max-w-2xl'
|
11
11
|
}[attributes[:max_width] || '2xl']
|
12
12
|
@attributes = attributes.without(:name, :show, :max_width)
|
13
13
|
end
|
@@ -2,7 +2,8 @@ class Auth::AuthenticatedSessionController < ApplicationController
|
|
2
2
|
include RedirectIfAuthenticated
|
3
3
|
|
4
4
|
skip_authenticate only: %i[new create]
|
5
|
-
skip_redirect_if_authenticated only:
|
5
|
+
skip_redirect_if_authenticated only: :destroy
|
6
|
+
skip_ensure_email_is_verified only: :destroy
|
6
7
|
|
7
8
|
layout 'guest'
|
8
9
|
|
@@ -13,7 +14,7 @@ class Auth::AuthenticatedSessionController < ApplicationController
|
|
13
14
|
end
|
14
15
|
|
15
16
|
def create
|
16
|
-
@form = Auth::LoginForm.new
|
17
|
+
@form = Auth::LoginForm.new(params.permit(:email, :password, :remember))
|
17
18
|
|
18
19
|
@form.authenticate
|
19
20
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Auth::EmailVerificationNotificationController < ApplicationController
|
2
|
+
skip_ensure_email_is_verified
|
3
|
+
|
4
|
+
before_action { redirect_to dashboard_path unless User.include?(MustVerifyEmail) }
|
5
|
+
|
6
|
+
layout 'guest'
|
7
|
+
|
8
|
+
def new
|
9
|
+
return redirect_to dashboard_path if Current.auth.user.has_verified_email?
|
10
|
+
|
11
|
+
render 'auth/verify_email'
|
12
|
+
end
|
13
|
+
|
14
|
+
def create
|
15
|
+
return redirect_to dashboard_path if Current.auth.user.has_verified_email?
|
16
|
+
|
17
|
+
Current.auth.user.send_email_verification_notification
|
18
|
+
|
19
|
+
redirect_back_or_to verification_notice_path, flash: { status: 'verification-link-sent' }
|
20
|
+
end
|
21
|
+
end
|
@@ -6,13 +6,13 @@ class Auth::NewPasswordController < ApplicationController
|
|
6
6
|
layout 'guest'
|
7
7
|
|
8
8
|
def new
|
9
|
-
@form = Auth::NewPasswordForm.new
|
9
|
+
@form = Auth::NewPasswordForm.new(params.permit(:token))
|
10
10
|
|
11
11
|
render 'auth/reset_password'
|
12
12
|
end
|
13
13
|
|
14
14
|
def create
|
15
|
-
@form = Auth::NewPasswordForm.new
|
15
|
+
@form = Auth::NewPasswordForm.new(params.permit(:token, :password, :password_confirmation))
|
16
16
|
|
17
17
|
return redirect_to login_path, flash: { status: 'Your password has been reset.' } if @form.reset?
|
18
18
|
|
@@ -12,7 +12,7 @@ class Auth::PasswordResetLinkController < ApplicationController
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def create
|
15
|
-
@form = Auth::SendPasswordResetLinkForm.new
|
15
|
+
@form = Auth::SendPasswordResetLinkForm.new(params.permit(:email))
|
16
16
|
|
17
17
|
return redirect_back_or_to password_request_path, flash: { status: 'We have emailed your password reset link.' } if @form.send_reset_link?
|
18
18
|
|
@@ -12,13 +12,17 @@ class Auth::RegisteredUserController < ApplicationController
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def create
|
15
|
-
@form = Auth::RegisterForm.new
|
15
|
+
@form = Auth::RegisterForm.new(params.permit(:name, :email, :password, :password_confirmation))
|
16
16
|
|
17
17
|
return render 'auth/register', status: :unprocessable_entity if @form.invalid?
|
18
18
|
|
19
19
|
user = User.create(name: @form.name, email: @form.email, password: @form.password)
|
20
20
|
|
21
|
-
|
21
|
+
if User.include?(MustVerifyEmail) && !user.has_verified_email?
|
22
|
+
user.send_email_verification_notification
|
23
|
+
end
|
24
|
+
|
25
|
+
Current.auth.login(user)
|
22
26
|
|
23
27
|
redirect_to dashboard_path
|
24
28
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Auth::VerifiedEmailController < ApplicationController
|
2
|
+
include ValidateSignature
|
3
|
+
|
4
|
+
skip_ensure_email_is_verified
|
5
|
+
|
6
|
+
before_action { redirect_to dashboard_path unless User.include?(MustVerifyEmail) }
|
7
|
+
|
8
|
+
def create
|
9
|
+
return redirect_to dashboard_path(verified: '1') if Current.auth.user.has_verified_email?
|
10
|
+
|
11
|
+
Current.auth.user.mark_email_as_verified if email_verification_request_is_authorized?
|
12
|
+
|
13
|
+
redirect_to dashboard_path(verified: '1')
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def email_verification_request_is_authorized?
|
19
|
+
return false if !ActiveSupport::SecurityUtils.secure_compare Current.auth.user.id.to_s, params[:id]
|
20
|
+
|
21
|
+
ActiveSupport::SecurityUtils.secure_compare Digest::SHA1.hexdigest(Current.auth.user.email), params[:hash]
|
22
|
+
end
|
23
|
+
end
|
@@ -3,11 +3,17 @@ module Authenticate
|
|
3
3
|
|
4
4
|
included do
|
5
5
|
before_action :authenticate!
|
6
|
+
before_action :ensure_email_is_verified! if User.include?(MustVerifyEmail)
|
6
7
|
end
|
7
8
|
|
8
9
|
class_methods do
|
9
10
|
def skip_authenticate(**options)
|
10
11
|
skip_before_action :authenticate!, **options
|
12
|
+
skip_ensure_email_is_verified(**options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def skip_ensure_email_is_verified(**options)
|
16
|
+
skip_before_action :ensure_email_is_verified!, **options if User.include?(MustVerifyEmail)
|
11
17
|
end
|
12
18
|
end
|
13
19
|
|
@@ -16,4 +22,8 @@ module Authenticate
|
|
16
22
|
def authenticate!
|
17
23
|
redirect_to login_path unless Current.auth.check?
|
18
24
|
end
|
25
|
+
|
26
|
+
def ensure_email_is_verified!
|
27
|
+
redirect_to verification_notice_path unless Current.auth.user && Current.auth.user.has_verified_email?
|
28
|
+
end
|
19
29
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ValidateSignature
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
before_action do
|
6
|
+
render file: "#{Rails.root}/public/404.html", layout: false, status: :not_found unless has_valid_signature?
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def has_valid_signature?
|
13
|
+
request.original_url.split('?')[0] == ActiveSupport::MessageVerifier.new(ENV.fetch('RAILS_MASTER_KEY', '')).verify(params[:signature])
|
14
|
+
rescue
|
15
|
+
false
|
16
|
+
end
|
17
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
class PasswordController < ApplicationController
|
2
2
|
def update
|
3
|
-
@update_password_form = UpdatePasswordForm.new
|
3
|
+
@update_password_form = UpdatePasswordForm.new(params.permit(:current_password, :password, :password_confirmation))
|
4
4
|
|
5
5
|
return render partial: 'profile/partials/update_password_form', status: :unprocessable_entity if @update_password_form.invalid?
|
6
6
|
|
@@ -8,7 +8,7 @@ class ProfileController < ApplicationController
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def update
|
11
|
-
@update_profile_information_form = UpdateProfileInformationForm.new
|
11
|
+
@update_profile_information_form = UpdateProfileInformationForm.new(params.permit(:name, :email))
|
12
12
|
|
13
13
|
return render partial: 'profile/partials/update_profile_information_form', status: :unprocessable_entity if @update_profile_information_form.invalid?
|
14
14
|
|
@@ -18,7 +18,7 @@ class ProfileController < ApplicationController
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def destroy
|
21
|
-
@delete_user_form = DeleteUserForm.new
|
21
|
+
@delete_user_form = DeleteUserForm.new(params.permit(:password))
|
22
22
|
|
23
23
|
return render partial: 'profile/partials/delete_user_form', status: :unprocessable_entity if @delete_user_form.invalid?
|
24
24
|
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
2
|
+
Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn't receive the email, we will gladly send you another.
|
3
|
+
</div>
|
4
|
+
<% if flash[:status] == 'verification-link-sent' %>
|
5
|
+
<div class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
|
6
|
+
A new verification link has been sent to the email address you provided during registration.
|
7
|
+
</div>
|
8
|
+
<% end %>
|
9
|
+
<div class="mt-4 flex items-center justify-between">
|
10
|
+
<form method="POST" action="<%= verification_send_path %>">
|
11
|
+
<div>
|
12
|
+
<%= render(PrimaryButtonComponent.new) do %>
|
13
|
+
Resend Verification Email
|
14
|
+
<% end %>
|
15
|
+
</div>
|
16
|
+
</form>
|
17
|
+
<form method="POST" action="<%= logout_path %>">
|
18
|
+
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
|
19
|
+
<button type="submit" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
|
20
|
+
Log Out
|
21
|
+
</button>
|
22
|
+
</form>
|
23
|
+
</div>
|
@@ -10,7 +10,7 @@
|
|
10
10
|
<%= render(DangerButtonComponent.new({"x-data": "", "x-on:click.prevent": "$dispatch('open-modal', 'confirm-user-deletion')"})) do %>
|
11
11
|
Delete Account
|
12
12
|
<% end %>
|
13
|
-
<%= render(ModalComponent.new({ :
|
13
|
+
<%= render(ModalComponent.new({ name: "confirm-user-deletion", show: @delete_user_form.error_messages.has_key?(:password), focusable: true })) do %>
|
14
14
|
<turbo-frame id="delete_user_form">
|
15
15
|
<%= form_with model: @delete_user_form, url: profile_destroy_path, method: "delete", class: "p-6" do %>
|
16
16
|
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|