practical 0.1.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 +7 -0
- data/README.md +37 -0
- data/Rakefile +10 -0
- data/app/components/practical/views/base_component.rb +6 -0
- data/app/components/practical/views/button_component.rb +27 -0
- data/app/components/practical/views/datatable/filter_applied.rb +25 -0
- data/app/components/practical/views/datatable/filter_section_component.html.erb +9 -0
- data/app/components/practical/views/datatable/filter_section_component.rb +19 -0
- data/app/components/practical/views/datatable/sort_link_component.rb +48 -0
- data/app/components/practical/views/datatable.rb +36 -0
- data/app/components/practical/views/flash_messages_component.rb +65 -0
- data/app/components/practical/views/form/error_list_component.rb +15 -0
- data/app/components/practical/views/form/error_list_item_component.rb +20 -0
- data/app/components/practical/views/form/error_list_item_template_component.rb +9 -0
- data/app/components/practical/views/form/fallback_errors_section_component.html.erb +7 -0
- data/app/components/practical/views/form/fallback_errors_section_component.rb +21 -0
- data/app/components/practical/views/form/field_errors_component.rb +28 -0
- data/app/components/practical/views/form/field_title_component.rb +23 -0
- data/app/components/practical/views/form/fieldset_title_component.rb +20 -0
- data/app/components/practical/views/form/input_component.html.erb +7 -0
- data/app/components/practical/views/form/input_component.rb +22 -0
- data/app/components/practical/views/form/option_label_component.rb +21 -0
- data/app/components/practical/views/form/practical_editor_component.rb +26 -0
- data/app/components/practical/views/form/required_radio_collection_wrapper_component.rb +23 -0
- data/app/components/practical/views/form_wrapper.rb +21 -0
- data/app/components/practical/views/icon_component.rb +36 -0
- data/app/components/practical/views/icon_for_file_extension_component.rb +53 -0
- data/app/components/practical/views/modal_dialog_component.html.erb +10 -0
- data/app/components/practical/views/modal_dialog_component.rb +16 -0
- data/app/components/practical/views/navigation/breadcrumb_item_component.rb +20 -0
- data/app/components/practical/views/navigation/breadcrumbs_component.html.erb +31 -0
- data/app/components/practical/views/navigation/breadcrumbs_component.rb +41 -0
- data/app/components/practical/views/navigation/navigation_link_component.rb +39 -0
- data/app/components/practical/views/navigation/pagination/goto_form_component.html.erb +31 -0
- data/app/components/practical/views/navigation/pagination/goto_form_component.rb +34 -0
- data/app/components/practical/views/navigation/pagination_component.html.erb +11 -0
- data/app/components/practical/views/navigation/pagination_component.rb +98 -0
- data/app/components/practical/views/open_dialog_button_component.rb +16 -0
- data/app/components/practical/views/page_component.html.erb +53 -0
- data/app/components/practical/views/page_component.rb +12 -0
- data/app/components/practical/views/relative_time_component.rb +13 -0
- data/app/components/practical/views/tiptap_document_component.rb +311 -0
- data/app/components/practical/views/toast_component.html.erb +26 -0
- data/app/components/practical/views/toast_component.rb +19 -0
- data/app/controllers/concerns/practical/auth/passkeys/emergency_registrations.rb +57 -0
- data/app/controllers/concerns/practical/auth/passkeys/web_authn_debug_context.rb +13 -0
- data/app/controllers/concerns/practical/views/flash_helpers.rb +37 -0
- data/app/controllers/concerns/practical/views/json_redirection.rb +7 -0
- data/app/lib/practical/defaults/shrine.rb +48 -0
- data/app/lib/practical/test/helpers/administrator/test_helpers.rb +7 -0
- data/app/lib/practical/test/helpers/extra_assertions.rb +7 -0
- data/app/lib/practical/test/helpers/flash_assertions.rb +8 -0
- data/app/lib/practical/test/helpers/integration/assertions.rb +23 -0
- data/app/lib/practical/test/helpers/passkey/system/base.rb +52 -0
- data/app/lib/practical/test/helpers/passkey/system/rack_test.rb +45 -0
- data/app/lib/practical/test/helpers/passkey/system/selenium.rb +107 -0
- data/app/lib/practical/test/helpers/passkey/test_helper.rb +128 -0
- data/app/lib/practical/test/helpers/postmark.rb +11 -0
- data/app/lib/practical/test/helpers/relation_builder_assertions.rb +18 -0
- data/app/lib/practical/test/helpers/setup/debug.rb +8 -0
- data/app/lib/practical/test/helpers/setup/faker_seed_pinning.rb +8 -0
- data/app/lib/practical/test/helpers/setup/simplecov.rb +17 -0
- data/app/lib/practical/test/helpers/shrine/test_data.rb +101 -0
- data/app/lib/practical/test/helpers/spy_assertions.rb +7 -0
- data/app/lib/practical/test/helpers/system/assertions.rb +33 -0
- data/app/lib/practical/test/helpers/system/capybara_prep.rb +10 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +372 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/self_service.rb +66 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +119 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_destroy.rb +13 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_signup.rb +22 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +134 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +221 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +220 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/base.rb +108 -0
- data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +82 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/base.rb +89 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/use_for_and_notify.rb +48 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/passkey.rb +101 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys.rb +57 -0
- data/app/lib/practical/test/shared/auth/passkeys/policies/passkey.rb +18 -0
- data/app/lib/practical/test/shared/auth/passkeys/services/send_emergency_registration.rb +41 -0
- data/app/lib/practical/test/shared/models/normalized_email.rb +23 -0
- data/app/lib/practical/test/shared/models/user.rb +27 -0
- data/app/lib/practical/test/shared/models/utility/ip_address.rb +42 -0
- data/app/lib/practical/test/shared/models/utility/user_agent.rb +43 -0
- data/app/lib/practical/views/button/styling.rb +23 -0
- data/app/lib/practical/views/error_handling.rb +33 -0
- data/app/lib/practical/views/form_builders/base.rb +152 -0
- data/app/lib/practical/views/icon_set.rb +156 -0
- data/app/lib/practical/views/web_awesome/style_utility/appearance_variant.rb +19 -0
- data/app/lib/practical/views/web_awesome/style_utility/base.rb +21 -0
- data/app/lib/practical/views/web_awesome/style_utility/color_variant.rb +17 -0
- data/app/lib/practical/views/web_awesome/style_utility/size.rb +31 -0
- data/config/locales/auth.en.yml +38 -0
- data/config/locales/devise.passkeys.en.yml +18 -0
- data/config/locales/practical_framework.en.yml +9 -0
- data/config/routes.rb +4 -0
- data/db/seeds/setup.rb +13 -0
- data/db/seeds/users/default.rb +34 -0
- data/lib/generators/practical/test/helper/USAGE +8 -0
- data/lib/generators/practical/test/helper/helper_generator.rb +9 -0
- data/lib/generators/practical/test/helper/templates/helper.rb.tt +4 -0
- data/lib/generators/practical/test/shared_test/USAGE +9 -0
- data/lib/generators/practical/test/shared_test/shared_test_generator.rb +7 -0
- data/lib/generators/practical/test/shared_test/templates/shared_test.rb.tt +9 -0
- data/lib/generators/practical/views/component/USAGE +9 -0
- data/lib/generators/practical/views/component/component_generator.rb +20 -0
- data/lib/practical/framework/engine.rb +35 -0
- data/lib/practical/helpers/form_with_helper.rb +10 -0
- data/lib/practical/helpers/icon_helper.rb +18 -0
- data/lib/practical/helpers/text_helper.rb +20 -0
- data/lib/practical/helpers/translation_helper.rb +25 -0
- data/lib/practical/version.rb +5 -0
- data/lib/practical/views/element_helper.rb +48 -0
- data/lib/practical.rb +21 -0
- data/lib/tasks/practical/coverage.rake +19 -0
- data/lib/tasks/practical/framework_tasks.rake +6 -0
- metadata +303 -0
@@ -0,0 +1,311 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Practical::Views::TiptapDocumentComponent < Practical::Views::BaseComponent
|
4
|
+
class UnknownNodeTypeError < StandardError; end
|
5
|
+
class UnknownMarkupTypeError < StandardError; end
|
6
|
+
|
7
|
+
module NodeRendering
|
8
|
+
def render_node(node:)
|
9
|
+
case node[:type].to_sym
|
10
|
+
when :text
|
11
|
+
render Text.new(node_content: node)
|
12
|
+
when :paragraph
|
13
|
+
render Paragraph.new(node_content: node)
|
14
|
+
when :heading
|
15
|
+
render Heading.new(node_content: node)
|
16
|
+
when :codeBlock
|
17
|
+
render CodeBlock.new(node_content: node)
|
18
|
+
when :listItem
|
19
|
+
render ListItem.new(node_content: node)
|
20
|
+
when :bulletList
|
21
|
+
render UnorderedList.new(node_content: node)
|
22
|
+
when :orderedList
|
23
|
+
render OrderedList.new(node_content: node)
|
24
|
+
when :"attachment-figure", :"previewable-attachment-figure"
|
25
|
+
render Attachment.new(node_content: node)
|
26
|
+
when :blockquote
|
27
|
+
render Blockquote.new(node_content: node)
|
28
|
+
when :hardBreak
|
29
|
+
render HardBreak.new(node_content: node)
|
30
|
+
else
|
31
|
+
raise UnknownNodeTypeError
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
include NodeRendering
|
37
|
+
|
38
|
+
attr_reader :document
|
39
|
+
|
40
|
+
def initialize(document:)
|
41
|
+
raise ArgumentError if document["type"] != "doc"
|
42
|
+
@document = document.with_indifferent_access
|
43
|
+
end
|
44
|
+
|
45
|
+
def call
|
46
|
+
safe_join([document[:content].map do |node|
|
47
|
+
render_node(node: node)
|
48
|
+
end])
|
49
|
+
end
|
50
|
+
|
51
|
+
class Node < Practical::Views::BaseComponent
|
52
|
+
include NodeRendering
|
53
|
+
|
54
|
+
attr_reader :node_content
|
55
|
+
|
56
|
+
def initialize(node_content:)
|
57
|
+
@node_content = node_content
|
58
|
+
end
|
59
|
+
|
60
|
+
def render_node_contents
|
61
|
+
if node_content.present? && node_content[:content].present?
|
62
|
+
safe_join(node_content[:content].map{|node| render_node(node: node)})
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class HardBreak < Node
|
68
|
+
def call
|
69
|
+
tag.br
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Text < Node
|
74
|
+
SORTED_MARKUP_TYPES = [
|
75
|
+
"rhino-strike", "link", "bold", "italic"
|
76
|
+
].freeze
|
77
|
+
|
78
|
+
def applicable_markup_types
|
79
|
+
SORTED_MARKUP_TYPES.select{|type| node_content[:marks].any?{ |mark| mark[:type] == type }}
|
80
|
+
end
|
81
|
+
|
82
|
+
def call
|
83
|
+
if node_content[:marks].present? && node_content[:marks].any?
|
84
|
+
render_with_marks(markup_to_apply: applicable_markup_types)
|
85
|
+
else
|
86
|
+
render_plaintext
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def render_with_marks(markup_to_apply:)
|
91
|
+
markup_type = markup_to_apply.shift
|
92
|
+
case markup_type
|
93
|
+
when "italic"
|
94
|
+
tag.em{ render_with_marks(markup_to_apply: markup_to_apply) }
|
95
|
+
when "bold"
|
96
|
+
tag.strong { render_with_marks(markup_to_apply: markup_to_apply) }
|
97
|
+
when "rhino-strike"
|
98
|
+
tag.del { render_with_marks(markup_to_apply: markup_to_apply) }
|
99
|
+
when "link"
|
100
|
+
tag.a(**link_attributes) { render_with_marks(markup_to_apply: markup_to_apply) }
|
101
|
+
when nil
|
102
|
+
render_plaintext
|
103
|
+
else
|
104
|
+
raise UnknownMarkupTypeError
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def link_attributes
|
109
|
+
node_content[:marks]&.find{|mark| mark[:type] == "link" }&.dig(:attrs)&.slice(:href, :target, :rel).to_h
|
110
|
+
end
|
111
|
+
|
112
|
+
def render_plaintext
|
113
|
+
helpers.sanitize(node_content[:text])
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class Paragraph < Node
|
118
|
+
def call
|
119
|
+
tag.p {
|
120
|
+
render_node_contents
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class Heading < Node
|
126
|
+
def call
|
127
|
+
heading_element {
|
128
|
+
render_node_contents
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
def heading_element(&block)
|
133
|
+
case node_content.dig(:attrs, :level)
|
134
|
+
when 1
|
135
|
+
tag.h1(&block)
|
136
|
+
when 2
|
137
|
+
tag.h2(&block)
|
138
|
+
when 3
|
139
|
+
tag.h3(&block)
|
140
|
+
when 4
|
141
|
+
tag.h4(&block)
|
142
|
+
when 5
|
143
|
+
tag.h5(&block)
|
144
|
+
when 6
|
145
|
+
tag.h6(&block)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class Blockquote < Node
|
151
|
+
def call
|
152
|
+
tag.blockquote {
|
153
|
+
render_node_contents
|
154
|
+
}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class CodeBlock < Node
|
159
|
+
def call
|
160
|
+
tag.pre {
|
161
|
+
tag.code { render_node_contents }
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
class ListItem < Node
|
167
|
+
def call
|
168
|
+
tag.li {
|
169
|
+
render_node_contents
|
170
|
+
}
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class UnorderedList < Node
|
175
|
+
def call
|
176
|
+
tag.ul {
|
177
|
+
render_node_contents
|
178
|
+
}
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class OrderedList < Node
|
183
|
+
def call
|
184
|
+
tag.ol(start: node_content.dig(:attrs, :start)) {
|
185
|
+
render_node_contents
|
186
|
+
}
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class Attachment < Node
|
191
|
+
def call
|
192
|
+
tag.figure(class: 'wa-stack wa-gap-s') {
|
193
|
+
if missing_attachment?
|
194
|
+
missing_attachment_figure
|
195
|
+
else
|
196
|
+
attachment_figure
|
197
|
+
end
|
198
|
+
}
|
199
|
+
end
|
200
|
+
|
201
|
+
def missing_attachment_figure
|
202
|
+
safe_join([
|
203
|
+
tag.div {
|
204
|
+
render Practical::Views::IconForFileExtensionComponent.new(extension: "missing")
|
205
|
+
},
|
206
|
+
|
207
|
+
tag.section(class: 'attachment-details') {
|
208
|
+
tag.p(t("tiptap_document.attachment_missing.text"))
|
209
|
+
},
|
210
|
+
|
211
|
+
figure_caption
|
212
|
+
])
|
213
|
+
end
|
214
|
+
|
215
|
+
def attachment_figure
|
216
|
+
if previewable?
|
217
|
+
image = tag.div {
|
218
|
+
tag.img(src: url, width: width, height: height)
|
219
|
+
}
|
220
|
+
else
|
221
|
+
image = tag.div {
|
222
|
+
render Practical::Views::IconForFileExtensionComponent.new(extension: extension)
|
223
|
+
}
|
224
|
+
end
|
225
|
+
|
226
|
+
safe_join([
|
227
|
+
image,
|
228
|
+
attachment_details_and_download,
|
229
|
+
figure_caption
|
230
|
+
])
|
231
|
+
end
|
232
|
+
|
233
|
+
def figure_caption
|
234
|
+
if node_content[:content].present?
|
235
|
+
tag.figcaption { render_node_contents }
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def attachment_details_and_download
|
240
|
+
tag.section(class: 'attachment-details') {
|
241
|
+
tag.p {
|
242
|
+
tag.a(href: url, target: "_blank") {
|
243
|
+
"#{filename} – #{human_file_size}"
|
244
|
+
}
|
245
|
+
}
|
246
|
+
}
|
247
|
+
end
|
248
|
+
|
249
|
+
def attachment
|
250
|
+
@attachment ||= GlobalID::Locator.locate_signed(sgid.to_s, for: :document)&.attachment
|
251
|
+
end
|
252
|
+
|
253
|
+
def missing_attachment?
|
254
|
+
attachment.nil?
|
255
|
+
end
|
256
|
+
|
257
|
+
def attrs
|
258
|
+
node_content[:attrs]
|
259
|
+
end
|
260
|
+
|
261
|
+
def previewable?
|
262
|
+
attrs.dig(:previewable)
|
263
|
+
end
|
264
|
+
|
265
|
+
def sgid
|
266
|
+
attrs.dig(:sgid)
|
267
|
+
end
|
268
|
+
|
269
|
+
def filename
|
270
|
+
attachment.original_filename
|
271
|
+
end
|
272
|
+
|
273
|
+
def human_file_size
|
274
|
+
helpers.number_to_human_size(attachment.size)
|
275
|
+
end
|
276
|
+
|
277
|
+
def url
|
278
|
+
attachment.url
|
279
|
+
end
|
280
|
+
|
281
|
+
def extension
|
282
|
+
attachment.extension
|
283
|
+
end
|
284
|
+
|
285
|
+
def stored_width
|
286
|
+
attrs.dig(:width)
|
287
|
+
end
|
288
|
+
|
289
|
+
def stored_height
|
290
|
+
attrs.dig(:height)
|
291
|
+
end
|
292
|
+
|
293
|
+
def has_dimensions?
|
294
|
+
!stored_width.blank? && !stored_height.blank?
|
295
|
+
end
|
296
|
+
|
297
|
+
def width
|
298
|
+
return stored_width if has_dimensions?
|
299
|
+
default_figure_size
|
300
|
+
end
|
301
|
+
|
302
|
+
def height
|
303
|
+
return stored_height if has_dimensions?
|
304
|
+
default_figure_size
|
305
|
+
end
|
306
|
+
|
307
|
+
def default_figure_size
|
308
|
+
100
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<toast-dialog>
|
2
|
+
<dialog open class='wa-off toast'>
|
3
|
+
<%= tag.section(**finalized_callout_options) do %>
|
4
|
+
<div class="wa-flank:end wa-gap-xs">
|
5
|
+
<% if icon? %>
|
6
|
+
<section class='wa-flank'>
|
7
|
+
<%= icon %>
|
8
|
+
<p><%= content %></p>
|
9
|
+
</section>
|
10
|
+
<% else %>
|
11
|
+
<p><%= content %></p>
|
12
|
+
<% end %>
|
13
|
+
|
14
|
+
<form method="dialog">
|
15
|
+
<%= render Practical::Views::ButtonComponent.new(type: :submit, appearance: :plain, ) do %>
|
16
|
+
<wa-progress-ring value="100" remaining-time-indicator>
|
17
|
+
<%= render icon_set.dialog_close_icon %>
|
18
|
+
</wa-progress-ring>
|
19
|
+
<% end %>
|
20
|
+
</form>
|
21
|
+
</div>
|
22
|
+
<% end %>
|
23
|
+
|
24
|
+
<auto-expire duration-seconds="8" tick-rate-ms="150"></auto-expire>
|
25
|
+
</dialog>
|
26
|
+
</toast-dialog>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Practical::Views::ToastComponent < Practical::Views::BaseComponent
|
4
|
+
include Practical::Views::Button::Styling
|
5
|
+
attr_accessor :appearance, :color_variant, :size, :options
|
6
|
+
|
7
|
+
renders_one :icon
|
8
|
+
|
9
|
+
def initialize(appearance: nil, color_variant: nil, size: nil, options: {})
|
10
|
+
@options = options
|
11
|
+
initialize_style_utilities(appearance: appearance, color_variant: color_variant, size: size)
|
12
|
+
end
|
13
|
+
|
14
|
+
def finalized_callout_options
|
15
|
+
mix({
|
16
|
+
class: class_names("wa-callout", css_classes_from_style_utilities)
|
17
|
+
}, options)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Auth::Passkeys::EmergencyRegistrations
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include Practical::Auth::Passkeys::WebAuthnDebugContext
|
6
|
+
|
7
|
+
def new_challenge
|
8
|
+
options_for_registration = generate_registration_options(
|
9
|
+
relying_party: relying_party,
|
10
|
+
user_details: user_details_for_registration,
|
11
|
+
exclude: exclude_external_ids_for_registration
|
12
|
+
)
|
13
|
+
|
14
|
+
store_challenge_in_session(options_for_registration: options_for_registration)
|
15
|
+
|
16
|
+
render json: options_for_registration
|
17
|
+
end
|
18
|
+
|
19
|
+
def raw_credential
|
20
|
+
passkey_params[:passkey_credential]
|
21
|
+
end
|
22
|
+
|
23
|
+
def verify_credential_integrity
|
24
|
+
return render_credential_missing_or_could_not_be_parsed_error if parsed_credential.nil?
|
25
|
+
return render_credential_missing_or_could_not_be_parsed_error unless parsed_credential.kind_of?(Hash)
|
26
|
+
rescue JSON::JSONError, TypeError
|
27
|
+
return render_credential_missing_or_could_not_be_parsed_error
|
28
|
+
end
|
29
|
+
|
30
|
+
def verify_passkey_challenge
|
31
|
+
@webauthn_credential = verify_registration(relying_party: relying_party)
|
32
|
+
rescue ::WebAuthn::Error => e
|
33
|
+
Honeybadger.notify(e, context: honeybadger_webauthn_context)
|
34
|
+
error_key = Warden::WebAuthn::ErrorKeyFinder.webauthn_error_key(exception: e)
|
35
|
+
render_passkey_error(message: find_message(error_key))
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
|
39
|
+
def request_form_params
|
40
|
+
params.require(:new_emergency_passkey_registration_form).permit(:email)
|
41
|
+
end
|
42
|
+
|
43
|
+
def render_credential_missing_or_could_not_be_parsed_error
|
44
|
+
render_passkey_error(message: find_message(:credential_missing_or_could_not_be_parsed))
|
45
|
+
delete_registration_challenge
|
46
|
+
return false
|
47
|
+
end
|
48
|
+
|
49
|
+
def render_passkey_error(message:)
|
50
|
+
errors = Practical::Views::ErrorHandling.build_error_json(
|
51
|
+
model: temporary_form_with_passkey_credential_error(message: message),
|
52
|
+
helpers: helpers
|
53
|
+
)
|
54
|
+
|
55
|
+
render json: errors, status: :unprocessable_entity
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Auth::Passkeys::WebAuthnDebugContext
|
4
|
+
def honeybadger_webauthn_context
|
5
|
+
debug_credential = WebAuthn::Credential.from_create(parsed_credential, relying_party: relying_party)
|
6
|
+
debug_client_data_json = debug_credential.response.client_data.as_json
|
7
|
+
|
8
|
+
return {
|
9
|
+
debug_client_data_json: debug_client_data_json,
|
10
|
+
relying_party_json: relying_party.as_json
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Views::FlashHelpers
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
add_flash_types :success
|
8
|
+
end
|
9
|
+
|
10
|
+
def flash_message_with_icon(message:, icon:)
|
11
|
+
{message: message, icon: icon}
|
12
|
+
end
|
13
|
+
|
14
|
+
def flash_notice_with_icon(message:, icon: default_notice_icon)
|
15
|
+
flash_message_with_icon(message: message, icon: icon)
|
16
|
+
end
|
17
|
+
|
18
|
+
def flash_alert_with_icon(message:, icon: default_alert_icon)
|
19
|
+
flash_message_with_icon(message: message, icon: icon)
|
20
|
+
end
|
21
|
+
|
22
|
+
def flash_success_with_icon(message:, icon: default_success_icon)
|
23
|
+
flash_message_with_icon(message: message, icon: icon)
|
24
|
+
end
|
25
|
+
|
26
|
+
def default_notice_icon
|
27
|
+
return helpers.icon_set.info_icon
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_alert_icon
|
31
|
+
return helpers.icon_set.alert_icon
|
32
|
+
end
|
33
|
+
|
34
|
+
def default_success_icon
|
35
|
+
return helpers.icon_set.success_icon
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Defaults::Shrine
|
4
|
+
def self.max_file_size
|
5
|
+
(20*1024*1024).freeze # 20 MB
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.mime_types
|
9
|
+
%w(image/jpeg
|
10
|
+
image/png
|
11
|
+
image/webp
|
12
|
+
image/tiff
|
13
|
+
image/gif
|
14
|
+
image/heic
|
15
|
+
text/csv
|
16
|
+
application/pdf
|
17
|
+
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
18
|
+
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
19
|
+
application/msword
|
20
|
+
application/vnd.ms-excel
|
21
|
+
text/plain
|
22
|
+
application/rtf
|
23
|
+
text/rtf
|
24
|
+
application/vnd.apple.numbers
|
25
|
+
).freeze
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.extensions
|
29
|
+
%w(jpg
|
30
|
+
jpeg
|
31
|
+
png
|
32
|
+
webp
|
33
|
+
tiff
|
34
|
+
tif
|
35
|
+
gif
|
36
|
+
heic
|
37
|
+
csv
|
38
|
+
pdf
|
39
|
+
docx
|
40
|
+
xlsx
|
41
|
+
doc
|
42
|
+
xls
|
43
|
+
txt
|
44
|
+
rtf
|
45
|
+
numbers
|
46
|
+
).freeze
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Helpers::FlashAssertions
|
4
|
+
def assert_flash_message(type:, message:, icon_name:)
|
5
|
+
assert_equal message, flash[type][:message], flash
|
6
|
+
assert_includes flash[type][:icon].name.to_s, icon_name, flash
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Helpers::Integration::Assertions
|
4
|
+
def assert_error_json_contains(container_id:, element_id:, message:, type:)
|
5
|
+
found_message = response.parsed_body.find do |error_json|
|
6
|
+
error_json["container_id"] == container_id &&
|
7
|
+
error_json["element_to_invalidate_id"] == element_id &&
|
8
|
+
error_json["message"] == message &&
|
9
|
+
error_json["type"] == type
|
10
|
+
end
|
11
|
+
|
12
|
+
assert_not_nil found_message, response.parsed_body
|
13
|
+
end
|
14
|
+
|
15
|
+
def assert_json_redirected_to(location)
|
16
|
+
assert_equal "322", response.code
|
17
|
+
assert_equal location, response.parsed_body["location"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def assert_error_dom(container_id:, message:)
|
21
|
+
assert_dom("##{container_id}", text: message)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webauthn/fake_client"
|
4
|
+
|
5
|
+
module Practical::Test::Helpers::Passkey::System::Base
|
6
|
+
def fake_authenticator
|
7
|
+
@fake_authenticator ||= WebAuthn::FakeAuthenticator.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def user_webauthn_client
|
11
|
+
@webauthn_client ||= WebAuthn::FakeClient.new(relying_party_origin, authenticator: fake_authenticator)
|
12
|
+
end
|
13
|
+
|
14
|
+
def administrator_webauthn_client
|
15
|
+
@webauthn_client ||= WebAuthn::FakeClient.new(admin_relying_party_origin, authenticator: fake_authenticator)
|
16
|
+
end
|
17
|
+
|
18
|
+
def user_relying_party
|
19
|
+
return WebAuthn::RelyingParty.new(
|
20
|
+
allowed_origins: relying_party_origin,
|
21
|
+
name: I18n.translate("app_title.text")
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_webauthn_credential_from_scratch(webauthn_client:, rp_id: nil, relying_party:)
|
26
|
+
rp_id ||= relying_party.id || URI.parse(webauthn_client.origin).host
|
27
|
+
|
28
|
+
create_result = webauthn_client.create(rp_id: rp_id)
|
29
|
+
|
30
|
+
attestation_object =
|
31
|
+
if webauthn_client.encoding
|
32
|
+
relying_party.encoder.decode(create_result["response"]["attestationObject"])
|
33
|
+
else
|
34
|
+
create_result["response"]["attestationObject"]
|
35
|
+
end
|
36
|
+
|
37
|
+
client_data_json =
|
38
|
+
if webauthn_client.encoding
|
39
|
+
relying_party.encoder.decode(create_result["response"]["clientDataJSON"])
|
40
|
+
else
|
41
|
+
create_result["response"]["clientDataJSON"]
|
42
|
+
end
|
43
|
+
|
44
|
+
response = WebAuthn::AuthenticatorAttestationResponse.new(
|
45
|
+
attestation_object: attestation_object,
|
46
|
+
client_data_json: client_data_json,
|
47
|
+
relying_party: relying_party
|
48
|
+
)
|
49
|
+
|
50
|
+
return response.credential
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webauthn/fake_client"
|
4
|
+
|
5
|
+
module Practical::Test::Helpers::Passkey::System::RackTest
|
6
|
+
include Practical::Test::Helpers::Passkey::System::Base
|
7
|
+
|
8
|
+
def create_passkey_for_user_and_return_webauthn_credential(user:)
|
9
|
+
webauthn_credential = create_webauthn_credential_from_scratch(webauthn_client: user_webauthn_client,
|
10
|
+
rp_id: user_relying_party_id,
|
11
|
+
relying_party: user_relying_party
|
12
|
+
)
|
13
|
+
fake_authenticator.instance_variable_get("@credentials")[user_relying_party_id]
|
14
|
+
[webauthn_credential.id]
|
15
|
+
[:credential_key]
|
16
|
+
|
17
|
+
user.passkeys.create!(
|
18
|
+
label: SecureRandom.hex,
|
19
|
+
external_id: Base64.strict_encode64(webauthn_credential.id),
|
20
|
+
public_key: Base64.strict_encode64(webauthn_credential.public_key),
|
21
|
+
sign_count: 0
|
22
|
+
)
|
23
|
+
|
24
|
+
return webauthn_credential
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_passkey_for_administrator_and_return_webauthn_credential(administrator:)
|
28
|
+
webauthn_credential = create_webauthn_credential_from_scratch(webauthn_client: administrator_webauthn_client,
|
29
|
+
rp_id: admin_relying_party_id,
|
30
|
+
relying_party: admin_relying_party
|
31
|
+
)
|
32
|
+
fake_authenticator.instance_variable_get("@credentials")[admin_relying_party_id]
|
33
|
+
[webauthn_credential.id]
|
34
|
+
[:credential_key]
|
35
|
+
|
36
|
+
administrator.passkeys.create!(
|
37
|
+
label: SecureRandom.hex,
|
38
|
+
external_id: Base64.strict_encode64(webauthn_credential.id),
|
39
|
+
public_key: Base64.strict_encode64(webauthn_credential.public_key),
|
40
|
+
sign_count: 0
|
41
|
+
)
|
42
|
+
|
43
|
+
return webauthn_credential
|
44
|
+
end
|
45
|
+
end
|