Class: Sessions::Generators::InstallGenerator

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

Overview

‘rails generate sessions:install` — detects the app’s auth stack and writes the right pieces:

Rails 8 omakase auth → ONE migration extending the existing
  `sessions` table (the Devise-extends-`users` precedent) + the
  events table. The generated Session model stays untouched.

Devise → the Rails-8-shaped `sessions` table (plus our columns,
  with token_digest populated by the Warden adapter) + the events
  table + a 3-line app-owned Session shell model. The app converges
  on the omakase shape: a future Devise→Rails-auth migration finds
  its table already waiting.

Neither → aborts with guidance. The gem decorates a session of
  record; it never creates one.

Plus, in every mode: the annotated initializer, the SessionsSweepJob (host-scheduled — the trackdown/nondisposable pattern), and the post-install steps.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.next_migration_number(dir) ⇒ Object



38
39
40
# File 'lib/generators/sessions/install_generator.rb', line 38

def self.next_migration_number(dir)
  ActiveRecord::Generators::Base.next_migration_number(dir)
end

Instance Method Details

#check_devise_auth_model_fit!Object

Default (non-polymorphic) mode assumes a ‘User` class — the same assumption `rails generate authentication` makes: `belongs_to :user` and a foreign key to `users`. A Devise app whose auth model is Member/Account would pass detection and then break at db:migrate (FK to a missing `users` table) or at runtime (`belongs_to :user` constantizing a class that doesn’t exist) — catch it HERE, with the fix in the error message. ‘–polymorphic` works with any model(s).

Raises:

  • (Thor::Error)


88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/generators/sessions/install_generator.rb', line 88

def check_devise_auth_model_fit!
  return if polymorphic?
  return unless devise_detected?
  # An adopted omakase table already proves whatever owner shape the
  # host made work — nothing for us to second-guess.
  return if adopt_existing_table?

  classes = devise_auth_class_names
  return if classes.empty?         # mappings unreadable — stay permissive
  return if classes == ["User"]    # the default assumption holds

  if classes.include?("User")
    # User plus other scopes: default mode tracks User and SILENTLY
    # skips the rest (the runtime adapter's row_accepts? guard) —
    # surface that tradeoff at install time, but proceed.
    say "⚠️  Multiple Devise models detected (#{classes.join(", ")}).", :yellow
    say "   The default install tracks only User — sessions for the other models", :yellow
    say "   stay untracked. Re-run with --polymorphic to track them all.", :yellow
    return
  end

  raise Thor::Error, <<~MSG
    ❌ Your Devise model is #{classes.join(", ")} — not User. The default install
    assumes a `User` class (`belongs_to :user`, foreign key to `users`, the
    same assumption `rails generate authentication` makes) and would break
    at migrate/runtime.

    Re-run with the polymorphic owner, which works with any model(s):

      rails generate sessions:install --polymorphic

    …then declare `has_sessions` on #{classes.join(" and ")}.
  MSG
end

#check_for_conflicting_sessions_table!Object

Raises:

  • (Thor::Error)


65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/generators/sessions/install_generator.rb', line 65

def check_for_conflicting_sessions_table!
  return unless conflicting_sessions_table?

  raise Thor::Error, <<~MSG
    ❌ A `#{table_name}` table exists but doesn't look like the Rails 8 auth
    shape (no user reference + ip_address + user_agent columns) — most
    likely a legacy table (activerecord-session_store?).

    Two ways forward:

      1. Re-run with a different model: rails g sessions:install --model=SessionRecord
         (and set `config.session_class = "SessionRecord"` in the initializer)
      2. Migrate/rename the legacy table first, then re-run.
  MSG
end

#create_initializerObject



144
145
146
# File 'lib/generators/sessions/install_generator.rb', line 144

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

#create_migration_filesObject



123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/generators/sessions/install_generator.rb', line 123

def create_migration_files
  if adopt_existing_table?
    migration_template "add_sessions_columns.rb.erb",
                       File.join(db_migrate_path, "add_sessions_columns_to_#{table_name}.rb")
  else
    migration_template "create_sessions.rb.erb",
                       File.join(db_migrate_path, "create_#{table_name}.rb")
  end

  migration_template "create_sessions_events.rb.erb",
                     File.join(db_migrate_path, "create_sessions_events.rb")
end

#create_session_modelObject

Devise mode only: the app-owned 3-line shell. All gem logic lives in the Sessions::Model concern, so this file never goes stale.



138
139
140
141
142
# File 'lib/generators/sessions/install_generator.rb', line 138

def create_session_model
  return if adopt_existing_table? || session_model_file?

  template "session.rb.erb", "app/models/#{model_name.underscore}.rb"
end

#create_sweep_jobObject



148
149
150
# File 'lib/generators/sessions/install_generator.rb', line 148

def create_sweep_job
  template "sessions_sweep_job.rb", "app/jobs/sessions_sweep_job.rb"
end

#detect_auth_stack!Object

Raises:

  • (Thor::Error)


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/generators/sessions/install_generator.rb', line 42

def detect_auth_stack!
  # Detection is MEMOIZED here, before anything is generated:
  # create_session_model writes app/models/session.rb a few steps
  # below, which would otherwise flip omakase_detected? mid-run and
  # make the post-install message claim the wrong stack.
  adopt_existing_table?
  detected_stack

  return if omakase_detected? || devise_detected?

  raise Thor::Error, <<~MSG
    ❌ sessions couldn't detect an authentication system to decorate.

    The gem tracks the session of record your app already has — it never
    creates one. Set one up first:

      • Rails 8+ omakase auth:  bin/rails generate authentication
      • or Devise:              https://github.com/heartcombo/devise

    …then run `rails generate sessions:install` again.
  MSG
end

#display_post_install_messageObject



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/generators/sessions/install_generator.rb', line 152

def display_post_install_message
  say "\n🔐 The `sessions` gem has been installed#{" (#{detected_stack} detected)" if detected_stack}.",
      :green
  say "\nTo complete the setup:"

  migrate_verb = adopt_existing_table? ? "enrich your sessions table" : "create the sessions tables"
  say "  1. Run 'rails db:migrate' to #{migrate_verb}."
  say "     ⚠️  You must run migrations before starting your app!", :yellow

  say "  2. Add the macro to your auth model:"
  say "       class User < ApplicationRecord"
  say "         has_sessions"
  say "       end"

  say "  3. Mount the \"Your devices\" page wherever you want it to live:"
  say "       # config/routes.rb"
  if devise_detected? && !omakase_detected?
    say "       authenticate :user do"
    say "         mount Sessions::Engine => \"/settings/sessions\""
    say "       end"
  else
    say "       mount Sessions::Engine => \"/settings/sessions\""
  end

  say "  4. Schedule the sweep (retention purge + cap + opt-in expiry):"
  say "       # config/recurring.yml (Solid Queue)"
  say "       production:"
  say "         sessions_sweep:"
  say "           class: SessionsSweepJob"
  say "           schedule: every day at 4am"

  say "\nEvery login now lands on the devices page and in the trail:"
  say "  current_user.sessions.active     # live devices, revocable"
  say "  current_user.session_history     # the trail — logins, failures, revocations"
  say "\nEvery session, every device, every login — tracked. 🔐✨\n", :green
end