orthodox 0.2.4 → 0.3.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/authentication/USAGE +14 -0
  3. data/lib/generators/authentication/authentication_generator.rb +214 -0
  4. data/lib/generators/authentication/templates/controllers/concerns/authentication.rb.erb +110 -0
  5. data/lib/generators/authentication/templates/controllers/concerns/two_factor_authentication.rb +40 -0
  6. data/lib/generators/authentication/templates/controllers/password_resets_controller.rb.erb +54 -0
  7. data/lib/generators/authentication/templates/controllers/sessions_controller.rb.erb +36 -0
  8. data/lib/generators/authentication/templates/controllers/tfa_sessions_controller.rb.erb +48 -0
  9. data/lib/generators/authentication/templates/controllers/tfas_controller.rb.erb +38 -0
  10. data/lib/generators/authentication/templates/helpers/otp_credentials_helper.rb +33 -0
  11. data/lib/generators/authentication/templates/javascript/tfa_forms.js +19 -0
  12. data/lib/generators/authentication/templates/models/concerns/authenticateable.rb +37 -0
  13. data/lib/generators/authentication/templates/models/concerns/otpable.rb +26 -0
  14. data/lib/generators/authentication/templates/models/concerns/password_resetable.rb +19 -0
  15. data/lib/generators/authentication/templates/models/otp_credential.rb.erb +133 -0
  16. data/lib/generators/authentication/templates/models/password_reset_token.rb +64 -0
  17. data/lib/generators/authentication/templates/models/session.rb.erb +80 -0
  18. data/lib/generators/authentication/templates/models/tfa_session.rb +77 -0
  19. data/lib/generators/authentication/templates/spec/models/otp_credential_spec.rb +215 -0
  20. data/lib/generators/authentication/templates/spec/models/password_reset_token_spec.rb +146 -0
  21. data/lib/generators/authentication/templates/spec/models/session_spec.rb.erb +45 -0
  22. data/lib/generators/authentication/templates/spec/models/tfa_session_spec.rb.erb +115 -0
  23. data/lib/generators/authentication/templates/spec/support/authentication_helpers.rb +18 -0
  24. data/lib/generators/authentication/templates/spec/support/factory_bot.rb +5 -0
  25. data/lib/generators/authentication/templates/spec/system/authentication_spec.rb.erb +25 -0
  26. data/lib/generators/authentication/templates/spec/system/password_resets_spec.rb.erb +73 -0
  27. data/lib/generators/authentication/templates/spec/system/tfa_authentication_spec.rb.erb +38 -0
  28. data/lib/generators/authentication/templates/views/mailers/password_reset_link.html.slim.erb +7 -0
  29. data/lib/generators/authentication/templates/views/password_resets/edit.html.slim.erb +16 -0
  30. data/lib/generators/authentication/templates/views/password_resets/new.html.slim.erb +12 -0
  31. data/lib/generators/authentication/templates/views/sessions/new.html.slim.erb +21 -0
  32. data/lib/generators/authentication/templates/views/tfa_sessions/new.html.slim.erb +26 -0
  33. data/lib/generators/authentication/templates/views/tfas/show.html.slim.erb +9 -0
  34. data/lib/generators/base_controller/USAGE +8 -0
  35. data/lib/generators/base_controller/base_controller_generator.rb +22 -0
  36. data/lib/generators/base_controller/templates/base_controller.rb.erb +7 -0
  37. data/lib/generators/layout_helper/USAGE +8 -0
  38. data/lib/generators/layout_helper/layout_helper_generator.rb +55 -0
  39. data/lib/orthodox/version.rb +1 -1
  40. metadata +39 -2
@@ -0,0 +1,45 @@
1
+ require "rails_helper"
2
+
3
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
4
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
5
+ RSpec.describe <%= class_name %>Session, type: :model do
6
+
7
+ context "when credentials valid" do
8
+
9
+ let(:<%= singular_name %>) { create(:<%= singular_name %>) }
10
+
11
+ before do
12
+ subject.email = <%= singular_name %>.email
13
+ subject.password = <%= singular_name %>.password
14
+ end
15
+
16
+ it { is_expected.to be_valid }
17
+
18
+ it "sets the <%= singular_name %> attribute to the matching record" do
19
+ subject.valid?
20
+ expect(subject.<%= singular_name %>).to eql(<%= singular_name %>)
21
+ end
22
+
23
+ end
24
+
25
+ context "when credentials are invalid" do
26
+
27
+ before do
28
+ subject.email = Faker::Internet.safe_email
29
+ subject.password = "wrong-password"
30
+ end
31
+
32
+ it { is_expected.not_to be_valid }
33
+
34
+ it "sets the <%= singular_name %> attribute to nil" do
35
+ expect(subject.<%= singular_name %>).to be_nil
36
+ end
37
+
38
+ it "has errors on base" do
39
+ subject.valid?
40
+ expect(subject.errors[:base]).to be_present
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,115 @@
1
+ require "rails_helper"
2
+
3
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
4
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
5
+ RSpec.describe TfaSession, type: :model do
6
+
7
+ let!(:<%= singular_name %>) { create(:<%= singular_name %>) }
8
+
9
+ before do
10
+ <%= singular_name %>.create_otp_credential!
11
+ end
12
+
13
+ describe "#valid?" do
14
+
15
+ subject { TfaSession.new(record: <%= singular_name %>) }
16
+
17
+ context "when otp correct" do
18
+
19
+ before do
20
+ subject.otp = <%= singular_name %>.otp_credential.send(:current_otp)
21
+ end
22
+
23
+ it { is_expected.to be_valid }
24
+
25
+ end
26
+
27
+ context "when otp incorrect" do
28
+
29
+ before do
30
+ subject.otp = "12345"
31
+ end
32
+
33
+ it { is_expected.to be_invalid }
34
+
35
+ end
36
+
37
+ context "when recovery code correct" do
38
+
39
+ before do
40
+ subject.recovery_code = <%= singular_name %>.otp_credential.recovery_codes.sample
41
+ end
42
+
43
+ it { is_expected.to be_valid }
44
+
45
+ end
46
+
47
+ context "when recovery code incorrect" do
48
+
49
+ before do
50
+ subject.recovery_code = "abcd-efgh"
51
+ end
52
+
53
+ it { is_expected.to be_invalid }
54
+
55
+ end
56
+ end
57
+
58
+
59
+ describe "#otp?" do
60
+
61
+ context "when present" do
62
+
63
+ before do
64
+ subject.otp = "12345"
65
+ end
66
+
67
+ it "is expected to be truthy" do
68
+ expect(subject.otp?).to be_truthy
69
+ end
70
+
71
+ end
72
+
73
+ context "when blank" do
74
+
75
+ before do
76
+ subject.otp = nil
77
+ end
78
+
79
+ it "is expected to be falsey" do
80
+ expect(subject.otp?).to be_falsey
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+
87
+ describe "#recovery_code?" do
88
+
89
+ context "when present" do
90
+
91
+ before do
92
+ subject.recovery_code = "abcd-defg"
93
+ end
94
+
95
+ it "is expected to be truthy" do
96
+ expect(subject.recovery_code?).to be_truthy
97
+ end
98
+
99
+ end
100
+
101
+ context "when blank" do
102
+
103
+ before do
104
+ subject.recovery_code = nil
105
+ end
106
+
107
+ it "is expected to be falsey" do
108
+ expect(subject.recovery_code?).to be_falsey
109
+ end
110
+
111
+ end
112
+
113
+ end
114
+
115
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal
2
+ #
3
+ # Helpers for automating sign-in and out during tests
4
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
5
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
6
+ #
7
+ module AuthenticationHelpers
8
+ def sign_in(record, **options)
9
+ type = record.class.model_name.singular
10
+ visit(public_send("new_#{type.pluralize}_session_path"))
11
+ fill_in :"#{type}_session_email", with: record.email
12
+ fill_in :"#{type}_session_password", with: record.password
13
+ click_button "Sign in"
14
+ end
15
+ end
16
+ RSpec.configure do |config|
17
+ config.include(AuthenticationHelpers, type: :system)
18
+ end
@@ -0,0 +1,5 @@
1
+ require "factory_bot"
2
+
3
+ RSpec.configure do |config|
4
+ config.include(FactoryBot::Syntax::Methods)
5
+ end
@@ -0,0 +1,25 @@
1
+ require "rails_helper"
2
+
3
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
4
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
5
+ RSpec.describe "<%= plural_class_name %>::Authentication", type: :system do
6
+
7
+ let!(:<%= singular_name %>) { create(:<%= singular_name %>) }
8
+
9
+ scenario "A <%= singular_name %> signs in successfully" do
10
+ visit new_<%= plural_name %>_session_path
11
+ fill_in :<%= singular_name %>_session_email, with: <%= singular_name %>.email
12
+ fill_in :<%= singular_name %>_session_password, with: <%= singular_name %>.password
13
+ click_button "Sign in"
14
+ expect(current_path).to eql(<%= plural_name %>_dashboard_path)
15
+ end
16
+
17
+ scenario "A <%= singular_name %> attempts to sign in unsuccessfully" do
18
+ visit new_<%= plural_name %>_session_path
19
+ fill_in :<%= singular_name %>_session_email, with: <%= singular_name %>.email
20
+ fill_in :<%= singular_name %>_session_password, with: "WRONG-PASSWORD"
21
+ click_button "Sign in"
22
+ expect(page).to have_text(<%= class_name%>Session::INVALID_CREDENTIALS)
23
+ end
24
+
25
+ end
@@ -0,0 +1,73 @@
1
+ require "rails_helper"
2
+
3
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
4
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
5
+ RSpec.describe "<%= plural_class_name %>::PasswordResets", type: :system do
6
+
7
+ include ActiveSupport::Testing::TimeHelpers
8
+
9
+ let(:<%= singular_name %>) { create(:<%= singular_name %>) }
10
+
11
+ scenario "<%= class_name %> resets password successfully" do
12
+ visit new_<%= plural_name %>_session_path
13
+ click_link "Forgot password"
14
+ fill_in :<%= singular_name %>_email, with: <%= singular_name %>.email
15
+ click_button "Send"
16
+ expect(current_path).to eql(new_<%= plural_name %>_session_path)
17
+ expect(enqueued_jobs)
18
+ expect(last_mailer_job[:args][0..1]).to eql(["<%= class_name %>Mailer", "password_reset_link"])
19
+ visit(edit_<%= plural_name %>_password_reset_path(token: <%= singular_name %>.password_reset_token.secret))
20
+ fill_in(:<%= singular_name %>_password, with: "new-password")
21
+ fill_in(:<%= singular_name %>_password_confirmation, with: "new-password")
22
+ click_button("Save")
23
+ expect(current_path).to eql(new_<%= plural_name %>_session_path)
24
+ expect(<%= singular_name %>.reload.authenticate("new-password")).to eql(<%= singular_name %>)
25
+ end
26
+
27
+ scenario "<%= class_name %> clicks link once already used" do
28
+ visit new_<%= plural_name %>_session_path
29
+ click_link "Forgot password"
30
+ fill_in :<%= singular_name %>_email, with: <%= singular_name %>.email
31
+ click_button "Send"
32
+ expect(current_path).to eql(new_<%= plural_name %>_session_path)
33
+ expect(enqueued_jobs)
34
+ expect(last_mailer_job[:args][0..1]).to eql(["<%= class_name %>Mailer", "password_reset_link"])
35
+ visit(edit_<%= plural_name %>_password_reset_path(token: <%= singular_name %>.password_reset_token.secret))
36
+ fill_in(:<%= singular_name %>_password, with: "new-password")
37
+ fill_in(:<%= singular_name %>_password_confirmation, with: "new-password")
38
+ click_button("Save")
39
+ expect(current_path).to eql(new_<%= plural_name %>_session_path)
40
+ visit(edit_<%= plural_name %>_password_reset_path(token: <%= singular_name %>.password_reset_token.secret))
41
+ expect(current_path).to eql(new_<%= plural_name %>_session_path)
42
+ end
43
+
44
+ scenario "<%= class_name %> clicks link once expired" do
45
+ visit new_<%= plural_name %>_session_path
46
+ click_link "Forgot password"
47
+ fill_in :<%= singular_name %>_email, with: <%= singular_name %>.email
48
+ click_button "Send"
49
+ expect(current_path).to eql(new_<%= plural_name %>_session_path)
50
+ expect(enqueued_jobs)
51
+ expect(last_mailer_job[:args][0..1]).to eql(["<%= class_name %>Mailer", "password_reset_link"])
52
+ travel(16.minutes) do
53
+ visit(edit_<%= plural_name %>_password_reset_path(token: <%= singular_name %>.password_reset_token.secret))
54
+ expect(current_path).to eql(new_<%= plural_name %>_session_path)
55
+ end
56
+ end
57
+
58
+ scenario "<%= class_name %> email address not recognised" do
59
+ visit new_<%= plural_name %>_session_path
60
+ click_link "Forgot password"
61
+ fill_in :<%= singular_name %>_email, with: "wrong-email@example.com"
62
+ click_button "Send"
63
+ expect(current_path).to eql(new_<%= plural_name %>_session_path)
64
+ expect(last_mailer_job).to be_nil
65
+ end
66
+
67
+ private
68
+
69
+ def last_mailer_job
70
+ enqueued_jobs.select { |job| job[:job] == ActionMailer::MailDeliveryJob }.last
71
+ end
72
+
73
+ end
@@ -0,0 +1,38 @@
1
+ require "rails_helper"
2
+
3
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
4
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
5
+ RSpec.describe "<%= plural_class_name %>::TfaAuthentication", type: :system do
6
+
7
+ let!(:<%= singular_name %>) { create(:<%= singular_name %>) }
8
+
9
+ before do
10
+ <%= singular_name %>.create_otp_credential!
11
+ end
12
+
13
+ scenario "A <%= singular_name %> signs in successfully with otp" do
14
+ sign_in(<%= singular_name %>, tfa: false)
15
+ expect(current_path).to eql(new_<%= plural_name %>_tfa_session_path)
16
+ fill_in(:tfa_session_otp, with: <%= singular_name %>.otp_credential.send(:current_otp))
17
+ click_button("Submit code")
18
+ expect(current_path).to eql(<%= plural_name %>_dashboard_path)
19
+ end
20
+
21
+ scenario "A <%= singular_name %> attempts with the wrong otp" do
22
+ sign_in(<%= singular_name %>, tfa: false)
23
+ expect(current_path).to eql(new_<%= plural_name %>_tfa_session_path)
24
+ fill_in(:tfa_session_otp, with: '999999')
25
+ click_button("Submit code")
26
+ expect(page).to have_text("not correct")
27
+ end
28
+
29
+ scenario "A <%= singular_name %> signs in successfully with a recovery code" do
30
+ sign_in(<%= singular_name %>, tfa: false)
31
+ expect(current_path).to eql(new_<%= plural_name %>_tfa_session_path)
32
+ click_link("Use a recovery code")
33
+ fill_in(:tfa_session_recovery_code, with: <%= singular_name %>.otp_credential.recovery_codes.sample)
34
+ click_button("Submit code")
35
+ expect(current_path).to eql(<%= plural_name %>_dashboard_path)
36
+ end
37
+
38
+ end
@@ -0,0 +1,7 @@
1
+ p Reset your password by clicking on this link
2
+
3
+ = link_to(edit_<%= plural_name %>_password_reset_url(token: @<%= singular_name%>.password_reset_token.secret), edit_<%= plural_name %>_password_reset_url(token: @<%= singular_name%>.password_reset_token.secret))
4
+
5
+ p
6
+ em
7
+ Note, this link will expire after #{@<%= singular_name%>.password_reset_token.expires_at.to_s(:long)}
@@ -0,0 +1,16 @@
1
+ - title "Set a new password"
2
+
3
+ h1 = title
4
+
5
+ = form_tag(<%= plural_name %>_password_reset_path(token: params[:token]), class: "form", method: :patch) do |f|
6
+
7
+ .form-group
8
+ = label(:<%= singular_name %>, :password, "Choose a new password")
9
+ = password_field(:<%= singular_name %>, :password, class: "form-control")
10
+
11
+ .form-group
12
+ = label(:<%= singular_name %>, :password_confirmation, "Re-enter your password")
13
+ = password_field(:<%= singular_name %>, :password_confirmation, class: "form-control")
14
+
15
+ = submit_tag("Save changes", class: "btn btn-primary")
16
+
@@ -0,0 +1,12 @@
1
+ - title "Request a password reset"
2
+
3
+ h1 = title
4
+
5
+ = form_tag(<%= plural_name %>_password_resets_path, class: "form") do |f|
6
+
7
+ .form-group
8
+ = label(:<%= singular_name %>, :email, "Email address")
9
+ = email_field(:<%= singular_name %>, :email, class: "form-control")
10
+
11
+ = submit_tag("Send me a link", class: "btn btn-primary")
12
+
@@ -0,0 +1,21 @@
1
+ - title "Sign in"
2
+
3
+ = form_for(@<%= singular_name %>_session, url: <%= plural_name %>_session_path) do |f|
4
+
5
+ ul
6
+ - @<%= singular_name %>_session.errors.full_messages.each do |error_message|
7
+ li = error_message
8
+
9
+ .form-group
10
+ = f.label(:email, "Email")
11
+ = f.email_field(:email, class: "form-control")
12
+
13
+ .form-group
14
+ = f.label(:password, "Password")
15
+ = f.password_field(:password, class: "form-control")
16
+
17
+ div
18
+ = f.submit("Sign in")
19
+
20
+ div
21
+ = link_to("Forgot password?", new_<%= plural_name %>_password_reset_path)
@@ -0,0 +1,26 @@
1
+ - title "Two-factor authentication"
2
+
3
+ = form_for(@tfa_session, url: <%= plural_name %>_tfa_session_path) do |f|
4
+
5
+ ul
6
+ - @tfa_session.errors.full_messages.each do |error_message|
7
+ li = error_message
8
+
9
+ .form-group.js-tfa-field-group
10
+ = f.label(:otp, "Enter your one-time password")
11
+ = f.text_field(:otp, class: "form-control", autofocus: true, pattern: "[0-9]{3,6}")
12
+
13
+ .form-group.js-tfa-field-group.d-none
14
+ = f.label(:recovery_code, "Use a recovery code")
15
+ = f.text_field(:recovery_code, class: "form-control", pattern: "[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}")
16
+
17
+ div
18
+ = link_to("Use a recovery code instead", "#",
19
+ class: "js-tfa-link js-tfa-link--recovery-code")
20
+
21
+ = link_to("Use one-time password instead", "#",
22
+ class: "js-tfa-link js-tfa-link--otp d-none")
23
+
24
+ div
25
+ = f.submit("Submit code")
26
+
@@ -0,0 +1,9 @@
1
+ - title "Your two-factor authentication"
2
+
3
+ h1 Your Two-Factor Authentication
4
+
5
+ = svg_url_for_otp_credential(current_<%= singular_name %>.otp_credential)
6
+
7
+ pre
8
+ code.code
9
+ = current_<%= singular_name %>.otp_credential.recovery_codes.join("\n")
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Create a base controller for a namespace
3
+
4
+ Example:
5
+ rails generate base_controller Member
6
+
7
+ This will create:
8
+ app/controllers/members/base_controller.rb
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal
2
+
3
+ class BaseControllerGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path('templates', __dir__)
5
+
6
+ check_class_collision suffix: "Controller"
7
+
8
+ desc "This generator creates a base controller for the named namespace"
9
+
10
+ def ensure_file
11
+ template "base_controller.rb.erb",
12
+ File.join("app", "controllers", plural_file_name, "base_controller.rb")
13
+ end
14
+
15
+ private
16
+
17
+
18
+ def namespace_module
19
+ file_name.to_s.split("/").first.classify.pluralize
20
+ end
21
+
22
+ end