orthodox 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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