Class: Seams::Generators::AdminGenerator
- Inherits:
-
Rails::Generators::Base
- Object
- Rails::Generators::Base
- Seams::Generators::AdminGenerator
- 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 — the gate concern.
- config/routes.rb scoped to the engine, with insertion-point
markers + the twelve canonical resource declarations.
Phase 2 (dashboards) ships:
- app/dashboards/admin/<name>_dashboard.rb — twelve Administrate
dashboards covering Identity, Account, Membership (x2),
Team, TeamMembership, Invitation, Notification,
NotificationPreference, Plan, Subscription, Invoice,
LifetimePass. Each subclasses Administrate::BaseDashboard.
- app/controllers/admin/<plural>_controller.rb — twelve
thin Administrate controllers, each subclassing
Seams::Admin::ApplicationController (NOT Administrate's
directly) so they inherit the gate, the pundit_user hook,
and Phase 3's audit-log auto-write.
- Dummy app schema + slim model stubs for each of the twelve
so the engine's runtime spec can boot Administrate against
a real ActiveRecord schema.
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-next 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. [ # [dashboard_basename, model_class, owning_engine] ["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
- #append_administrate_to_engine_gemfile ⇒ Object
-
#append_pundit_to_engine_gemfile ⇒ Object
Phase 3 — append
punditto the engine's standalone Gemfile socd engines/admin && bundle exec rspeccan require it. - #create_application_controller ⇒ Object
- #create_authenticator_concern ⇒ Object
-
#create_base_engine ⇒ Object
The base engine would mount
Admin::Engineand write anAdmin.configureinitializer, but this engine's constants live underSeams::Admin. - #create_configuration ⇒ Object
-
#create_context ⇒ Object
Phase 3 — Seams::Admin::Context Struct (the value pundit_user returns).
-
#create_dashboards ⇒ Object
Phase 2: emit one dashboard + one controller per entry in DASHBOARD_MODELS.
- #create_dummy_app ⇒ Object
- #create_factories ⇒ Object
-
#create_fields ⇒ Object
Association fields that name their dashboard explicitly; see templates/app/fields/admin/fields/dashboard_option.rb.tt.
- #create_policies ⇒ Object
- #create_runtime_specs ⇒ Object
- #create_unit_specs ⇒ Object
-
#create_view_overrides ⇒ Object
Administrate partial overrides: navigation built from this engine's routes, and an index header whose New link does not depend on the dashboard class name.
- #overwrite_engine_entry_point ⇒ Object
- #overwrite_readme ⇒ Object
- #overwrite_routes ⇒ Object
-
#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::*). - #report_summary ⇒ Object
- #report_summary_text ⇒ Object
-
#rewrite_dummy_routes_for_namespaced_engine ⇒ Object
DummyAppWriter renders
mount Admin::Engine, at: "/admin"from theengine_module: "Admin"argument — but our engine lives atSeams::Admin::Engine(two-namespace, matching the gem layout). - #wire_into_host ⇒ Object
Methods included from EjectAware
#ejected?, #template_unless_ejected
Methods included from HostInjector
#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
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 270 def append_administrate_to_engine_gemfile # The engine's standalone Gemfile (engine_path("Gemfile")) # gets `administrate` appended so that running engine specs # in isolation (cd engines/admin && bundle exec rspec) can # require the gem. The host-side Gemfile is updated separately # by `wire_into_host`. gemfile = engine_path("Gemfile") return unless File.exist?(gemfile) contents = File.read(gemfile) return if contents.include?('gem "administrate"') File.write(gemfile, contents.rstrip + <<~RB) # Phase 2 dashboards subclass Administrate::BaseDashboard; the # gem must be available when the engine runs its own specs. gem "administrate", "~> 1.0" RB end |
#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).
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 295 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 + <<~RB) # Phase 3 ApplicationController includes Pundit::Authorization # and the per-model policies live under Admin::Platform::* # and Admin::Tenant::*. The gem must be available when the # engine runs its own specs. gem "pundit", "~> 2.4" RB end |
#create_application_controller ⇒ Object
137 138 139 140 141 142 143 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 137 def create_application_controller # Zeitwerk-friendly path: lives at app/controllers/seams/admin/ # so the constant `Seams::Admin::ApplicationController` resolves # without explicit requires. template_unless_ejected "app/controllers/admin/application_controller.rb.tt", engine_path("app/controllers/seams/admin/application_controller.rb") end |
#create_authenticator_concern ⇒ Object
150 151 152 153 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 150 def create_authenticator_concern template_unless_ejected "lib/concerns/authenticator.rb.tt", engine_path("lib/admin/concerns/authenticator.rb") end |
#create_base_engine ⇒ Object
The base engine would mount Admin::Engine and write an
Admin.configure initializer, but this engine's constants live
under Seams::Admin. wire_into_host mounts the real class.
93 94 95 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 93 def create_base_engine EngineGenerator.start([ENGINE_NAME, "--skip-host-wiring"], destination_root: destination_root) end |
#create_configuration ⇒ Object
145 146 147 148 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 145 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.
159 160 161 162 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 159 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.
219 220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 219 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
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 233 def create_dummy_app # Phase 2 dashboards target every canonical seams model, so # the dummy schema covers every table they read. Slim model # stubs ship alongside so Administrate's Zeitwerk-driven class # resolution finds an `Auth::Identity` / `Billing::Plan` / # etc. constant when the dashboard class loads. 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 write_dummy_ability_registrations amend_dummy_application_rb rewrite_dummy_routes_for_namespaced_engine end |
#create_factories ⇒ Object
313 314 315 316 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 313 def create_factories template_unless_ejected "spec/factories/admin.rb.tt", engine_path("spec/factories/admin.rb") end |
#create_fields ⇒ Object
Association fields that name their dashboard explicitly; see templates/app/fields/admin/fields/dashboard_option.rb.tt.
206 207 208 209 210 211 212 213 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 206 def create_fields %w[dashboard_option belongs_to has_many].each do |field| template_unless_ejected( "app/fields/admin/fields/#{field}.rb.tt", engine_path("app/fields/admin/fields/#{field}.rb") ) end end |
#create_policies ⇒ Object
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 174 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
324 325 326 327 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 324 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
318 319 320 321 322 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 318 def create_unit_specs # Per-dashboard specs are deferred to Phase 3 (where the # Pundit policies introduce real behaviour worth covering). # Phase 2's coverage lives in the runtime boot spec. end |
#create_view_overrides ⇒ Object
Administrate partial overrides: navigation built from this engine's routes, and an index header whose New link does not depend on the dashboard class name. See each partial's header.
195 196 197 198 199 200 201 202 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 195 def create_view_overrides %w[_navigation _index_header].each do |partial| template_unless_ejected( "app/views/seams/admin/application/#{partial}.html.erb.tt", engine_path("app/views/seams/admin/application/#{partial}.html.erb") ) end end |
#overwrite_engine_entry_point ⇒ Object
97 98 99 100 101 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 97 def overwrite_engine_entry_point # engine.rb / lib/admin.rb stay framework-managed. 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
329 330 331 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 329 def overwrite_readme template "README.md.tt", engine_path("README.md"), force: true end |
#overwrite_routes ⇒ Object
103 104 105 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 103 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/.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 116 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 # Best-effort: clean up the now-empty parent dir for app/models/admin. # (app/controllers/admin gets repopulated by Phase 2 dashboards.) full = engine_path("app/models/admin") return unless File.directory?(full) && Dir.empty?(full) Dir.rmdir(full) end |
#report_summary ⇒ Object
343 344 345 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 343 def report_summary say report_summary_text, :green end |
#report_summary_text ⇒ Object
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 347 def report_summary_text <<~TXT Admin engine generated at engines/admin/ Next steps: 1. bundle install (picks up administrate + pundit, both injected into the host Gemfile) 2. Promote yourself to a platform admin: bin/rails runner 'Auth::Identity.find_by(email: "[email protected]").update!(staff: true)' No migration is needed — admin is read-only over the existing seams tables, and `staff` already lives on auth_identities (Wave 9). 3. Boot the host: bin/rails server 4. Visit /admin in your browser. You should land on the Identities index with sidebar entries for all twelve canonical seams models. Tenancy modes: - :platform (default) — admins see every Account's data. Gate: Auth::Identity#staff?. - :tenant — admins see only their own Account's data. Gate: Accounts::Membership#role == "admin". Switch via config/initializers/seams_admin.rb: Seams::Admin.configure { |c| c.tenancy_scope = :tenant } Customise a dashboard: bin/seams resolve --eject admin/app/dashboards/admin/identity_dashboard.rb # Edit your local copy; future `bin/seams admin` runs leave it alone. Audit log: Every successful create/update/destroy writes a Core::AuditLog row keyed on Auth::Current.identity. No-ops cleanly if the core engine isn't installed. Run the engine specs: bin/rails seams:test[admin] See engines/admin/README.md for the full configuration reference and the four config knobs (authenticator, tenancy_scope, theme_css_path, before_admin_action). TXT end |
#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.
262 263 264 265 266 267 268 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 262 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
333 334 335 336 337 338 339 340 341 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 333 def wire_into_host # Administrate is the dashboard framework; Pundit is the # authorisation layer Phase 3 will wire policies through. # Both go into the host's main bundle (admin runs in-app, not # a separate process). host_inject_gem("administrate", "~> 1.0") host_inject_gem("pundit", "~> 2.4") host_inject_mount(engine_class: "Seams::Admin::Engine", at: "/admin") end |