Class: Passkeyed::Generators::InstallGenerator

Inherits:
Rails::Generators::Base
  • Object
show all
Includes:
ActiveRecord::Generators::Migration
Defined in:
lib/generators/passkeyed/install/install_generator.rb

Overview

Wires passkeyed into a host Rails app:

bin/rails generate passkeyed:install

Adds the credentials table (and a webauthn_id column on the owner), an initializer, the Stimulus controller, and the model concern.

Instance Method Summary collapse

Instance Method Details

#copy_stimulus_controllerObject



35
36
37
38
39
40
# File 'lib/generators/passkeyed/install/install_generator.rb', line 35

def copy_stimulus_controller
  copy_file(
    "passkey_controller.js",
    "app/javascript/controllers/passkey_controller.js"
  )
end

#create_initializerObject



31
32
33
# File 'lib/generators/passkeyed/install/install_generator.rb', line 31

def create_initializer
  template "initializer.rb", "config/initializers/passkeyed.rb"
end

#create_migration_fileObject



24
25
26
27
28
29
# File 'lib/generators/passkeyed/install/install_generator.rb', line 24

def create_migration_file
  migration_template(
    "create_passkeyed_credentials.rb.erb",
    "db/migrate/create_passkeyed_credentials.rb"
  )
end

#inject_model_concernObject



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/generators/passkeyed/install/install_generator.rb', line 42

def inject_model_concern
  model_path = "app/models/#{owner_model.underscore}.rb"
  full_path = File.join(destination_root, model_path)

  unless File.exist?(full_path)
    say_status :skip, "#{model_path} not found; add `include Passkeyed::Model` to your owner model", :yellow
    return
  end

  source = File.read(full_path)
  class_name = owner_model.demodulize

  # Idempotent: re-running the generator must not inject a second include.
  # Anchor on a real include line (not a bare substring, which a comment or
  # the just-injected snippet would satisfy).
  if source.match?(/^\s*include\s+Passkeyed::Model\b/)
    say_status :identical, "Passkeyed::Model already included in #{model_path}", :blue
    return
  end

  # A single-line body (`class User; end`) can't take an injected line
  # safely, so bail to a manual instruction rather than placing the include
  # outside the class.
  if source.match?(/^\s*class\s+#{Regexp.escape(class_name)}\b[^\n]*;\s*end/)
    say_status :skip, "#{model_path} defines #{class_name} on one line; add `include Passkeyed::Model` manually",
               :yellow
    return
  end

  # inject_into_class places the include after the class declaration with
  # correct indentation, and handles namespaced/indented definitions.
  inject_into_class model_path, class_name, "  include Passkeyed::Model\n"

  # inject_into_class is a silent no-op when the class can't be matched, so
  # confirm the include actually landed rather than reporting false success.
  return if File.read(full_path).match?(/^\s*include\s+Passkeyed::Model\b/)

  say_status :skip, "could not edit #{model_path}; add `include Passkeyed::Model` manually", :yellow
end


82
83
84
85
86
# File 'lib/generators/passkeyed/install/install_generator.rb', line 82

def print_next_steps
  say "\npasskeyed installed. Next:", :green
  say "  1. bin/rails db:migrate"
  say "  2. add routes + a controller using Passkeyed::Ceremonies (see the README)"
end