devise-otp 0.6.0 → 0.8.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +7 -10
  3. data/.gitignore +3 -1
  4. data/CHANGELOG.md +54 -8
  5. data/Gemfile +10 -0
  6. data/README.md +8 -2
  7. data/app/controllers/devise_otp/devise/otp_credentials_controller.rb +46 -27
  8. data/app/controllers/devise_otp/devise/otp_tokens_controller.rb +28 -10
  9. data/app/views/devise/otp_credentials/show.html.erb +6 -6
  10. data/app/views/devise/otp_tokens/_token_secret.html.erb +3 -4
  11. data/app/views/devise/otp_tokens/edit.html.erb +26 -0
  12. data/app/views/devise/otp_tokens/show.html.erb +7 -14
  13. data/config/locales/en.yml +23 -14
  14. data/devise-otp.gemspec +3 -13
  15. data/docs/QR_CODES.md +1 -40
  16. data/lib/devise/strategies/database_authenticatable.rb +64 -0
  17. data/lib/devise-otp/version.rb +1 -1
  18. data/lib/devise-otp.rb +31 -11
  19. data/lib/devise_otp_authenticatable/controllers/helpers.rb +9 -10
  20. data/lib/devise_otp_authenticatable/controllers/public_helpers.rb +39 -0
  21. data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +10 -0
  22. data/lib/devise_otp_authenticatable/engine.rb +2 -5
  23. data/lib/devise_otp_authenticatable/hooks/refreshable.rb +5 -0
  24. data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +22 -20
  25. data/lib/devise_otp_authenticatable/routes.rb +3 -1
  26. data/lib/generators/active_record/templates/migration.rb +1 -1
  27. data/test/dummy/app/controllers/admin_posts_controller.rb +85 -0
  28. data/test/dummy/app/controllers/application_controller.rb +0 -1
  29. data/test/dummy/app/controllers/base_controller.rb +6 -0
  30. data/test/dummy/app/models/admin.rb +25 -0
  31. data/test/dummy/app/views/admin_posts/_form.html.erb +25 -0
  32. data/test/dummy/app/views/admin_posts/edit.html.erb +6 -0
  33. data/test/dummy/app/views/admin_posts/index.html.erb +25 -0
  34. data/test/dummy/app/views/admin_posts/new.html.erb +5 -0
  35. data/test/dummy/app/views/admin_posts/show.html.erb +15 -0
  36. data/test/dummy/app/views/base/home.html.erb +1 -0
  37. data/test/dummy/config/application.rb +0 -2
  38. data/test/dummy/config/routes.rb +4 -1
  39. data/test/dummy/db/migrate/20240604000001_create_admins.rb +9 -0
  40. data/test/dummy/db/migrate/20240604000002_add_devise_to_admins.rb +52 -0
  41. data/test/dummy/db/migrate/20240604000003_devise_otp_add_to_admins.rb +28 -0
  42. data/test/integration/disable_token_test.rb +53 -0
  43. data/test/integration/enable_otp_form_test.rb +57 -0
  44. data/test/integration/persistence_test.rb +3 -6
  45. data/test/integration/refresh_test.rb +32 -0
  46. data/test/integration/reset_token_test.rb +45 -0
  47. data/test/integration/sign_in_test.rb +10 -14
  48. data/test/integration/trackable_test.rb +50 -0
  49. data/test/integration_tests_helper.rb +24 -6
  50. data/test/models/otp_authenticatable_test.rb +62 -27
  51. data/test/orm/active_record.rb +6 -1
  52. data/test/test_helper.rb +1 -71
  53. metadata +26 -135
  54. data/lib/devise_otp_authenticatable/hooks/sessions.rb +0 -58
  55. data/lib/devise_otp_authenticatable/hooks.rb +0 -11
  56. data/test/integration/token_test.rb +0 -30
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'devise/strategies/authenticatable'
4
+
5
+ module Devise
6
+ module Strategies
7
+ # Default strategy for signing in a user, based on their email and password in the database.
8
+ class DatabaseAuthenticatable < Authenticatable
9
+ def authenticate!
10
+ resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
11
+ hashed = false
12
+
13
+ if validate(resource){ hashed = true; resource.valid_password?(password) }
14
+ if otp_challenge_required_on?(resource)
15
+ # Redirect to challenge
16
+ challenge = resource.generate_otp_challenge!
17
+ redirect!(otp_challenge_url, {:challenge => challenge})
18
+ else
19
+ # Sign in user as usual
20
+ remember_me(resource)
21
+ resource.after_database_authentication
22
+ success!(resource)
23
+ end
24
+ end
25
+
26
+ # In paranoid mode, hash the password even when a resource doesn't exist for the given authentication key.
27
+ # This is necessary to prevent enumeration attacks - e.g. the request is faster when a resource doesn't
28
+ # exist in the database if the password hashing algorithm is not called.
29
+ mapping.to.new.password = password if !hashed && Devise.paranoid
30
+ unless resource
31
+ Devise.paranoid ? fail(:invalid) : fail(:not_found_in_database)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ #
38
+ # resource should be challenged for otp
39
+ #
40
+ def otp_challenge_required_on?(resource)
41
+ resource.respond_to?(:otp_enabled?) && resource.otp_enabled?
42
+ end
43
+
44
+ def otp_challenge_url
45
+ if Rails.env.development? || Rails.env.test?
46
+ host = "#{request.host}:#{request.port}"
47
+ else
48
+ host = "#{request.host}"
49
+ end
50
+
51
+ path_fragments = ["otp", mapping.path_names[:credentials]]
52
+ if mapping.fullpath == "/"
53
+ path = mapping.fullpath + path_fragments.join("/")
54
+ else
55
+ path = path_fragments.prepend(mapping.fullpath).join("/")
56
+ end
57
+
58
+ request.protocol + host + path
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ Warden::Strategies.add(:database_authenticatable, Devise::Strategies::DatabaseAuthenticatable)
@@ -1,5 +1,5 @@
1
1
  module Devise
2
2
  module OTP
3
- VERSION = "0.6.0"
3
+ VERSION = "0.8.0"
4
4
  end
5
5
  end
data/lib/devise-otp.rb CHANGED
@@ -9,6 +9,24 @@ require "active_support/concern"
9
9
 
10
10
  require "devise"
11
11
 
12
+ #
13
+ # define DeviseOtpAuthenticatable module, and autoload hooks and helpers
14
+ #
15
+ module DeviseOtpAuthenticatable
16
+ module Controllers
17
+ autoload :Helpers, "devise_otp_authenticatable/controllers/helpers"
18
+ autoload :UrlHelpers, "devise_otp_authenticatable/controllers/url_helpers"
19
+ autoload :PublicHelpers, "devise_otp_authenticatable/controllers/public_helpers"
20
+ end
21
+ end
22
+
23
+ require "devise_otp_authenticatable/routes"
24
+ require "devise_otp_authenticatable/engine"
25
+ require "devise_otp_authenticatable/hooks/refreshable"
26
+
27
+ #
28
+ # update Devise module with additions needed for DeviseOtpAuthenticatable
29
+ #
12
30
  module Devise
13
31
  mattr_accessor :otp_mandatory
14
32
  @@otp_mandatory = false
@@ -49,22 +67,24 @@ module Devise
49
67
  mattr_accessor :otp_controller_path
50
68
  @@otp_controller_path = "devise"
51
69
 
70
+ #
71
+ # add PublicHelpers to helpers class variable to ensure that per-mapping helpers are present.
72
+ # this integrates with the "define_helpers," which is run when adding each mapping in the Devise gem (lib/devise.rb#541)
73
+ #
74
+ @@helpers << DeviseOtpAuthenticatable::Controllers::PublicHelpers
75
+
52
76
  module Otp
53
77
  end
54
- end
55
-
56
- module DeviseOtpAuthenticatable
57
- autoload :Hooks, "devise_otp_authenticatable/hooks"
58
78
 
59
- module Controllers
60
- autoload :Helpers, "devise_otp_authenticatable/controllers/helpers"
61
- autoload :UrlHelpers, "devise_otp_authenticatable/controllers/url_helpers"
62
- end
63
79
  end
64
80
 
65
- require "devise_otp_authenticatable/routes"
66
- require "devise_otp_authenticatable/engine"
67
-
68
81
  Devise.add_module :otp_authenticatable,
69
82
  controller: :tokens,
70
83
  model: "devise_otp_authenticatable/models/otp_authenticatable", route: :otp
84
+
85
+ #
86
+ # add PublicHelpers after adding Devise module to ensure that per-mapping routes from above are included
87
+ #
88
+ ActiveSupport.on_load(:action_controller) do
89
+ include DeviseOtpAuthenticatable::Controllers::PublicHelpers
90
+ end
@@ -39,7 +39,6 @@ module DeviseOtpAuthenticatable
39
39
  end
40
40
  end
41
41
 
42
- # fixme do cookies and persistence need to be scoped? probably
43
42
  #
44
43
  # check if the resource needs a credentials refresh. IE, they need to be asked a password again to access
45
44
  # this resource.
@@ -47,8 +46,8 @@ module DeviseOtpAuthenticatable
47
46
  def needs_credentials_refresh?(resource)
48
47
  return false unless resource.class.otp_credentials_refresh
49
48
 
50
- (!session[otp_scoped_refresh_property].present? ||
51
- (session[otp_scoped_refresh_property] < DateTime.now)).tap { |need| otp_set_refresh_return_url if need }
49
+ (!warden.session(resource_name)[otp_refresh_property].present? ||
50
+ (warden.session(resource_name)[otp_refresh_property] < DateTime.now)).tap { |need| otp_set_refresh_return_url if need }
52
51
  end
53
52
 
54
53
  #
@@ -56,7 +55,7 @@ module DeviseOtpAuthenticatable
56
55
  #
57
56
  def otp_refresh_credentials_for(resource)
58
57
  return false unless resource.class.otp_credentials_refresh
59
- session[otp_scoped_refresh_property] = (Time.now + resource.class.otp_credentials_refresh)
58
+ warden.session(resource_name)[otp_refresh_property] = (Time.now + resource.class.otp_credentials_refresh)
60
59
  end
61
60
 
62
61
  #
@@ -85,19 +84,19 @@ module DeviseOtpAuthenticatable
85
84
  end
86
85
 
87
86
  def otp_set_refresh_return_url
88
- session[otp_scoped_refresh_return_url_property] = request.fullpath
87
+ warden.session(resource_name)[otp_refresh_return_url_property] = request.fullpath
89
88
  end
90
89
 
91
90
  def otp_fetch_refresh_return_url
92
- session.delete(otp_scoped_refresh_return_url_property) { :root }
91
+ warden.session(resource_name).delete(otp_refresh_return_url_property) { :root }
93
92
  end
94
93
 
95
- def otp_scoped_refresh_return_url_property
96
- "otp_#{resource_name}refresh_return_url".to_sym
94
+ def otp_refresh_return_url_property
95
+ "refresh_return_url"
97
96
  end
98
97
 
99
- def otp_scoped_refresh_property
100
- "otp_#{resource_name}refresh_after".to_sym
98
+ def otp_refresh_property
99
+ "credentials_refreshed_at"
101
100
  end
102
101
 
103
102
  def otp_scoped_persistence_cookie
@@ -0,0 +1,39 @@
1
+ module DeviseOtpAuthenticatable
2
+ module Controllers
3
+ module PublicHelpers
4
+ extend ActiveSupport::Concern
5
+
6
+ def self.generate_helpers!
7
+ Devise.mappings.each do |key, mapping|
8
+ self.define_helpers(mapping)
9
+ end
10
+ end
11
+
12
+ def self.define_helpers(mapping) #:nodoc:
13
+ mapping = mapping.name
14
+
15
+ class_eval <<-METHODS, __FILE__, __LINE__ + 1
16
+ def ensure_mandatory_#{mapping}_otp!
17
+ resource = current_#{mapping}
18
+ if !devise_controller?
19
+ if mandatory_otp_missing_on?(resource)
20
+ redirect_to edit_#{mapping}_otp_token_path
21
+ end
22
+ end
23
+ end
24
+ METHODS
25
+ end
26
+
27
+ def otp_mandatory_on?(resource)
28
+ return false unless resource.respond_to?(:otp_mandatory)
29
+
30
+ resource.class.otp_mandatory or resource.otp_mandatory
31
+ end
32
+
33
+ def mandatory_otp_missing_on?(resource)
34
+ otp_mandatory_on?(resource) && !resource.otp_enabled
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -21,6 +21,16 @@ module DeviseOtpAuthenticatable
21
21
  send("#{scope}_otp_token_path", opts)
22
22
  end
23
23
 
24
+ def edit_otp_token_path_for(resource_or_scope, opts = {})
25
+ scope = ::Devise::Mapping.find_scope!(resource_or_scope)
26
+ send("edit_#{scope}_otp_token_path", opts)
27
+ end
28
+
29
+ def reset_otp_token_path_for(resource_or_scope, opts = {})
30
+ scope = ::Devise::Mapping.find_scope!(resource_or_scope)
31
+ send("reset_#{scope}_otp_token_path", opts)
32
+ end
33
+
24
34
  def otp_credential_path_for(resource_or_scope, opts = {})
25
35
  scope = ::Devise::Mapping.find_scope!(resource_or_scope)
26
36
  send("#{scope}_otp_credential_path", opts)
@@ -3,20 +3,17 @@ module DeviseOtpAuthenticatable
3
3
  config.devise_otp = ActiveSupport::OrderedOptions.new
4
4
  config.devise_otp.precompile_assets = true
5
5
 
6
- # We use to_prepare instead of after_initialize here because Devise is a Rails engine;
7
- config.to_prepare do
8
- DeviseOtpAuthenticatable::Hooks.apply
9
- end
10
-
11
6
  initializer "devise-otp", group: :all do |app|
12
7
  ActiveSupport.on_load(:devise_controller) do
13
8
  include DeviseOtpAuthenticatable::Controllers::UrlHelpers
14
9
  include DeviseOtpAuthenticatable::Controllers::Helpers
10
+ include DeviseOtpAuthenticatable::Controllers::PublicHelpers
15
11
  end
16
12
 
17
13
  ActiveSupport.on_load(:action_view) do
18
14
  include DeviseOtpAuthenticatable::Controllers::UrlHelpers
19
15
  include DeviseOtpAuthenticatable::Controllers::Helpers
16
+ include DeviseOtpAuthenticatable::Controllers::PublicHelpers
20
17
  end
21
18
 
22
19
  # See: https://guides.rubyonrails.org/engines.html#separate-assets-and-precompiling
@@ -0,0 +1,5 @@
1
+ # After each sign in, update credentials refreshed at time
2
+ Warden::Manager.after_set_user except: :fetch do |record, warden, options|
3
+ warden.session(options[:scope])["credentials_refreshed_at"] = (Time.now + record.class.otp_credentials_refresh)
4
+ end
5
+
@@ -5,8 +5,6 @@ module Devise::Models
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- before_validation :generate_otp_auth_secret, on: :create
9
- before_validation :generate_otp_persistence_seed, on: :create
10
8
  scope :with_valid_otp_challenge, lambda { |time| where("otp_challenge_expires > ?", time) }
11
9
  end
12
10
 
@@ -36,21 +34,6 @@ module Devise::Models
36
34
  email
37
35
  end
38
36
 
39
- def reset_otp_credentials
40
- @time_based_otp = nil
41
- @recovery_otp = nil
42
- generate_otp_auth_secret
43
- reset_otp_persistence
44
- update!(otp_enabled: false,
45
- otp_session_challenge: nil, otp_challenge_expires: nil,
46
- otp_recovery_counter: 0)
47
- end
48
-
49
- def reset_otp_credentials!
50
- reset_otp_credentials
51
- save!
52
- end
53
-
54
37
  def reset_otp_persistence
55
38
  generate_otp_persistence_seed
56
39
  end
@@ -60,11 +43,30 @@ module Devise::Models
60
43
  save!
61
44
  end
62
45
 
63
- def enable_otp!
64
- if otp_persistence_seed.nil?
65
- reset_otp_credentials!
46
+ def populate_otp_secrets!
47
+ if [otp_auth_secret, otp_recovery_secret, otp_persistence_seed].any? { |a| a.blank? }
48
+ generate_otp_auth_secret
49
+ generate_otp_persistence_seed
50
+ self.save!
66
51
  end
52
+ end
53
+
54
+ def clear_otp_fields!
55
+ @time_based_otp = nil
56
+ @recovery_otp = nil
57
+
58
+ self.update!(
59
+ :otp_auth_secret => nil,
60
+ :otp_recovery_secret => nil,
61
+ :otp_persistence_seed => nil,
62
+ :otp_session_challenge => nil,
63
+ :otp_challenge_expires => nil,
64
+ :otp_failed_attempts => 0,
65
+ :otp_recovery_counter => 0
66
+ )
67
+ end
67
68
 
69
+ def enable_otp!
68
70
  update!(otp_enabled: true, otp_enabled_on: Time.now)
69
71
  end
70
72
 
@@ -4,7 +4,7 @@ module ActionDispatch::Routing
4
4
 
5
5
  def devise_otp(mapping, controllers)
6
6
  namespace :otp, module: :devise_otp do
7
- resource :token, only: [:show, :update, :destroy],
7
+ resource :token, only: [:show, :edit, :update, :destroy],
8
8
  path: mapping.path_names[:token], controller: controllers[:otp_tokens] do
9
9
  if Devise.otp_trust_persistence
10
10
  get :persistence, action: "get_persistence"
@@ -13,6 +13,7 @@ module ActionDispatch::Routing
13
13
  end
14
14
 
15
15
  get :recovery
16
+ post :reset
16
17
  end
17
18
 
18
19
  resource :credential, only: [:show, :update],
@@ -20,6 +21,7 @@ module ActionDispatch::Routing
20
21
  get :refresh, action: "get_refresh"
21
22
  put :refresh, action: "set_refresh"
22
23
  end
24
+
23
25
  end
24
26
  end
25
27
  end
@@ -1,4 +1,4 @@
1
- class DeviseOtpAddTo<%= table_name.camelize %> < ActiveRecord::Migration
1
+ class DeviseOtpAddTo<%= table_name.camelize %> < ActiveRecord::Migration[7.0]
2
2
  def self.up
3
3
  change_table :<%= table_name %> do |t|
4
4
  t.string :otp_auth_secret
@@ -0,0 +1,85 @@
1
+ class AdminPostsController < ApplicationController
2
+ before_action :authenticate_admin!
3
+
4
+ # GET /posts
5
+ # GET /posts.json
6
+ def index
7
+ @posts = Post.all
8
+
9
+ respond_to do |format|
10
+ format.html # index.html.erb
11
+ format.json { render json: @posts }
12
+ end
13
+ end
14
+
15
+ # GET /posts/1
16
+ # GET /posts/1.json
17
+ def show
18
+ @post = Post.find(params[:id])
19
+
20
+ respond_to do |format|
21
+ format.html # show.html.erb
22
+ format.json { render json: @post }
23
+ end
24
+ end
25
+
26
+ # GET /posts/new
27
+ # GET /posts/new.json
28
+ def new
29
+ @post = Post.new
30
+
31
+ respond_to do |format|
32
+ format.html # new.html.erb
33
+ format.json { render json: @post }
34
+ end
35
+ end
36
+
37
+ # GET /posts/1/edit
38
+ def edit
39
+ @post = Post.find(params[:id])
40
+ end
41
+
42
+ # POST /posts
43
+ # POST /posts.json
44
+ def create
45
+ @post = Post.new(params[:post])
46
+
47
+ respond_to do |format|
48
+ if @post.save
49
+ format.html { redirect_to @post, notice: "Post was successfully created." }
50
+ format.json { render json: @post, status: :created, location: @post }
51
+ else
52
+ format.html { render action: "new" }
53
+ format.json { render json: @post.errors, status: :unprocessable_entity }
54
+ end
55
+ end
56
+ end
57
+
58
+ # PUT /posts/1
59
+ # PUT /posts/1.json
60
+ def update
61
+ @post = Post.find(params[:id])
62
+
63
+ respond_to do |format|
64
+ if @post.update_attributes(params[:post])
65
+ format.html { redirect_to @post, notice: "Post was successfully updated." }
66
+ format.json { head :ok }
67
+ else
68
+ format.html { render action: "edit" }
69
+ format.json { render json: @post.errors, status: :unprocessable_entity }
70
+ end
71
+ end
72
+ end
73
+
74
+ # DELETE /posts/1
75
+ # DELETE /posts/1.json
76
+ def destroy
77
+ @post = Post.find(params[:id])
78
+ @post.destroy
79
+
80
+ respond_to do |format|
81
+ format.html { redirect_to posts_url }
82
+ format.json { head :ok }
83
+ end
84
+ end
85
+ end
@@ -1,4 +1,3 @@
1
1
  class ApplicationController < ActionController::Base
2
2
  protect_from_forgery
3
- before_action :authenticate_user!
4
3
  end
@@ -0,0 +1,6 @@
1
+ class BaseController < ApplicationController
2
+
3
+ def home
4
+ end
5
+
6
+ end
@@ -0,0 +1,25 @@
1
+ class Admin < PARENT_MODEL_CLASS
2
+ if DEVISE_ORM == :mongoid
3
+ include Mongoid::Document
4
+
5
+ ## Database authenticatable
6
+ field :email, type: String, null: false, default: ""
7
+ field :encrypted_password, type: String, null: false, default: ""
8
+
9
+ ## Recoverable
10
+ field :reset_password_token, type: String
11
+ field :reset_password_sent_at, type: Time
12
+ end
13
+
14
+ devise :otp_authenticatable, :database_authenticatable, :registerable,
15
+ :trackable, :validatable
16
+
17
+ # Setup accessible (or protected) attributes for your model
18
+ # attr_accessible :otp_enabled, :otp_mandatory, :as => :otp_privileged
19
+ # attr_accessible :email, :password, :password_confirmation, :remember_me
20
+
21
+ def self.otp_mandatory
22
+ true
23
+ end
24
+
25
+ end
@@ -0,0 +1,25 @@
1
+ <%= form_for([:admin, @post]) do |f| %>
2
+ <% if @post.errors.any? %>
3
+ <div id="error_explanation">
4
+ <h2><%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:</h2>
5
+
6
+ <ul>
7
+ <% @post.errors.full_messages.each do |msg| %>
8
+ <li><%= msg %></li>
9
+ <% end %>
10
+ </ul>
11
+ </div>
12
+ <% end %>
13
+
14
+ <div class="field">
15
+ <%= f.label :title %><br />
16
+ <%= f.text_field :title %>
17
+ </div>
18
+ <div class="field">
19
+ <%= f.label :body %><br />
20
+ <%= f.text_area :body %>
21
+ </div>
22
+ <div class="actions">
23
+ <%= f.submit %>
24
+ </div>
25
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <h1>Editing post</h1>
2
+
3
+ <%= render 'form' %>
4
+
5
+ <%= link_to 'Show', @post %> |
6
+ <%= link_to 'Back', admin_posts_path %>
@@ -0,0 +1,25 @@
1
+ <h1>Listing posts</h1>
2
+
3
+ <table>
4
+ <tr>
5
+ <th>Title</th>
6
+ <th>Body</th>
7
+ <th></th>
8
+ <th></th>
9
+ <th></th>
10
+ </tr>
11
+
12
+ <% @posts.each do |post| %>
13
+ <tr>
14
+ <td><%= post.title %></td>
15
+ <td><%= post.body %></td>
16
+ <td><%= link_to 'Show', post %></td>
17
+ <td><%= link_to 'Edit', edit_admin_post_path(post) %></td>
18
+ <td><%= link_to 'Destroy', [:admin, post], confirm: 'Are you sure?', method: :delete %></td>
19
+ </tr>
20
+ <% end %>
21
+ </table>
22
+
23
+ <br />
24
+
25
+ <%= link_to 'New Post', new_admin_post_path %>
@@ -0,0 +1,5 @@
1
+ <h1>New post</h1>
2
+
3
+ <%= render 'form' %>
4
+
5
+ <%= link_to 'Back', admin_posts_path %>
@@ -0,0 +1,15 @@
1
+ <p id="notice"><%= notice %></p>
2
+
3
+ <p>
4
+ <b>Title:</b>
5
+ <%= @post.title %>
6
+ </p>
7
+
8
+ <p>
9
+ <b>Body:</b>
10
+ <%= @post.body %>
11
+ </p>
12
+
13
+
14
+ <%= link_to 'Edit', edit_admin_post_path(@post) %> |
15
+ <%= link_to 'Back', admin_posts_path %>
@@ -0,0 +1 @@
1
+ <h1>Hello world!</h1>
@@ -53,8 +53,6 @@ module Dummy
53
53
  # Enable escaping HTML in JSON.
54
54
  config.active_support.escape_html_entities_in_json = true
55
55
 
56
- config.active_record.legacy_connection_handling = false
57
-
58
56
  # Use SQL instead of Active Record's schema dumper when creating the database.
59
57
  # This is necessary if your schema can't be completely dumped by the schema dumper,
60
58
  # like if you have constraints or database-specific column types
@@ -1,6 +1,9 @@
1
1
  Dummy::Application.routes.draw do
2
+ devise_for :admins
2
3
  devise_for :users
3
4
 
4
5
  resources :posts
5
- root to: "posts#index"
6
+ resources :admin_posts
7
+
8
+ root to: "base#home"
6
9
  end
@@ -0,0 +1,9 @@
1
+ class CreateAdmins < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :admins do |t|
4
+ t.string :name
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,52 @@
1
+ class AddDeviseToAdmins < ActiveRecord::Migration[5.0]
2
+ def self.up
3
+ change_table(:admins) do |t|
4
+ ## Database authenticatable
5
+ t.string :email, null: false, default: ""
6
+ t.string :encrypted_password, null: false, default: ""
7
+
8
+ ## Recoverable
9
+ t.string :reset_password_token
10
+ t.datetime :reset_password_sent_at
11
+
12
+ ## Rememberable
13
+ t.datetime :remember_created_at
14
+
15
+ ## Trackable
16
+ t.integer :sign_in_count, default: 0
17
+ t.datetime :current_sign_in_at
18
+ t.datetime :last_sign_in_at
19
+ t.string :current_sign_in_ip
20
+ t.string :last_sign_in_ip
21
+
22
+ ## Confirmable
23
+ # t.string :confirmation_token
24
+ # t.datetime :confirmed_at
25
+ # t.datetime :confirmation_sent_at
26
+ # t.string :unconfirmed_email # Only if using reconfirmable
27
+
28
+ ## Lockable
29
+ t.integer :failed_attempts, default: 0 # Only if lock strategy is :failed_attempts
30
+ t.string :unlock_token # Only if unlock strategy is :email or :both
31
+ t.datetime :locked_at
32
+
33
+ ## Token authenticatable
34
+ t.string :authentication_token
35
+
36
+ # Uncomment below if timestamps were not included in your original model.
37
+ # t.timestamps
38
+ end
39
+
40
+ add_index :admins, :email, unique: true
41
+ add_index :admins, :reset_password_token, unique: true
42
+ # add_index :admins, :confirmation_token, :unique => true
43
+ add_index :admins, :unlock_token, unique: true
44
+ add_index :admins, :authentication_token, unique: true
45
+ end
46
+
47
+ def self.down
48
+ # By default, we don't want to make any assumption about how to roll back a migration when your
49
+ # model already existed. Please edit below which fields you would like to remove in this migration.
50
+ raise ActiveRecord::IrreversibleMigration
51
+ end
52
+ end