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 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 ‘pundit` to the engine’s standalone Gemfile so ‘cd engines/admin && bundle exec rspec` can require it.
- #create_application_controller ⇒ Object
- #create_authenticator_concern ⇒ Object
- #create_base_engine ⇒ Object
- #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_policies ⇒ Object
- #create_runtime_specs ⇒ Object
- #create_unit_specs ⇒ Object
- #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 the `engine_module: “Admin”` argument — but our engine lives at `Seams::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
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 # 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).
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 + <<~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
134 135 136 137 138 139 140 |
# File 'lib/generators/seams/admin/admin_generator.rb', line 134 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
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 # 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 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 # 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 |
#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 # 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
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 # 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
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 <<~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: "you@example.com").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.
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 # 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 |