b4um_generators 0.1.1 → 0.2.7
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/.rubocop.yml +2 -2
- data/README.de.md +177 -5
- data/README.md +175 -5
- data/lib/b4um_generators/version.rb +1 -1
- data/lib/generators/b4um/attachment/attachment_generator.rb +514 -0
- data/lib/generators/b4um/authentication/authentication_generator.rb +414 -0
- data/lib/generators/b4um/authentication/templates/new.html.erb.tt +35 -0
- data/lib/generators/b4um/authentication/templates/sessions_controller.rb.tt +25 -0
- data/lib/generators/b4um/help/help_generator.rb +210 -5
- data/lib/generators/b4um/infinite_scroll/infinite_scroll_generator.rb +31 -0
- data/lib/generators/b4um/infinite_scroll/templates/infinite_scroll_controller.js +2 -2
- data/lib/generators/b4um/install/install_generator.rb +141 -2
- data/lib/generators/b4um/install/templates/b4um/forms.css +161 -1
- data/lib/generators/b4um/install/templates/b4um/navigation.css +13 -0
- data/lib/generators/b4um/install/templates/b4um/resources.css +18 -0
- data/lib/generators/b4um/install/templates/b4um.css +14 -0
- data/lib/generators/b4um/install/templates/navigation_helper.rb +13 -0
- data/lib/generators/b4um/readable/readable_generator.rb +77 -0
- data/lib/generators/b4um/search/search_generator.rb +58 -0
- metadata +7 -2
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module B4um
|
|
6
|
+
module Generators
|
|
7
|
+
class AuthenticationGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Installs session-based authentication for B4UM."
|
|
11
|
+
|
|
12
|
+
argument :authentication_model,
|
|
13
|
+
type: :string,
|
|
14
|
+
required: true,
|
|
15
|
+
banner: "MODEL"
|
|
16
|
+
|
|
17
|
+
class_option :protect,
|
|
18
|
+
type: :string,
|
|
19
|
+
required: false,
|
|
20
|
+
desc: "Comma-separated controllers to protect"
|
|
21
|
+
|
|
22
|
+
def validate_authentication_model
|
|
23
|
+
model_path = File.join(
|
|
24
|
+
destination_root,
|
|
25
|
+
"app/models",
|
|
26
|
+
"#{authentication_model_name}.rb"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
unless File.exist?(model_path)
|
|
30
|
+
raise Thor::Error,
|
|
31
|
+
"Authentication model #{authentication_class_name} was not found. " \
|
|
32
|
+
"Expected: app/models/#{authentication_model_name}.rb"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
model = File.read(model_path)
|
|
36
|
+
|
|
37
|
+
unless model.match?(/\bhas_secure_password\b/)
|
|
38
|
+
raise Thor::Error,
|
|
39
|
+
"Authentication model #{authentication_class_name} must use " \
|
|
40
|
+
"has_secure_password."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
return if password_digest_column_defined?
|
|
44
|
+
|
|
45
|
+
raise Thor::Error,
|
|
46
|
+
"Authentication model #{authentication_class_name} requires a " \
|
|
47
|
+
"password_digest column. " \
|
|
48
|
+
"Create the model with: " \
|
|
49
|
+
"bin/rails generate b4um:scaffold " \
|
|
50
|
+
"#{authentication_class_name} email:string password_digest:string"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_protected_controllers
|
|
54
|
+
protected_controller_names.each do |controller_name|
|
|
55
|
+
controller_path = File.join(
|
|
56
|
+
"app/controllers",
|
|
57
|
+
"#{controller_name.underscore}_controller.rb"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
full_controller_path = File.join(
|
|
61
|
+
destination_root,
|
|
62
|
+
controller_path
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
next if File.exist?(full_controller_path)
|
|
66
|
+
|
|
67
|
+
raise Thor::Error,
|
|
68
|
+
"Protected controller #{controller_name.camelize}Controller " \
|
|
69
|
+
"was not found: #{controller_path}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_bcrypt
|
|
74
|
+
gemfile_path = File.join(
|
|
75
|
+
destination_root,
|
|
76
|
+
"Gemfile"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
unless File.exist?(gemfile_path)
|
|
80
|
+
raise Thor::Error,
|
|
81
|
+
"Gemfile not found. bcrypt is required for authentication."
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
gemfile = File.read(gemfile_path)
|
|
85
|
+
|
|
86
|
+
return if gemfile.match?(
|
|
87
|
+
/^\s*gem\s+["']bcrypt["'](?:\s*,.*)?$/
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
raise Thor::Error,
|
|
91
|
+
"bcrypt is required for authentication. " \
|
|
92
|
+
"Run bin/rails generate b4um:install and enable bcrypt, " \
|
|
93
|
+
"or add gem \"bcrypt\" to your Gemfile and run bundle install."
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def create_sessions_controller
|
|
97
|
+
template(
|
|
98
|
+
"sessions_controller.rb.tt",
|
|
99
|
+
"app/controllers/sessions_controller.rb"
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def create_login_view
|
|
104
|
+
template(
|
|
105
|
+
"new.html.erb.tt",
|
|
106
|
+
"app/views/sessions/new.html.erb"
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def add_authentication_helpers
|
|
111
|
+
controller_path = "app/controllers/application_controller.rb"
|
|
112
|
+
full_controller_path = File.join(destination_root, controller_path)
|
|
113
|
+
|
|
114
|
+
unless File.exist?(full_controller_path)
|
|
115
|
+
raise Thor::Error,
|
|
116
|
+
"ApplicationController not found: #{controller_path}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
controller = File.read(full_controller_path)
|
|
120
|
+
|
|
121
|
+
return if controller.include?(
|
|
122
|
+
"helper_method :#{current_authentication_method}, :logged_in?"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
inject_into_file(
|
|
126
|
+
controller_path,
|
|
127
|
+
before: /^end\s*$/
|
|
128
|
+
) do
|
|
129
|
+
<<~RUBY
|
|
130
|
+
|
|
131
|
+
helper_method :#{current_authentication_method}, :logged_in?
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def #{current_authentication_method}
|
|
136
|
+
@#{current_authentication_method} ||= #{authentication_class_name}.find_by(id: session[:#{authentication_model_name}_id])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def logged_in?
|
|
140
|
+
#{current_authentication_method}.present?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def require_login
|
|
144
|
+
return if logged_in?
|
|
145
|
+
|
|
146
|
+
redirect_to login_path, alert: "Please log in first."
|
|
147
|
+
end
|
|
148
|
+
RUBY
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def add_session_routes
|
|
153
|
+
routes_path = "config/routes.rb"
|
|
154
|
+
full_routes_path = File.join(destination_root, routes_path)
|
|
155
|
+
|
|
156
|
+
routes = File.read(full_routes_path)
|
|
157
|
+
|
|
158
|
+
return if routes.include?(
|
|
159
|
+
'get "login", to: "sessions#new", as: :login'
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
inject_into_file(
|
|
163
|
+
routes_path,
|
|
164
|
+
before: /^end\s*$/
|
|
165
|
+
) do
|
|
166
|
+
<<~RUBY
|
|
167
|
+
get "login", to: "sessions#new", as: :login
|
|
168
|
+
post "login", to: "sessions#create"
|
|
169
|
+
delete "logout", to: "sessions#destroy", as: :logout
|
|
170
|
+
RUBY
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def add_navigation_links
|
|
175
|
+
navigation_path = "app/views/shared/_navigation.html.erb"
|
|
176
|
+
full_navigation_path = File.join(
|
|
177
|
+
destination_root,
|
|
178
|
+
navigation_path
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return unless File.exist?(full_navigation_path)
|
|
182
|
+
|
|
183
|
+
navigation = File.read(full_navigation_path)
|
|
184
|
+
|
|
185
|
+
return if navigation.include?(
|
|
186
|
+
'navigation_button_to "Logout", logout_path, method: :delete'
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
marker = "<%# B4UM_NAVIGATION_LINKS %>"
|
|
190
|
+
|
|
191
|
+
return unless navigation.include?(marker)
|
|
192
|
+
|
|
193
|
+
authentication_links = <<~ERB
|
|
194
|
+
<% if logged_in? %>
|
|
195
|
+
<%= navigation_button_to "Logout", logout_path, method: :delete %>
|
|
196
|
+
<% else %>
|
|
197
|
+
<%= navigation_link_to "Login",
|
|
198
|
+
login_path,
|
|
199
|
+
controller: :sessions,
|
|
200
|
+
action: :new %>
|
|
201
|
+
<% end %>
|
|
202
|
+
|
|
203
|
+
<%# B4UM_NAVIGATION_LINKS %>
|
|
204
|
+
ERB
|
|
205
|
+
|
|
206
|
+
gsub_file(
|
|
207
|
+
navigation_path,
|
|
208
|
+
marker,
|
|
209
|
+
authentication_links.chomp
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def protect_controllers
|
|
214
|
+
protected_controller_names.each do |controller_name|
|
|
215
|
+
controller_path = File.join(
|
|
216
|
+
"app/controllers",
|
|
217
|
+
"#{controller_name.underscore}_controller.rb"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
full_controller_path = File.join(
|
|
221
|
+
destination_root,
|
|
222
|
+
controller_path
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
controller = File.read(full_controller_path)
|
|
226
|
+
|
|
227
|
+
next if controller.include?(
|
|
228
|
+
"before_action :require_login, except: [:index, :show]"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
inject_into_file(
|
|
232
|
+
controller_path,
|
|
233
|
+
after: /^class .*Controller < ApplicationController\s*$/
|
|
234
|
+
) do
|
|
235
|
+
"\n before_action :require_login, except: [:index, :show]"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def protect_show_actions
|
|
241
|
+
protected_controller_names.each do |controller_name|
|
|
242
|
+
protect_show_actions_for(controller_name)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def protect_index_actions
|
|
247
|
+
protected_controller_names.each do |controller_name|
|
|
248
|
+
protect_index_actions_for(controller_name)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
def protect_index_actions_for(controller_name)
|
|
255
|
+
index_path = File.join(
|
|
256
|
+
"app/views",
|
|
257
|
+
controller_name.underscore,
|
|
258
|
+
"index.html.erb"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
full_index_path = File.join(
|
|
262
|
+
destination_root,
|
|
263
|
+
index_path
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
return unless File.exist?(full_index_path)
|
|
267
|
+
|
|
268
|
+
content = File.read(full_index_path)
|
|
269
|
+
|
|
270
|
+
return if content.include?("<% if logged_in? %>")
|
|
271
|
+
|
|
272
|
+
pattern = /
|
|
273
|
+
^[ \t]*<%=\s*link_to\s+"New\s+[^"]+",.*?%>
|
|
274
|
+
/mx
|
|
275
|
+
|
|
276
|
+
return unless content.match?(pattern)
|
|
277
|
+
|
|
278
|
+
wrap_protected_action(index_path, pattern)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def protect_show_actions_for(controller_name)
|
|
282
|
+
show_path = File.join(
|
|
283
|
+
"app/views",
|
|
284
|
+
controller_name.underscore,
|
|
285
|
+
"show.html.erb"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
full_show_path = File.join(destination_root, show_path)
|
|
289
|
+
|
|
290
|
+
return unless File.exist?(full_show_path)
|
|
291
|
+
|
|
292
|
+
content = File.read(full_show_path)
|
|
293
|
+
|
|
294
|
+
return if content.include?("<% if logged_in? %>")
|
|
295
|
+
|
|
296
|
+
protect_show_edit_action(show_path, content)
|
|
297
|
+
protect_show_destroy_action(show_path, content)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def protect_show_edit_action(show_path, content)
|
|
301
|
+
pattern = /
|
|
302
|
+
^[ \t]*<%=\s*link_to\s+"Edit",.*?%>
|
|
303
|
+
/mx
|
|
304
|
+
|
|
305
|
+
return unless content.match?(pattern)
|
|
306
|
+
|
|
307
|
+
wrap_protected_action(show_path, pattern)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def protect_show_destroy_action(show_path, content)
|
|
311
|
+
pattern = /
|
|
312
|
+
^[ \t]*<%=\s*button_to\s+"Destroy",.*?%>
|
|
313
|
+
/mx
|
|
314
|
+
|
|
315
|
+
return unless content.match?(pattern)
|
|
316
|
+
|
|
317
|
+
wrap_protected_action(show_path, pattern)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def wrap_protected_action(view_path, pattern)
|
|
321
|
+
gsub_file(view_path, pattern) do |match|
|
|
322
|
+
lines = match.lines
|
|
323
|
+
indentation = lines.first[/\A\s*/]
|
|
324
|
+
|
|
325
|
+
indented_match = lines.map do |line|
|
|
326
|
+
"#{indentation} #{line.delete_prefix(indentation)}"
|
|
327
|
+
end.join
|
|
328
|
+
|
|
329
|
+
[
|
|
330
|
+
"#{indentation}<% if logged_in? %>",
|
|
331
|
+
indented_match.chomp,
|
|
332
|
+
"#{indentation}<% end %>"
|
|
333
|
+
].join("\n")
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def protected_controller_names
|
|
338
|
+
options[:protect]
|
|
339
|
+
.to_s
|
|
340
|
+
.split(",")
|
|
341
|
+
.map(&:strip)
|
|
342
|
+
.reject(&:empty?)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def authentication_class_name
|
|
346
|
+
authentication_model.to_s.classify
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def authentication_model_name
|
|
350
|
+
authentication_model.to_s.underscore
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def current_authentication_method
|
|
354
|
+
"current_#{authentication_model_name}"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def password_digest_column_defined?
|
|
358
|
+
password_digest_defined_in_schema? ||
|
|
359
|
+
password_digest_defined_in_migrations?
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def password_digest_defined_in_schema?
|
|
363
|
+
schema_path = File.join(
|
|
364
|
+
destination_root,
|
|
365
|
+
"db/schema.rb"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
return false unless File.exist?(schema_path)
|
|
369
|
+
|
|
370
|
+
schema = File.read(schema_path)
|
|
371
|
+
table_name = authentication_model_name.pluralize
|
|
372
|
+
|
|
373
|
+
table_match = schema.match(
|
|
374
|
+
/create_table\s+["']#{Regexp.escape(table_name)}["'].*?do\s+\|t\|(.*?)^\s*end/m
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
return false unless table_match
|
|
378
|
+
|
|
379
|
+
table_match[1].match?(
|
|
380
|
+
/t\.\w+\s+["']password_digest["']/
|
|
381
|
+
)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def password_digest_defined_in_migrations?
|
|
385
|
+
migration_paths = Dir.glob(
|
|
386
|
+
File.join(
|
|
387
|
+
destination_root,
|
|
388
|
+
"db/migrate/*.rb"
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
table_name = authentication_model_name.pluralize
|
|
393
|
+
|
|
394
|
+
migration_paths.any? do |migration_path|
|
|
395
|
+
migration = File.read(migration_path)
|
|
396
|
+
|
|
397
|
+
references_table =
|
|
398
|
+
migration.match?(
|
|
399
|
+
/create_table\s+[:'"]#{Regexp.escape(table_name)}['"]?/
|
|
400
|
+
) ||
|
|
401
|
+
migration.match?(
|
|
402
|
+
/change_table\s+[:'"]#{Regexp.escape(table_name)}['"]?/
|
|
403
|
+
) ||
|
|
404
|
+
migration.match?(
|
|
405
|
+
/add_column\s+[:'"]#{Regexp.escape(table_name)}['"]?/
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
references_table &&
|
|
409
|
+
migration.match?(/\bpassword_digest\b/)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<%% content_for :title, "Login" %>
|
|
2
|
+
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h1>Login</h1>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="b4um-grid">
|
|
8
|
+
<section class="b4um-card b4um-card--full">
|
|
9
|
+
<h2 class="b4um-card__title">
|
|
10
|
+
Sign in
|
|
11
|
+
</h2>
|
|
12
|
+
|
|
13
|
+
<%%= form_with url: login_path, class: "form" do |form| %>
|
|
14
|
+
<div class="form-field">
|
|
15
|
+
<%%= form.email_field :email,
|
|
16
|
+
class: "form-input",
|
|
17
|
+
placeholder: " ",
|
|
18
|
+
required: true %>
|
|
19
|
+
<%%= form.label :email, "Email", class: "form-label" %>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="form-field">
|
|
23
|
+
<%%= form.password_field :password,
|
|
24
|
+
class: "form-input",
|
|
25
|
+
placeholder: " ",
|
|
26
|
+
required: true %>
|
|
27
|
+
<%%= form.label :password, "Password", class: "form-label" %>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="form-actions">
|
|
31
|
+
<%%= form.submit "Login", class: "form-submit" %>
|
|
32
|
+
</div>
|
|
33
|
+
<%% end %>
|
|
34
|
+
</section>
|
|
35
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class SessionsController < ApplicationController
|
|
4
|
+
def new
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def create
|
|
8
|
+
<%= authentication_model_name %> = <%= authentication_class_name %>.find_by(email: params[:email])
|
|
9
|
+
|
|
10
|
+
if <%= authentication_model_name %>&.authenticate(params[:password])
|
|
11
|
+
session[:<%= authentication_model_name %>_id] = <%= authentication_model_name %>.id
|
|
12
|
+
|
|
13
|
+
redirect_to root_path, notice: "Successfully logged in."
|
|
14
|
+
else
|
|
15
|
+
flash.now[:alert] = "Invalid email or password."
|
|
16
|
+
render :new, status: :unprocessable_content
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def destroy
|
|
21
|
+
session.delete(:<%= authentication_model_name %>_id)
|
|
22
|
+
|
|
23
|
+
redirect_to root_path, notice: "Successfully logged out."
|
|
24
|
+
end
|
|
25
|
+
end
|