Class: Seams::Generators::AdminGenerator
- Inherits:
-
Rails::Generators::Base
- Object
- Rails::Generators::Base
- Seams::Generators::AdminGenerator
show all
- Includes:
- EjectAware, HostInjector
- Defined in:
- lib/generators/seams/admin/admin_generator.rb
Overview
Generates a canonical Admin engine on top of the generic engine
scaffold. Wave 11A — Phase 1 (foundation) + Phase 2 (dashboards).
Phase 3 wires Pundit policies + the audit-log auto-write.
Phase 1 (foundation) ships:
- lib/admin/engine.rb that registers events, mounts under the
host, and asserts Auth::Identity + Administrate are present
at boot.
- lib/admin.rb + lib/admin/configuration.rb exposing four
configuration knobs:
- authenticator (callable; default: staff? on current_identity)
- tenancy_scope (:platform | :tenant; default :platform)
- theme_css_path (nil; host-supplied admin restyle)
- before_admin_action (callable; hook for 2FA / IP allow-list)
- app/controllers/seams/admin/application_controller.rb
subclassing Administrate::ApplicationController, gating via
the authenticator concern.
- lib/admin/concerns/authenticator.rb
Phase 2 (dashboards) ships:
- app/dashboards/admin/<name>_dashboard.rb
The engine ships NO migrations. It is read-only over existing
tables; Phase 3 may revisit if the audit-log table needs an
extra column.
Run with: bin/rails generate seams:admin
rubocop:disable Metrics/ClassLength
Constant Summary
collapse
- ENGINE_NAME =
"admin"
- DASHBOARD_MODELS =
Phase 2 dashboard catalogue. Each entry carries:
- the snake-cased dashboard name (filename + dashboard class)
- the full model class name (used inside the controller via
`def resource_class`)
- the owning engine (informational; useful for documentation
and future Pundit policy splits in Phase 3)
The order here is the order routes + dashboards appear in
the generated files; Identity comes first so the engine root
(root to: "admin/identities#index") lands on something
meaningful.
[
["identity", "Auth::Identity", "auth"],
["account", "Accounts::Account", "accounts"],
["accounts_membership", "Accounts::Membership", "accounts"],
["team", "Teams::Team", "teams"],
["teams_membership", "Teams::Membership", "teams"],
["invitation", "Teams::Invitation", "teams"],
["notification", "Notifications::Notification", "notifications"],
["notification_preference", "Notifications::NotificationPreference", "notifications"],
["plan", "Billing::Plan", "billing"],
["subscription", "Billing::Subscription", "billing"],
["invoice", "Billing::Invoice", "billing"],
["lifetime_pass", "Billing::LifetimePass", "billing"]
].freeze
Constants included
from EjectAware
EjectAware::EJECT_HEADER_PREFIX
Instance Method Summary
collapse
Methods included from EjectAware
#ejected?, #template_unless_ejected
#host_inject_gem, #host_inject_include_in_application_controller, #host_inject_include_in_user, #host_inject_mount, #host_uninject_gem, #host_uninject_include, #host_uninject_mount, #routes_draw_anchor
Instance Method Details
#append_administrate_to_engine_gemfile ⇒ Object
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 243
def append_administrate_to_engine_gemfile
gemfile = engine_path("Gemfile")
return unless File.exist?(gemfile)
contents = File.read(gemfile)
return if contents.include?('gem "administrate"')
File.write(gemfile, contents.rstrip + "\n\n # Phase 2 dashboards subclass Administrate::BaseDashboard; the\n # gem must be available when the engine runs its own specs.\n gem \"administrate\", \"~> 1.0\"\n RB\nend\n")
|
#append_pundit_to_engine_gemfile ⇒ Object
Phase 3 — append pundit to the engine's standalone Gemfile so
cd engines/admin && bundle exec rspec can require it. The
host-side Gemfile already gets pundit via wire_into_host
(added in Phase 2 ahead of Phase 3 wiring).
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 268
def append_pundit_to_engine_gemfile
gemfile = engine_path("Gemfile")
return unless File.exist?(gemfile)
contents = File.read(gemfile)
return if contents.include?('gem "pundit"')
File.write(gemfile, contents.rstrip + "\n\n # Phase 3 ApplicationController includes Pundit::Authorization\n # and the per-model policies live under Admin::Platform::*\n # and Admin::Tenant::*. The gem must be available when the\n # engine runs its own specs.\n gem \"pundit\", \"~> 2.4\"\n RB\nend\n")
|
#create_application_controller ⇒ Object
134
135
136
137
138
139
140
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 134
def create_application_controller
template_unless_ejected "app/controllers/admin/application_controller.rb.tt",
engine_path("app/controllers/seams/admin/application_controller.rb")
end
|
#create_authenticator_concern ⇒ Object
147
148
149
150
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 147
def create_authenticator_concern
template_unless_ejected "lib/concerns/authenticator.rb.tt",
engine_path("lib/admin/concerns/authenticator.rb")
end
|
#create_base_engine ⇒ Object
90
91
92
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 90
def create_base_engine
EngineGenerator.start([ENGINE_NAME], destination_root: destination_root)
end
|
#create_configuration ⇒ Object
142
143
144
145
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 142
def create_configuration
template_unless_ejected "lib/configuration.rb.tt",
engine_path("lib/admin/configuration.rb")
end
|
#create_context ⇒ Object
Phase 3 — Seams::Admin::Context Struct (the value pundit_user
returns). Wraps the current Identity + Membership so policies
can read both signals (staff? on Identity, role/account_id on
Membership) without each policy reaching into the controller.
156
157
158
159
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 156
def create_context
template_unless_ejected "lib/context.rb.tt",
engine_path("lib/admin/context.rb")
end
|
#create_dashboards ⇒ Object
Phase 2: emit one dashboard + one controller per entry in
DASHBOARD_MODELS. template_unless_ejected so a host that
ejects an individual dashboard (e.g. to restyle the Identity
form) keeps their version on the next generator run.
193
194
195
196
197
198
199
200
201
202
203
204
205
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 193
def create_dashboards
DASHBOARD_MODELS.each do |basename, _model_class, _engine|
template_unless_ejected(
"app/dashboards/admin/#{basename}_dashboard.rb.tt",
engine_path("app/dashboards/admin/#{basename}_dashboard.rb")
)
template_unless_ejected(
"app/controllers/admin/#{basename.pluralize}_controller.rb.tt",
engine_path("app/controllers/admin/#{basename.pluralize}_controller.rb")
)
end
end
|
#create_dummy_app ⇒ Object
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 207
def create_dummy_app
Seams::Generators::DummyAppWriter.write!(
engine_path: File.join(destination_root, "engines", ENGINE_NAME),
engine_module: "Admin",
mount_at: "/admin",
schema: dummy_schema,
host_user: dummy_host_identity,
host_user_path: "app/models/auth/identity.rb"
)
write_auth_current_stub
write_accounts_current_stub
write_dummy_model_stubs
amend_dummy_application_rb
rewrite_dummy_routes_for_namespaced_engine
end
|
#create_factories ⇒ Object
286
287
288
289
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 286
def create_factories
template_unless_ejected "spec/factories/admin.rb.tt",
engine_path("spec/factories/admin.rb")
end
|
#create_policies ⇒ Object
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 171
def create_policies
POLICY_NAMESPACES.each do |namespace|
template_unless_ejected(
"app/policies/admin/#{namespace}/application_policy.rb.tt",
engine_path("app/policies/admin/#{namespace}/application_policy.rb")
)
end
DASHBOARD_MODELS.each do |basename, _model_class, _engine|
POLICY_NAMESPACES.each do |namespace|
template_unless_ejected(
"app/policies/admin/#{namespace}/#{basename}_policy.rb.tt",
engine_path("app/policies/admin/#{namespace}/#{basename}_policy.rb")
)
end
end
end
|
#create_runtime_specs ⇒ Object
297
298
299
300
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 297
def create_runtime_specs
template "spec/runtime/admin_boot_spec.rb.tt",
engine_path("spec/runtime/admin_boot_spec.rb")
end
|
#create_unit_specs ⇒ Object
291
292
293
294
295
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 291
def create_unit_specs
end
|
#overwrite_engine_entry_point ⇒ Object
94
95
96
97
98
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 94
def overwrite_engine_entry_point
template "lib/engine.rb.tt", engine_path("lib/admin/engine.rb"), force: true
template "lib/admin.rb.tt", engine_path("lib/admin.rb"), force: true
end
|
#overwrite_readme ⇒ Object
302
303
304
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 302
def overwrite_readme
template "README.md.tt", engine_path("README.md"), force: true
end
|
#overwrite_routes ⇒ Object
100
101
102
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 100
def overwrite_routes
template_unless_ejected "config/routes.rb.tt", engine_path("config/routes.rb"), force: true
end
|
#remove_single_namespace_leftovers ⇒ Object
The base EngineGenerator creates app/controllers/admin/ and
app/models/admin/ files because it assumes a single-segment
namespace (Admin::*). The admin engine uses the two-segment
Seams::Admin::* namespace for its own ApplicationController,
but the per-dashboard controllers + dashboards live under
the single-segment Admin::* namespace (Administrate's
convention). We delete only the leftover ApplicationController
/ ApplicationRecord files; Phase 2 templates re-populate
app/controllers/admin/.
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 113
def remove_single_namespace_leftovers
%w[
app/controllers/admin/application_controller.rb
app/models/admin/application_record.rb
spec/admin_spec.rb
].each do |relative|
full = engine_path(relative)
next unless File.exist?(full)
FileUtils.rm(full)
say " remove #{relative} (single-namespace leftover)", :red
end
full = engine_path("app/models/admin")
return unless File.directory?(full) && Dir.empty?(full)
Dir.rmdir(full)
end
|
#report_summary ⇒ Object
316
317
318
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 316
def report_summary
say report_summary_text, :green
end
|
#report_summary_text ⇒ Object
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 320
def report_summary_text
"\n Admin engine generated at engines/admin/\n\n Next steps:\n 1. bundle install\n (picks up administrate + pundit, both injected into the host Gemfile)\n\n 2. Promote yourself to a platform admin:\n bin/rails runner 'Auth::Identity.find_by(email: \"[email protected]\").update!(staff: true)'\n No migration is needed \u2014 admin is read-only over the\n existing seams tables, and `staff` already lives on\n auth_identities (Wave 9).\n\n 3. Boot the host:\n bin/rails server\n\n 4. Visit /admin in your browser. You should land on the\n Identities index with sidebar entries for all twelve\n canonical seams models.\n\n Tenancy modes:\n - :platform (default) \u2014 admins see every Account's data.\n Gate: Auth::Identity#staff?.\n - :tenant \u2014 admins see only their own Account's data.\n Gate: Accounts::Membership#role == \"admin\".\n Switch via config/initializers/seams_admin.rb:\n Seams::Admin.configure { |c| c.tenancy_scope = :tenant }\n\n Customise a dashboard:\n bin/seams resolve --eject admin/app/dashboards/admin/identity_dashboard.rb\n # Edit your local copy; future `bin/seams admin` runs leave it alone.\n\n Audit log:\n Every successful create/update/destroy writes a Core::AuditLog\n row keyed on Auth::Current.identity. No-ops cleanly if the\n core engine isn't installed.\n\n Run the engine specs:\n bin/rails seams:test[admin]\n\n See engines/admin/README.md for the full configuration\n reference and the four config knobs (authenticator,\n tenancy_scope, theme_css_path, before_admin_action).\n\n TXT\nend\n"
|
#rewrite_dummy_routes_for_namespaced_engine ⇒ Object
DummyAppWriter renders mount Admin::Engine, at: "/admin" from
the engine_module: "Admin" argument — but our engine lives at
Seams::Admin::Engine (two-namespace, matching the gem layout).
We can't pass engine_module: "Seams::Admin" because DummyAppWriter
also uses that value to require "<downcase>" the engine's lib
entry point, and require "seams::admin" isn't a thing. Cheapest
fix: rewrite the dummy's routes.rb after DummyAppWriter writes it.
235
236
237
238
239
240
241
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 235
def rewrite_dummy_routes_for_namespaced_engine
path = File.join(destination_root, "engines", ENGINE_NAME, "spec/dummy/config/routes.rb")
return unless File.exist?(path)
contents = File.read(path)
File.write(path, contents.sub("mount Admin::Engine", "mount Seams::Admin::Engine"))
end
|
#wire_into_host ⇒ Object
306
307
308
309
310
311
312
313
314
|
# File 'lib/generators/seams/admin/admin_generator.rb', line 306
def wire_into_host
host_inject_gem("administrate", "~> 1.0")
host_inject_gem("pundit", "~> 2.4")
host_inject_mount(engine_class: "Seams::Admin::Engine", at: "/admin")
end
|