b4um_generators 0.2.17 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.de.md +418 -4
  4. data/README.md +405 -5
  5. data/lib/b4um_generators/version.rb +1 -1
  6. data/lib/generators/b4um/admin/admin_generator.rb +614 -0
  7. data/lib/generators/b4um/admin/templates/add_admin_fields_migration.rb.tt +10 -0
  8. data/lib/generators/b4um/admin/templates/admin_base_controller.rb.tt +5 -0
  9. data/lib/generators/b4um/admin/templates/admin_dashboard.html.erb.tt +19 -0
  10. data/lib/generators/b4um/admin/templates/admin_dashboard_controller.rb.tt +6 -0
  11. data/lib/generators/b4um/admin/templates/admin_users_controller.rb.tt +70 -0
  12. data/lib/generators/b4um/admin/templates/admin_users_index.html.erb.tt +110 -0
  13. data/lib/generators/b4um/admin/templates/b4um_admin.rake.tt +106 -0
  14. data/lib/generators/b4um/authentication/authentication_generator.rb +90 -1
  15. data/lib/generators/b4um/guest/guest_generator.rb +673 -0
  16. data/lib/generators/b4um/guest/templates/add_guest_fields_migration.rb.tt +12 -0
  17. data/lib/generators/b4um/guest/templates/b4um_guests.rake.tt +12 -0
  18. data/lib/generators/b4um/guest/templates/edit.html.erb.tt +156 -0
  19. data/lib/generators/b4um/guest/templates/guest_expiration_warning.html.erb.tt +12 -0
  20. data/lib/generators/b4um/guest/templates/guests_controller.rb.tt +88 -0
  21. data/lib/generators/b4um/guest/templates/new.html.erb.tt +136 -0
  22. data/lib/generators/b4um/help/help_generator.rb +228 -0
  23. data/lib/generators/b4um/install/templates/b4um/sortable.css +158 -0
  24. data/lib/generators/b4um/install/templates/b4um.css +1 -0
  25. data/lib/generators/b4um/pagination/pagination_generator.rb +32 -5
  26. data/lib/generators/b4um/scaffold/scaffold_generator.rb +50 -0
  27. data/lib/generators/b4um/scaffold/templates/_form.html.erb.tt +18 -14
  28. data/lib/generators/b4um/search/search_generator.rb +22 -8
  29. data/lib/generators/b4um/sortable/sortable_generator.rb +529 -0
  30. data/lib/generators/b4um/sortable/templates/_sortable.html.erb.tt +40 -0
  31. data/lib/generators/b4um/sortable/templates/add_position_migration.rb.tt +26 -0
  32. data/lib/generators/b4um/sortable/templates/sort.html.erb.tt +3 -0
  33. data/lib/generators/b4um/sortable/templates/sortable_controller.js +166 -0
  34. metadata +22 -1
@@ -0,0 +1,614 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/migration"
5
+
6
+ module B4um
7
+ module Generators
8
+ class AdminGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ desc "Adds admin user management to an existing B4UM authentication model."
14
+
15
+ argument :authentication_model,
16
+ type: :string,
17
+ required: true,
18
+ banner: "MODEL"
19
+
20
+ def validate_authentication_model
21
+ model_path = File.join(
22
+ destination_root,
23
+ "app/models",
24
+ "#{authentication_model_name}.rb"
25
+ )
26
+
27
+ return if File.exist?(model_path)
28
+
29
+ raise Thor::Error,
30
+ "Authentication model #{authentication_class_name} was not found. " \
31
+ "Expected: app/models/#{authentication_model_name}.rb"
32
+ end
33
+
34
+ def validate_authentication_setup
35
+ application_controller_path = File.join(
36
+ destination_root,
37
+ "app/controllers/application_controller.rb"
38
+ )
39
+
40
+ authentication_helper =
41
+ "helper_method :#{current_authentication_method}, :logged_in?"
42
+
43
+ authentication_installed =
44
+ File.exist?(application_controller_path) &&
45
+ File.read(application_controller_path).include?(authentication_helper)
46
+
47
+ unless authentication_installed
48
+ raise Thor::Error,
49
+ "b4um authentication is not installed for " \
50
+ "#{authentication_class_name}. " \
51
+ "Run: bin/rails generate b4um:authentication " \
52
+ "#{authentication_class_name}"
53
+ end
54
+
55
+ application_controller =
56
+ File.read(application_controller_path)
57
+
58
+ current_user_assignment =
59
+ "@#{current_authentication_method} ||= " \
60
+ "#{authentication_class_name}.find_by(" \
61
+ "id: session[:#{authentication_model_name}_id])"
62
+
63
+ unless application_controller.include?(current_user_assignment)
64
+ raise Thor::Error,
65
+ "Could not find the current authentication method " \
66
+ "in app/controllers/application_controller.rb"
67
+ end
68
+
69
+ sessions_controller_path = File.join(
70
+ destination_root,
71
+ "app/controllers/sessions_controller.rb"
72
+ )
73
+
74
+ unless File.exist?(sessions_controller_path)
75
+ raise Thor::Error,
76
+ "SessionsController not found: " \
77
+ "app/controllers/sessions_controller.rb"
78
+ end
79
+
80
+ sessions_controller = File.read(sessions_controller_path)
81
+
82
+ session_assignment =
83
+ "session[:#{authentication_model_name}_id] = " \
84
+ "#{authentication_model_name}.id"
85
+
86
+ return if sessions_controller.include?(session_assignment)
87
+
88
+ raise Thor::Error,
89
+ "Could not find the authentication session assignment " \
90
+ "in app/controllers/sessions_controller.rb"
91
+ end
92
+
93
+ def protect_admin_fields
94
+ controller_path = File.join(
95
+ destination_root,
96
+ "app/controllers",
97
+ "#{authentication_model_name.pluralize}_controller.rb"
98
+ )
99
+
100
+ return unless File.exist?(controller_path)
101
+
102
+ controller = File.read(controller_path)
103
+
104
+ params_method = controller.match(
105
+ /def #{Regexp.escape(authentication_model_name)}_params\b.*?^\s*end$/m
106
+ )
107
+
108
+ return unless params_method
109
+
110
+ protected_params = params_method[0].dup
111
+
112
+ %w[admin blocked].each do |field|
113
+ protected_params.gsub!(
114
+ /:#{field}\s*,?/,
115
+ ""
116
+ )
117
+ end
118
+
119
+ controller.sub!(
120
+ params_method[0],
121
+ protected_params
122
+ )
123
+
124
+ File.write(
125
+ controller_path,
126
+ controller
127
+ )
128
+ end
129
+
130
+ def create_admin_fields_migration
131
+ return if admin_fields_exist?
132
+
133
+ migration_template(
134
+ "add_admin_fields_migration.rb.tt",
135
+ "db/migrate/add_admin_fields_to_#{authentication_model_name.pluralize}.rb"
136
+ )
137
+ end
138
+
139
+ def add_admin_helper
140
+ controller_path = "app/controllers/application_controller.rb"
141
+
142
+ full_controller_path = File.join(
143
+ destination_root,
144
+ controller_path
145
+ )
146
+
147
+ controller = File.read(full_controller_path)
148
+
149
+ return if controller.include?("def require_admin")
150
+
151
+ class_end_position = controller.rindex(/^end\s*$/)
152
+
153
+ unless class_end_position
154
+ raise Thor::Error,
155
+ "Could not find the end of ApplicationController."
156
+ end
157
+
158
+ require_admin = <<~RUBY
159
+
160
+ def require_admin
161
+ return if logged_in? && #{current_authentication_method}.admin?
162
+
163
+ redirect_to root_path, alert: "Access denied."
164
+ end
165
+
166
+ RUBY
167
+
168
+ controller.insert(
169
+ class_end_position,
170
+ require_admin
171
+ )
172
+
173
+ File.write(
174
+ full_controller_path,
175
+ controller
176
+ )
177
+ end
178
+
179
+ def create_admin_base_controller
180
+ template(
181
+ "admin_base_controller.rb.tt",
182
+ "app/controllers/admin/base_controller.rb"
183
+ )
184
+ end
185
+
186
+ def create_admin_dashboard_controller
187
+ template(
188
+ "admin_dashboard_controller.rb.tt",
189
+ "app/controllers/admin/dashboard_controller.rb"
190
+ )
191
+ end
192
+
193
+ def create_admin_users_controller
194
+ template(
195
+ "admin_users_controller.rb.tt",
196
+ "app/controllers/admin/users_controller.rb"
197
+ )
198
+ end
199
+
200
+ def create_admin_dashboard_view
201
+ template(
202
+ "admin_dashboard.html.erb.tt",
203
+ "app/views/admin/dashboard/index.html.erb"
204
+ )
205
+ end
206
+
207
+ def create_admin_users_index_view
208
+ template(
209
+ "admin_users_index.html.erb.tt",
210
+ "app/views/admin/users/index.html.erb"
211
+ )
212
+ end
213
+
214
+ def create_admin_creation_task
215
+ template(
216
+ "b4um_admin.rake.tt",
217
+ "lib/tasks/b4um_admin.rake"
218
+ )
219
+ end
220
+
221
+ def add_admin_route
222
+ routes_path = "config/routes.rb"
223
+ full_routes_path = File.join(destination_root, routes_path)
224
+
225
+ unless File.exist?(full_routes_path)
226
+ raise Thor::Error,
227
+ "Routes not found: #{routes_path}"
228
+ end
229
+
230
+ admin_users_route_status =
231
+ upgrade_existing_admin_users_route(
232
+ routes_path,
233
+ full_routes_path
234
+ )
235
+
236
+ routes = File.read(full_routes_path)
237
+
238
+ routes_to_add = missing_admin_routes(
239
+ routes,
240
+ admin_users_route_exists:
241
+ admin_users_route_status == :upgraded
242
+ )
243
+
244
+ return if routes_to_add.empty?
245
+
246
+ inject_into_file(
247
+ routes_path,
248
+ before: /^end\s*$/
249
+ ) do
250
+ "#{routes_to_add.join("\n")}\n"
251
+ end
252
+ end
253
+
254
+ def invalidate_blocked_sessions
255
+ controller_path = "app/controllers/application_controller.rb"
256
+
257
+ full_controller_path = File.join(
258
+ destination_root,
259
+ controller_path
260
+ )
261
+
262
+ controller = File.read(full_controller_path)
263
+
264
+ return if controller.include?("if @#{current_authentication_method}&.blocked?")
265
+
266
+ current_user_assignment =
267
+ "@#{current_authentication_method} ||= " \
268
+ "#{authentication_class_name}.find_by(" \
269
+ "id: session[:#{authentication_model_name}_id])"
270
+
271
+ unless controller.include?(current_user_assignment)
272
+ raise Thor::Error,
273
+ "Could not find the current authentication method " \
274
+ "in #{controller_path}"
275
+ end
276
+
277
+ replacement = <<~RUBY.chomp
278
+ #{current_user_assignment}
279
+
280
+ if @#{current_authentication_method}&.blocked?
281
+ session.delete(:#{authentication_model_name}_id)
282
+ @#{current_authentication_method} = nil
283
+ end
284
+
285
+ @#{current_authentication_method}
286
+ RUBY
287
+
288
+ gsub_file(
289
+ controller_path,
290
+ current_user_assignment,
291
+ replacement
292
+ )
293
+ end
294
+
295
+ def prevent_blocked_login
296
+ controller_path = "app/controllers/sessions_controller.rb"
297
+
298
+ full_controller_path = File.join(
299
+ destination_root,
300
+ controller_path
301
+ )
302
+
303
+ unless File.exist?(full_controller_path)
304
+ raise Thor::Error,
305
+ "SessionsController not found: #{controller_path}"
306
+ end
307
+
308
+ controller = File.read(full_controller_path)
309
+
310
+ return if controller.include?(
311
+ "if #{authentication_model_name}.blocked?"
312
+ )
313
+
314
+ session_assignment =
315
+ "session[:#{authentication_model_name}_id] = " \
316
+ "#{authentication_model_name}.id"
317
+
318
+ session_match = controller.match(
319
+ /^(\s*)#{Regexp.escape(session_assignment)}$/
320
+ )
321
+
322
+ unless session_match
323
+ raise Thor::Error,
324
+ "Could not find the authentication session assignment " \
325
+ "in #{controller_path}"
326
+ end
327
+
328
+ indent = session_match[1]
329
+ inner_indent = "#{indent} "
330
+
331
+ replacement = [
332
+ "#{indent}if #{authentication_model_name}.blocked?",
333
+ "#{inner_indent}flash.now[:alert] = \"Account is blocked.\"",
334
+ "#{inner_indent}render :new, status: :unprocessable_content",
335
+ "#{indent}else",
336
+ "#{inner_indent}#{session_assignment}"
337
+ ].join("\n")
338
+
339
+ gsub_file(
340
+ controller_path,
341
+ /^#{Regexp.escape(indent)}#{Regexp.escape(session_assignment)}$/,
342
+ replacement
343
+ )
344
+
345
+ login_redirect =
346
+ 'redirect_to root_path, notice: "Successfully logged in."'
347
+
348
+ controller = File.read(full_controller_path)
349
+
350
+ redirect_match = controller.match(
351
+ /^(\s*)#{Regexp.escape(login_redirect)}$/
352
+ )
353
+
354
+ unless redirect_match
355
+ raise Thor::Error,
356
+ "Could not find the successful login redirect " \
357
+ "in #{controller_path}"
358
+ end
359
+
360
+ redirect_indent = redirect_match[1]
361
+
362
+ gsub_file(
363
+ controller_path,
364
+ /^#{Regexp.escape(redirect_indent)}#{Regexp.escape(login_redirect)}$/,
365
+ [
366
+ "",
367
+ "#{inner_indent}#{login_redirect}",
368
+ "#{indent}end"
369
+ ].join("\n")
370
+ )
371
+ end
372
+
373
+ def self.next_migration_number(_dirname)
374
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
375
+ end
376
+
377
+ private
378
+
379
+ def upgrade_existing_admin_users_route(
380
+ _routes_path,
381
+ full_routes_path
382
+ )
383
+ routes = File.read(full_routes_path)
384
+
385
+ namespace_match = routes.match(
386
+ /^([ \t]*)namespace :admin do\s*$/
387
+ )
388
+
389
+ return :unchanged unless namespace_match
390
+
391
+ namespace_indent = namespace_match[1]
392
+ route_indent = "#{namespace_indent} "
393
+
394
+ namespace_start = namespace_match.end(0)
395
+
396
+ namespace_end = routes.match(
397
+ /^#{Regexp.escape(namespace_indent)}end\s*$/,
398
+ namespace_start
399
+ )
400
+
401
+ return :unchanged unless namespace_end
402
+
403
+ namespace_body = routes[
404
+ namespace_start...namespace_end.begin(0)
405
+ ]
406
+
407
+ old_route_match = namespace_body.match(
408
+ /^#{Regexp.escape(route_indent)}#{Regexp.escape(old_admin_users_route)}$/
409
+ )
410
+
411
+ return :unchanged unless old_route_match
412
+
413
+ replacement = admin_users_resources
414
+ .lines
415
+ .map { |line| "#{route_indent}#{line}" }
416
+ .join
417
+ .chomp
418
+
419
+ route_start =
420
+ namespace_start + old_route_match.begin(0)
421
+
422
+ route_end =
423
+ namespace_start + old_route_match.end(0)
424
+
425
+ routes[route_start...route_end] = replacement
426
+
427
+ File.write(
428
+ full_routes_path,
429
+ routes
430
+ )
431
+
432
+ :upgraded
433
+ end
434
+
435
+ def missing_admin_routes(
436
+ routes,
437
+ admin_users_route_exists: false
438
+ )
439
+ routes_to_add = []
440
+
441
+ routes_to_add << " #{admin_root_route}" unless routes.include?(admin_root_route)
442
+
443
+ unless admin_users_route_exists ||
444
+ routes.include?(admin_users_route)
445
+ routes_to_add << indented_admin_users_route
446
+ end
447
+
448
+ routes_to_add
449
+ end
450
+
451
+ def admin_root_route
452
+ 'get "admin", to: "admin/dashboard#index", as: :admin_root'
453
+ end
454
+
455
+ def admin_users_route
456
+ resources = admin_users_resources
457
+ .lines
458
+ .map { |line| " #{line}" }
459
+ .join
460
+ .chomp
461
+
462
+ [
463
+ "namespace :admin do",
464
+ resources,
465
+ "end"
466
+ ].join("\n")
467
+ end
468
+
469
+ def admin_users_resources
470
+ <<~RUBY.chomp
471
+ resources :users, only: [:index, :destroy] do
472
+ member do
473
+ patch :block
474
+ patch :unblock
475
+ patch :grant_admin
476
+ patch :revoke_admin
477
+ end
478
+ end
479
+ RUBY
480
+ end
481
+
482
+ def old_admin_users_route
483
+ "resources :users, only: [:index]"
484
+ end
485
+
486
+ def indented_admin_users_route
487
+ admin_users_route
488
+ .lines
489
+ .map { |line| " #{line}" }
490
+ .join
491
+ .chomp
492
+ end
493
+
494
+ def admin_creation_fields
495
+ schema_path = File.join(
496
+ destination_root,
497
+ "db/schema.rb"
498
+ )
499
+
500
+ return [] unless File.exist?(schema_path)
501
+
502
+ schema = File.read(schema_path)
503
+ table_name = authentication_model_name.pluralize
504
+
505
+ table_match = schema.match(
506
+ /create_table "#{Regexp.escape(table_name)}".*?do \|t\|(.*?)^\s*end/m
507
+ )
508
+
509
+ return [] unless table_match
510
+
511
+ excluded_fields = %w[
512
+ id
513
+ email
514
+ password_digest
515
+ admin
516
+ blocked
517
+ created_at
518
+ updated_at
519
+ ]
520
+
521
+ fields = table_match[1].scan(
522
+ /^\s*t\.(string|text)\s+"([^"]+)"/
523
+ )
524
+
525
+ fields.filter_map do |_type, field|
526
+ field unless excluded_fields.include?(field)
527
+ end
528
+ end
529
+
530
+ def admin_user_display_fields
531
+ schema_path = File.join(
532
+ destination_root,
533
+ "db/schema.rb"
534
+ )
535
+
536
+ return ["email"] unless File.exist?(schema_path)
537
+
538
+ schema = File.read(schema_path)
539
+ table_name = authentication_model_name.pluralize
540
+
541
+ table_match = schema.match(
542
+ /create_table "#{Regexp.escape(table_name)}".*?do \|t\|(.*?)^\s*end/m
543
+ )
544
+
545
+ return ["email"] unless table_match
546
+
547
+ excluded_fields = %w[
548
+ id
549
+ admin
550
+ blocked
551
+ created_at
552
+ updated_at
553
+ ]
554
+
555
+ sensitive_field_pattern = /(?:password|token|secret|digest)/i
556
+
557
+ fields = table_match[1].scan(
558
+ /^\s*t\.\w+\s+"([^"]+)"/
559
+ ).flatten
560
+
561
+ fields.reject! do |field|
562
+ excluded_fields.include?(field) ||
563
+ field.match?(sensitive_field_pattern)
564
+ end
565
+
566
+ fields.empty? ? ["email"] : fields
567
+ end
568
+
569
+ def admin_user_field_label(field)
570
+ field.humanize
571
+ end
572
+
573
+ def authentication_class_name
574
+ authentication_model.to_s.classify
575
+ end
576
+
577
+ def authentication_model_name
578
+ authentication_model.to_s.underscore
579
+ end
580
+
581
+ def current_authentication_method
582
+ "current_#{authentication_model_name}"
583
+ end
584
+
585
+ def admin_fields_exist?
586
+ admin_column_exists? && blocked_column_exists?
587
+ end
588
+
589
+ def admin_column_exists?
590
+ schema_table&.match?(/t\.boolean\s+"admin"/) || false
591
+ end
592
+
593
+ def blocked_column_exists?
594
+ schema_table&.match?(/t\.boolean\s+"blocked"/) || false
595
+ end
596
+
597
+ def schema_table
598
+ schema_path = File.join(destination_root, "db/schema.rb")
599
+
600
+ return unless File.exist?(schema_path)
601
+
602
+ schema = File.read(schema_path)
603
+
604
+ table_name = authentication_model_name.pluralize
605
+
606
+ table_pattern = /
607
+ create_table\s+"#{Regexp.escape(table_name)}".*?^ end
608
+ /mx
609
+
610
+ schema[table_pattern]
611
+ end
612
+ end
613
+ end
614
+ end
@@ -0,0 +1,10 @@
1
+ class AddAdminFieldsTo<%= authentication_class_name.pluralize %> < ActiveRecord::Migration[8.0]
2
+ def change
3
+ <% unless admin_column_exists? -%>
4
+ add_column :<%= authentication_model_name.pluralize %>, :admin, :boolean, default: false, null: false
5
+ <% end -%>
6
+ <% unless blocked_column_exists? -%>
7
+ add_column :<%= authentication_model_name.pluralize %>, :blocked, :boolean, default: false, null: false
8
+ <% end -%>
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Admin::BaseController < ApplicationController
4
+ before_action :require_admin
5
+ end
@@ -0,0 +1,19 @@
1
+ <%% content_for :title, "Admin" %>
2
+
3
+ <div class="page-header">
4
+ <h1>Admin</h1>
5
+ </div>
6
+
7
+ <div class="b4um-grid">
8
+ <section class="b4um-card b4um-card--full">
9
+ <h2>User management</h2>
10
+
11
+ <p>
12
+ Manage registered users and administrator permissions.
13
+ </p>
14
+
15
+ <%%= link_to "Manage users",
16
+ admin_users_path,
17
+ class: "button button--primary" %>
18
+ </section>
19
+ </div>
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Admin::DashboardController < Admin::BaseController
4
+ def index
5
+ end
6
+ end