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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +150 -164
- data/README.md +287 -272
- data/Rakefile +5 -4
- data/app/controllers/bible270/admin_controller.rb +119 -0
- data/app/controllers/bible270/application_controller.rb +19 -2
- data/app/controllers/bible270/checkoffs_controller.rb +2 -3
- data/app/controllers/bible270/comments_controller.rb +6 -2
- data/app/controllers/bible270/days_controller.rb +5 -6
- data/app/controllers/bible270/readers_controller.rb +5 -4
- data/app/controllers/bible270/sessions_controller.rb +60 -26
- data/app/helpers/bible270/plan_helper.rb +11 -8
- data/app/mailers/bible270/application_mailer.rb +4 -1
- data/app/mailers/bible270/sign_in_mailer.rb +1 -0
- data/app/models/bible270/application_record.rb +1 -0
- data/app/models/bible270/checkoff.rb +4 -3
- data/app/models/bible270/comment.rb +26 -5
- data/app/models/bible270/reader.rb +78 -14
- data/app/models/bible270/sign_in_token.rb +5 -2
- data/app/views/bible270/admin/comments.html.erb +44 -0
- data/app/views/bible270/admin/index.html.erb +30 -0
- data/app/views/bible270/admin/show.html.erb +89 -0
- data/app/views/bible270/days/index.html.erb +10 -13
- data/app/views/bible270/readers/index.html.erb +2 -2
- data/app/views/bible270/readers/show.html.erb +5 -5
- data/app/views/bible270/sessions/new.html.erb +9 -3
- data/app/views/bible270/shared/_header.html.erb +1 -0
- data/app/views/bible270/shared/_styles.html.erb +10 -7
- data/app/views/layouts/bible270/application.html.erb +4 -3
- data/config/routes.rb +32 -18
- data/db/migrate/20260101000001_create_bible270_readers.rb +3 -2
- data/db/migrate/20260101000002_create_bible270_checkoffs.rb +3 -2
- data/db/migrate/20260101000003_create_bible270_comments.rb +4 -3
- data/db/migrate/20260101000004_create_bible270_sign_in_tokens.rb +4 -3
- data/db/migrate/20260101000005_add_names_to_bible270_readers.rb +12 -0
- data/db/migrate/20260101000006_add_approval_to_bible270_comments.rb +11 -0
- data/lib/bible270/configuration.rb +91 -19
- data/lib/bible270/email_sign_in.rb +7 -7
- data/lib/bible270/engine.rb +1 -0
- data/lib/bible270/plan.rb +449 -150
- data/lib/bible270/versification.rb +84 -67
- data/lib/bible270/version.rb +2 -1
- data/lib/bible270.rb +6 -5
- data/lib/generators/bible270/install/install_generator.rb +520 -39
- data/lib/generators/bible270/install/templates/bible270.rb.tt +8 -2
- data/lib/generators/bible270/install/templates/omniauth.rb.tt +6 -4
- metadata +17 -29
|
@@ -1,65 +1,546 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
3
4
|
|
|
4
5
|
module Bible270
|
|
5
6
|
module Generators
|
|
6
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
54
|
-
options[:providers]
|
|
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
|
-
|
|
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
|
|
62
|
-
|
|
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 = "
|
|
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 = "
|
|
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
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
|
15
|
+
path_prefix Bible270.config.auth_path_prefix
|
|
14
16
|
|
|
15
17
|
<% provider_list.each do |p| -%>
|
|
16
18
|
provider :<%= p %>,
|