bible270 0.6.3 → 0.10.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +150 -164
  3. data/README.md +287 -272
  4. data/Rakefile +5 -4
  5. data/app/controllers/bible270/admin_controller.rb +119 -0
  6. data/app/controllers/bible270/application_controller.rb +19 -2
  7. data/app/controllers/bible270/checkoffs_controller.rb +2 -3
  8. data/app/controllers/bible270/comments_controller.rb +6 -2
  9. data/app/controllers/bible270/days_controller.rb +5 -6
  10. data/app/controllers/bible270/readers_controller.rb +5 -4
  11. data/app/controllers/bible270/sessions_controller.rb +60 -26
  12. data/app/helpers/bible270/plan_helper.rb +11 -8
  13. data/app/mailers/bible270/application_mailer.rb +4 -1
  14. data/app/mailers/bible270/sign_in_mailer.rb +1 -0
  15. data/app/models/bible270/application_record.rb +1 -0
  16. data/app/models/bible270/checkoff.rb +4 -3
  17. data/app/models/bible270/comment.rb +26 -5
  18. data/app/models/bible270/reader.rb +78 -14
  19. data/app/models/bible270/sign_in_token.rb +5 -2
  20. data/app/views/bible270/admin/comments.html.erb +44 -0
  21. data/app/views/bible270/admin/index.html.erb +30 -0
  22. data/app/views/bible270/admin/show.html.erb +89 -0
  23. data/app/views/bible270/days/index.html.erb +10 -13
  24. data/app/views/bible270/readers/index.html.erb +2 -2
  25. data/app/views/bible270/readers/show.html.erb +5 -5
  26. data/app/views/bible270/sessions/new.html.erb +9 -3
  27. data/app/views/bible270/shared/_header.html.erb +1 -0
  28. data/app/views/bible270/shared/_styles.html.erb +10 -7
  29. data/app/views/layouts/bible270/application.html.erb +4 -3
  30. data/config/routes.rb +32 -18
  31. data/db/migrate/20260101000001_create_bible270_readers.rb +3 -2
  32. data/db/migrate/20260101000002_create_bible270_checkoffs.rb +3 -2
  33. data/db/migrate/20260101000003_create_bible270_comments.rb +4 -3
  34. data/db/migrate/20260101000004_create_bible270_sign_in_tokens.rb +4 -3
  35. data/db/migrate/20260101000005_add_names_to_bible270_readers.rb +12 -0
  36. data/db/migrate/20260101000006_add_approval_to_bible270_comments.rb +11 -0
  37. data/lib/bible270/configuration.rb +91 -19
  38. data/lib/bible270/email_sign_in.rb +7 -7
  39. data/lib/bible270/engine.rb +1 -0
  40. data/lib/bible270/plan.rb +449 -150
  41. data/lib/bible270/versification.rb +84 -67
  42. data/lib/bible270/version.rb +2 -1
  43. data/lib/bible270.rb +6 -5
  44. data/lib/generators/bible270/install/install_generator.rb +520 -39
  45. data/lib/generators/bible270/install/templates/bible270.rb.tt +8 -2
  46. data/lib/generators/bible270/install/templates/omniauth.rb.tt +6 -4
  47. metadata +17 -29
@@ -1,65 +1,546 @@
1
1
  # frozen_string_literal: true
2
- require "rails/generators"
2
+
3
+ require 'rails/generators'
3
4
 
4
5
  module Bible270
5
6
  module Generators
6
- # rails generate bible270:install [--mount-at=/reading-plan] [--providers=github]
7
+ # Interactive installer:
8
+ #
9
+ # bin/rails generate bible270:install
10
+ #
11
+ # Walks through the mount point, authentication, initializers, routing,
12
+ # migrations and (optionally) running them. Every prompt has a default, so
13
+ # holding Enter gives a working install. Pass --defaults to skip the prompts
14
+ # entirely, or set any answer up front with a flag:
15
+ #
16
+ # bin/rails generate bible270:install --defaults \
17
+ # --mount-at=/daily-bread --providers=github,google_oauth2 \
18
+ # --mailer-from=no-reply@example.org
7
19
  #
8
- # Writes the engine initializer and an OmniAuth initializer whose path_prefix
9
- # matches the mount point (the one detail that is easy to get wrong).
10
20
  class InstallGenerator < Rails::Generators::Base
11
- source_root File.expand_path("templates", __dir__)
21
+ MOUNT_LINE = 'mount Bible270::Engine, at: Bible270.config.mount_at'
22
+ DEFAULT_MOUNT = '/daily-bread'
23
+ DEFAULT_PROVIDERS = 'github'
24
+ KNOWN_PROVIDERS = {
25
+ 'github' => 'omniauth-github',
26
+ 'google_oauth2' => 'omniauth-google-oauth2',
27
+ 'facebook' => 'omniauth-facebook',
28
+ 'twitter' => 'omniauth-twitter',
29
+ 'linkedin' => 'omniauth-linkedin-oauth2',
30
+ 'gitlab' => 'omniauth-gitlab',
31
+ 'discord' => 'omniauth-discord',
32
+ 'apple' => 'omniauth-apple',
33
+ 'saml' => 'omniauth-saml',
34
+ 'openid_connect' => 'omniauth_openid_connect'
35
+ }.freeze
36
+
37
+ class_option :mount_at, type: :string, desc: "Path to mount the engine at (default #{DEFAULT_MOUNT})"
38
+ class_option :providers, type: :string, desc: 'Comma-separated OmniAuth providers, or "" for none'
39
+ class_option :email, type: :boolean, desc: 'Enable passwordless email sign-in'
40
+ class_option :mailer_from, type: :string, desc: 'From: address for sign-in emails'
41
+ class_option :migrate, type: :boolean, desc: 'Run db:migrate at the end'
42
+ class_option :bundle, type: :boolean, desc: 'Run bundle install after adding strategy gems'
43
+ class_option :defaults, type: :boolean, default: false, desc: 'Accept all defaults, ask nothing'
44
+
45
+ # ---- 1. greeting ----------------------------------------------------
46
+
47
+ def greet
48
+ say ''
49
+ say 'Installing bible270', :green
50
+ say 'A 270-day Bible reading plan, mounted into this application.'
51
+ say 'Press Enter to accept the [default] at any prompt.' unless quiet?
52
+ say ''
53
+ end
54
+
55
+ # ---- 2. where to mount ----------------------------------------------
56
+
57
+ def ask_mount_point
58
+ @mount_at = normalize_mount(options[:mount_at] || prompt('Mount the plan at which path?', DEFAULT_MOUNT))
59
+
60
+ until valid_mount?(@mount_at)
61
+ say " '#{@mount_at}' isn't a usable path.", :red
62
+ @mount_at = normalize_mount(prompt('Mount the plan at which path?', DEFAULT_MOUNT))
63
+ end
64
+
65
+ say " Plan will live at #{@mount_at}", :green
66
+ say ''
67
+ end
68
+
69
+ # ---- 3. authentication ----------------------------------------------
70
+
71
+ def ask_authentication
72
+ say 'Authentication', :yellow
73
+ say 'Readers must sign in to tick off readings and comment. Browsing is always public.'
74
+
75
+ @email = decide(:email, 'Enable email sign-in (a one-time link, no password)?', true)
76
+ @mailer_from = ask_mailer_from if @email
77
+
78
+ @providers = decide_providers
79
+
80
+ unless @email || @providers.any?
81
+ say ''
82
+ say ' No sign-in method chosen — readers would be unable to take part.', :red
83
+ @email = confirm?('Enable email sign-in after all?', true)
84
+ end
85
+
86
+ # Covers the case where email was turned on by the question above.
87
+ @mailer_from = ask_mailer_from if @email && @mailer_from.nil?
88
+
89
+ report_auth_choice
90
+ say ''
91
+ end
92
+
93
+ # ---- 4. strategy gems -------------------------------------------------
94
+
95
+ def add_strategy_gems
96
+ return if @providers.empty?
97
+
98
+ missing = @providers.filter_map { |p| KNOWN_PROVIDERS[p] }.reject { |g| gemfile_has?(g) }
99
+ unknown = @providers.reject { |p| KNOWN_PROVIDERS.key?(p) }
100
+
101
+ missing.each { |name| gem name }
102
+
103
+ return if unknown.empty?
104
+
105
+ say " Unrecognised provider(s): #{unknown.join(', ')}. Add their strategy gems yourself.", :yellow
106
+ end
107
+
108
+ def install_bundle
109
+ return if @providers.empty?
110
+
111
+ @bundled = decide(:bundle, 'Run bundle install now (needed before the app can boot)?', true)
112
+ run 'bundle install' if @bundled
113
+ end
114
+
115
+ # ---- 5. migrations ----------------------------------------------------
12
116
 
13
- class_option :mount_at, type: :string, default: "/reading-plan",
14
- desc: "Path the engine is mounted at"
15
- class_option :providers, type: :string, default: "github",
16
- desc: "Comma-separated OmniAuth providers (e.g. github,google_oauth2)"
117
+ # Migrations run before the initializers are written, so a problem in the
118
+ # generated config can never block the database work.
119
+ def copy_migrations
120
+ return if skip_rails_commands?
17
121
 
18
- def create_initializers
19
- template "bible270.rb.tt", "config/initializers/bible270.rb"
20
- template "omniauth.rb.tt", "config/initializers/omniauth.rb"
122
+ say ''
123
+ say 'Copying migrations into db/migrate', :yellow
124
+ rails_command 'bible270:install:migrations'
21
125
  end
22
126
 
127
+ def run_migrations
128
+ if skip_rails_commands?
129
+ @migrated = false
130
+ return
131
+ end
132
+
133
+ @migrated = decide(:migrate, 'Run the database migrations now?', true)
134
+
135
+ if @migrated
136
+ rails_command 'db:migrate'
137
+ else
138
+ say ' Skipped. Run bin/rails db:migrate when ready.', :yellow
139
+ end
140
+ end
141
+
142
+ # ---- 6. initializers ---------------------------------------------------
143
+
144
+ def create_engine_initializer
145
+ create_file 'config/initializers/bible270.rb', engine_initializer
146
+ end
147
+
148
+ def create_omniauth_initializer
149
+ return if @providers.empty?
150
+
151
+ create_file 'config/initializers/omniauth.rb', omniauth_initializer
152
+ end
153
+
154
+ # ---- 7. routing -------------------------------------------------------
155
+
23
156
  def add_route
24
- route %(mount Bible270::Engine, at: "#{mount_at}")
157
+ if routes_mounted?
158
+ say " config/routes.rb already mounts Bible270::Engine — left as is.#{cms_order_hint}", :yellow
159
+ return
160
+ end
161
+
162
+ route MOUNT_LINE
163
+
164
+ # Thor's route action injects after the `Rails.application.routes.draw do`
165
+ # sentinel and quietly does nothing if it can't find it (routes split
166
+ # across files, an unusual layout, a missing file). Verify rather than
167
+ # assume, since a silent miss leaves the plan unreachable.
168
+ if routes_mounted?
169
+ say " Mounted in config/routes.rb.#{cms_order_hint}", :green
170
+ else
171
+ @route_failed = true
172
+ say ' Could not edit config/routes.rb automatically.', :red
173
+ say ' Add this line yourself, inside Rails.application.routes.draw:', :red
174
+ say " #{MOUNT_LINE}"
175
+ end
176
+ end
177
+
178
+ # ---- 8. what is left to do -------------------------------------------
179
+
180
+ def summary
181
+ say ''
182
+ say 'bible270 installed.', :green
183
+ say ''
184
+ say " Plan mounted at #{@mount_at}"
185
+ say " Sign-in #{auth_summary}"
186
+ say " Initializers #{initializer_summary}"
187
+ say " Migrations #{@migrated ? 'copied and run' : 'copied (not yet run)'}"
188
+ say ''
189
+
190
+ remaining = outstanding_steps
191
+ if remaining.any?
192
+ say 'Still to do:', :yellow
193
+ remaining.each_with_index { |step, i| say " #{i + 1}. #{step}" }
194
+ say ''
195
+ end
196
+
197
+ say "Start the server and visit #{@mount_at}", :green
198
+ say ''
199
+ end
200
+
201
+ private
202
+
203
+ # Adding a gem to the Gemfile without bundling makes every subsequent
204
+ # bin/rails call fail ("Could not find ... in locally installed gems"), so
205
+ # don't even try.
206
+ def skip_rails_commands?
207
+ return false unless @providers.any? && @bundled == false
208
+ return true if @warned_unbundled
209
+
210
+ @warned_unbundled = true
211
+ say ''
212
+ say ' Gemfile changed but not bundled, so bin/rails cannot run yet.', :yellow
213
+ say ' Skipping the migration steps — run these once you have bundled:', :yellow
214
+ say ' bundle install'
215
+ say ' bin/rails bible270:install:migrations'
216
+ say ' bin/rails db:migrate'
217
+ true
218
+ end
219
+
220
+ # ---- prompting --------------------------------------------------------
221
+
222
+ def quiet? = options[:defaults]
223
+
224
+ # Ask unless --defaults was passed or the answer came in as a flag.
225
+ def prompt(question, default)
226
+ return default if quiet?
227
+
228
+ answer = ask(" #{question} [#{default}]")
229
+ answer.to_s.strip.empty? ? default : answer.strip
25
230
  end
26
231
 
27
- def show_next_steps
28
- say ""
29
- say "bible270 installed.", :green
30
- say ""
31
- say "Next steps:"
32
- say " 1. Add the strategy gems to your Gemfile, e.g.:"
33
- provider_list.each { |p| say " gem \"omniauth-#{p.tr('_', '-')}\"" }
34
- say " plus: gem \"omniauth\" and gem \"omniauth-rails_csrf_protection\""
35
- say " 2. bundle install"
36
- say " 3. bin/rails bible270:install:migrations && bin/rails db:migrate"
37
- say " 4. Set the provider credentials in your environment / credentials store."
38
- say " 5. Set config.mailer_from and make sure Action Mailer can deliver"
39
- say " (email sign-in is what lets readers without a social account join)."
40
- say " 6. Register the callback URL with each provider:"
41
- provider_list.each { |p| say " https://YOUR-HOST#{mount_at}/auth/#{p}/callback" }
42
- say ""
232
+ # Deliberately not built on Thor's yes?, which treats a bare Enter as "no"
233
+ # and is delegated to the shell (so wrapping ask here would never see it).
234
+ # Reading the answer directly lets Enter mean "take the default", as the
235
+ # [Y/n] hint implies.
236
+ def confirm?(question, default)
237
+ return default if quiet?
238
+
239
+ suffix = default ? 'Y/n' : 'y/N'
240
+ loop do
241
+ answer = ask(" #{question} [#{suffix}]").to_s.strip.downcase
242
+ return default if answer.empty?
243
+ return true if %w[y yes].include?(answer)
244
+ return false if %w[n no].include?(answer)
245
+
246
+ say ' Please answer y or n.', :red
247
+ end
43
248
  end
44
249
 
45
- private
250
+ # A boolean that may come from a flag, a prompt, or a default.
251
+ def decide(key, question, default)
252
+ return options[key] unless options[key].nil?
46
253
 
47
- def mount_at
48
- path = options[:mount_at].to_s
49
- path = "/#{path}" unless path.start_with?("/")
50
- path.chomp("/")
254
+ confirm?(question, default)
51
255
  end
52
256
 
53
- def provider_list
54
- options[:providers].to_s.split(",").map { |p| p.strip }.reject(&:empty?)
257
+ def decide_providers
258
+ if options[:providers]
259
+ return parse_providers(options[:providers])
260
+ end
261
+ return parse_providers(DEFAULT_PROVIDERS) if quiet?
262
+
263
+ return [] unless confirm?('Also offer social sign-in through OmniAuth?', true)
264
+
265
+ list = prompt("Which providers? (comma separated, from: #{KNOWN_PROVIDERS.keys.join(', ')})",
266
+ DEFAULT_PROVIDERS)
267
+ parse_providers(list)
268
+ end
269
+
270
+ def ask_mailer_from
271
+ default = "no-reply@#{app_domain}"
272
+ address = prompt('From: address for sign-in emails?', default)
273
+
274
+ until valid_email?(address)
275
+ say " '#{address}' doesn't look like an email address.", :red
276
+ address = prompt('From: address for sign-in emails?', default)
277
+ end
278
+ address
279
+ end
280
+
281
+ # Use the engine's own validator when it's loaded, else a simple check.
282
+ def valid_email?(value)
283
+ if defined?(Bible270::EmailSignIn)
284
+ Bible270::EmailSignIn.valid_email?(value)
285
+ else
286
+ value.to_s.match?(%r{\A[^@\s]+@[^@\s]+\.[A-Za-z]{2,}\z})
287
+ end
288
+ end
289
+
290
+ # ---- values -----------------------------------------------------------
291
+
292
+ def normalize_mount(value)
293
+ path = value.to_s.strip
294
+ path = "/#{path}" unless path.start_with?('/')
295
+ path = path.chomp('/')
296
+ path.empty? ? '/' : path
297
+ end
298
+
299
+ def valid_mount?(path)
300
+ path == '/' || path.match?(%r{\A(/[A-Za-z0-9\-_.~]+)+\z})
301
+ end
302
+
303
+ def parse_providers(value)
304
+ value.to_s.split(',').map { |p| p.strip.downcase.tr('-', '_') }.reject(&:empty?).uniq
305
+ end
306
+
307
+ def app_domain
308
+ name = Rails.application.class.module_parent_name.to_s
309
+ name.empty? ? 'example.com' : "#{name.underscore.dasherize}.example.com"
310
+ rescue StandardError
311
+ 'example.com'
55
312
  end
56
313
 
57
314
  def providers_literal
58
- provider_list.map { |p| ":#{p}" }.join(", ")
315
+ @providers.map { |p| ":#{p}" }.join(', ')
316
+ end
317
+
318
+ def auth_path_prefix
319
+ @mount_at == '/' ? '/auth' : "#{@mount_at}/auth"
320
+ end
321
+
322
+ def gemfile_has?(name)
323
+ path = File.join(destination_root, 'Gemfile')
324
+ File.exist?(path) && File.read(path).match?(%r{^\s*gem\s+['"]#{Regexp.escape(name)}['"]})
325
+ end
326
+
327
+ # A catch-all route (ComfortableMediaSurfer's `comfy_route :cms, path: '/'`
328
+ # is the common one) will swallow requests for anything declared below it,
329
+ # so the mount has to come first.
330
+ def cms_order_hint
331
+ path = File.join(destination_root, 'config', 'routes.rb')
332
+ return '' unless File.exist?(path)
333
+
334
+ body = File.read(path)
335
+ return '' unless body.match?(%r{comfy_route|match\s+['"]\*|get\s+['"]\*})
336
+
337
+ ' Check the mount line sits ABOVE any catch-all route.'
338
+ end
339
+
340
+ def routes_mounted?
341
+ path = File.join(destination_root, 'config', 'routes.rb')
342
+ File.exist?(path) && File.read(path).include?('Bible270::Engine')
343
+ end
344
+
345
+ def report_auth_choice
346
+ say " Email sign-in #{@email ? 'on' : 'off'}", @email ? :green : nil
347
+ say " OmniAuth #{@providers.any? ? @providers.join(', ') : 'off'}",
348
+ @providers.any? ? :green : nil
349
+ end
350
+
351
+ def auth_summary
352
+ parts = []
353
+ parts << 'email link' if @email
354
+ parts << "OmniAuth (#{@providers.join(', ')})" if @providers.any?
355
+ parts.empty? ? 'none configured' : parts.join(' + ')
356
+ end
357
+
358
+ # What actually landed on disk. A pre-existing file the conflict prompt was
359
+ # told not to overwrite would otherwise be reported as freshly written.
360
+ def initializer_summary
361
+ wrote = []
362
+ wrote << 'bible270.rb' if initializer_present?('bible270.rb')
363
+ wrote << 'omniauth.rb' if @providers.any? && initializer_present?('omniauth.rb')
364
+ missing = []
365
+ missing << 'bible270.rb' unless initializer_present?('bible270.rb')
366
+ missing << 'omniauth.rb' if @providers.any? && !initializer_present?('omniauth.rb')
367
+
368
+ summary = wrote.empty? ? 'none written' : wrote.join(' + ')
369
+ summary += " (MISSING: #{missing.join(', ')})" if missing.any?
370
+ summary
371
+ end
372
+
373
+ def initializer_present?(name)
374
+ File.exist?(File.join(destination_root, 'config', 'initializers', name))
375
+ end
376
+
377
+ # An omniauth.rb from an earlier hand setup may pin a literal path_prefix
378
+ # that no longer matches the mount point.
379
+ def stale_omniauth_prefix?
380
+ return false unless @providers.any? && initializer_present?('omniauth.rb')
381
+
382
+ body = File.read(File.join(destination_root, 'config', 'initializers', 'omniauth.rb'))
383
+ body.include?('path_prefix') && !body.include?('Bible270.config.auth_path_prefix')
384
+ end
385
+
386
+ def outstanding_steps
387
+ steps = []
388
+ steps << "Add to config/routes.rb: #{MOUNT_LINE}" if @route_failed
389
+ if @providers.any? && !initializer_present?('omniauth.rb')
390
+ steps << 'Create config/initializers/omniauth.rb (see the README) — social sign-in will 404 without it'
391
+ end
392
+ if stale_omniauth_prefix?
393
+ steps << "config/initializers/omniauth.rb sets its own path_prefix — make it 'Bible270.config.auth_path_prefix'"
394
+ end
395
+ steps << 'bundle install' if @providers.any? && !@bundled
396
+ steps << 'bin/rails bible270:install:migrations' if skip_rails_commands?
397
+ steps << 'bin/rails db:migrate' unless @migrated
398
+
399
+ if @providers.any?
400
+ steps << 'Set each provider\'s client id/secret in credentials or ENV'
401
+ @providers.each { |p| steps << "Register callback URL: https://YOUR-HOST#{auth_path_prefix}/#{p}/callback" }
402
+ end
403
+
404
+ if @email
405
+ steps << 'Confirm Action Mailer can deliver mail in this environment'
406
+ end
407
+ steps
408
+ end
409
+
410
+ # ---- file bodies ------------------------------------------------------
411
+
412
+ def engine_initializer
413
+ [initializer_head, indent(email_config_block), "\n",
414
+ indent(omniauth_config_block), "\n", initializer_tail].join
415
+ end
416
+
417
+ def initializer_head
418
+ <<~RUBY
419
+ # frozen_string_literal: true
420
+
421
+ # bible270 configuration.
422
+ # Reference: https://github.com/avonderluft/bible270
423
+ Bible270.configure do |config|
424
+ # --- Where the plan lives -------------------------------------------------
425
+ # Change this one value to move the plan. config/routes.rb and
426
+ # config/initializers/omniauth.rb both read it, so nothing else needs editing
427
+ # (but do update the callback URLs registered with each OAuth provider).
428
+ config.mount_at = '#{@mount_at}'
429
+
430
+ config.app_name = 'Daily Bread'
431
+ config.tagline = 'A 270-day journey through Scripture'
432
+
433
+ # Share this application's layout, helpers and CSRF setup.
434
+ config.parent_controller = '::ApplicationController'
435
+ # config.layout = 'layouts/application'
436
+
437
+ # --- Authentication -------------------------------------------------------
438
+ # Viewing the plan is always public; ticking off and commenting need a reader.
439
+ config.require_sign_in_to_participate = true
440
+
441
+ RUBY
442
+ end
443
+
444
+ def initializer_tail
445
+ <<~RUBY
446
+ # --- Start date -----------------------------------------------------------
447
+ # A community-wide start date, or leave nil for an undated plan.
448
+ # config.start_date = Date.new(#{Date.today.year}, 1, 1)
449
+ config.allow_reader_start_date = true
450
+
451
+ # --- Reading links --------------------------------------------------------
452
+ config.bible_version = 'NKJV'
453
+ end
454
+ RUBY
455
+ end
456
+
457
+ def email_config_block
458
+ if @email
459
+ <<~RUBY
460
+ # Passwordless email sign-in: a one-time link, no password, no third-party
461
+ # account. This is what lets anyone take part. Needs Action Mailer delivery.
462
+ config.email_sign_in = true
463
+ config.mailer_from = '#{@mailer_from}'
464
+ config.email_sign_in_ask_name = true
465
+ # config.email_sign_in_ttl = 20 * 60 # link lifetime, seconds
466
+ # config.email_sign_in_max_per_window = 5 # per address, per window
467
+ # config.email_sign_in_window = 15 * 60
468
+ # config.email_sign_in_deliver_later = true # needs an Active Job backend
469
+ RUBY
470
+ else
471
+ <<~RUBY
472
+ # Email sign-in is off. Turn it on to let readers without a social account
473
+ # take part (it needs Action Mailer delivery and a from: address).
474
+ config.email_sign_in = false
475
+ # config.mailer_from = 'no-reply@example.com'
476
+ RUBY
477
+ end
478
+ end
479
+
480
+ def omniauth_config_block
481
+ if @providers.any?
482
+ <<~RUBY
483
+ # Social sign-in. Strategy gems and credentials live in this application;
484
+ # see config/initializers/omniauth.rb.
485
+ config.omniauth_providers = [#{providers_literal}]
486
+ RUBY
487
+ else
488
+ <<~RUBY
489
+ # No social sign-in configured. Add providers here and register them in an
490
+ # OmniAuth initializer to offer it.
491
+ config.omniauth_providers = []
492
+ RUBY
493
+ end
494
+ end
495
+
496
+ def omniauth_initializer
497
+ <<~RUBY
498
+ # frozen_string_literal: true
499
+
500
+ # OmniAuth middleware for bible270's social sign-in.
501
+ #
502
+ # path_prefix must match where the engine is mounted so OmniAuth's callback
503
+ # (<prefix>/:provider/callback) lands on the engine's route. Rather than repeat
504
+ # the path we read it from Bible270.config, which is set in
505
+ # config/initializers/bible270.rb — Rails loads that first, because initializers
506
+ # run in alphabetical order and 'bible270' precedes 'omniauth'.
507
+ #
508
+ # OmniAuth 2.0+ only allows POST to the request phase (CVE-2015-9284);
509
+ # omniauth-rails_csrf_protection supplies the Rails-aware token check, and
510
+ # bible270's sign-in controls are already POST forms.
511
+ Rails.application.config.middleware.use OmniAuth::Builder do
512
+ # Scope every provider below to the engine's mount point. `options` is
513
+ # merged into each provider declared after it, so it has to come first.
514
+ #
515
+ # NB: OmniAuth::Builder has no `path_prefix` method — calling one raises
516
+ # NoMethodError. `OmniAuth.config.path_prefix = ...` would also work, but
517
+ # it is global and collides with Devise or any other OmniAuth user in
518
+ # this app. A strategy reads `options[:path_prefix]` first, so this is
519
+ # both correct and scoped.
520
+ options path_prefix: Bible270.config.auth_path_prefix
521
+
522
+ #{@providers.map { |p| indent(provider_line(p)) }.join("\n")}
523
+ end
524
+
525
+ # Send OmniAuth failures to the engine rather than raising.
526
+ OmniAuth.config.on_failure = proc do |env|
527
+ Bible270::SessionsController.action(:failure).call(env)
528
+ end
529
+ RUBY
530
+ end
531
+
532
+ # Indent every non-blank line, for slotting a block into a template.
533
+ def indent(text, spaces = 2)
534
+ text.gsub(%r{^(?!$)}, ' ' * spaces)
59
535
  end
60
536
 
61
- def omniauth_path_prefix
62
- "#{mount_at}/auth"
537
+ def provider_line(provider)
538
+ env = provider.upcase
539
+ <<~RUBY.chomp
540
+ provider :#{provider},
541
+ Rails.application.credentials.dig(:#{provider}, :client_id) || ENV.fetch('#{env}_CLIENT_ID', nil),
542
+ Rails.application.credentials.dig(:#{provider}, :client_secret) || ENV.fetch('#{env}_CLIENT_SECRET', nil)
543
+ RUBY
63
544
  end
64
545
  end
65
546
  end
@@ -2,8 +2,14 @@
2
2
  #
3
3
  # bible270 configuration. See the gem README for the full reference.
4
4
  Bible270.configure do |config|
5
+ # --- Where the plan lives --------------------------------------------------
6
+ # Change this one value to move the plan; config/routes.rb and
7
+ # config/initializers/omniauth.rb both read it, so nothing else needs editing.
8
+ # (Remember to update the callback URLs registered with each OAuth provider.)
9
+ config.mount_at = "<%= mount_at %>"
10
+
5
11
  config.app_name = "Daily Bread"
6
- config.tagline = "A 270-day journey through Scripture"
12
+ config.tagline = "Journeying through Scripture in 9 months"
7
13
 
8
14
  # Share the host application's layout, helpers and CSRF setup.
9
15
  config.parent_controller = "::ApplicationController"
@@ -37,5 +43,5 @@ Bible270.configure do |config|
37
43
  config.allow_reader_start_date = true
38
44
 
39
45
  # --- Reading links ---------------------------------------------------------
40
- config.bible_version = "ESV"
46
+ config.bible_version = "NKJV"
41
47
  end
@@ -2,15 +2,17 @@
2
2
  #
3
3
  # OmniAuth middleware for bible270's built-in sign-in.
4
4
  #
5
- # IMPORTANT: path_prefix must match where the engine is mounted, so that
6
- # OmniAuth's callback (<prefix>/:provider/callback) lands on the engine's
7
- # route. bible270 derives its sign-in button paths from the same value.
5
+ # path_prefix must match where the engine is mounted, so that OmniAuth's
6
+ # callback (<prefix>/:provider/callback) lands on the engine's route. Rather
7
+ # than repeat the path, we read it from Bible270.config.mount_at — set once in
8
+ # config/initializers/bible270.rb, which Rails loads before this file
9
+ # (initializers run in alphabetical order, and "bible270" precedes "omniauth").
8
10
  #
9
11
  # OmniAuth 2.0+ only allows POST to the request phase (CVE-2015-9284);
10
12
  # omniauth-rails_csrf_protection supplies the Rails-aware token check, and
11
13
  # bible270's sign-in controls are already POST forms.
12
14
  Rails.application.config.middleware.use OmniAuth::Builder do
13
- path_prefix "<%= omniauth_path_prefix %>"
15
+ path_prefix Bible270.config.auth_path_prefix
14
16
 
15
17
  <% provider_list.each do |p| -%>
16
18
  provider :<%= p %>,