Cleanup form helpers
This commit is contained in:
@@ -32,12 +32,4 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(rails-passkey-creation-button, rails-passkey-sign-in-button) [data-passkey-error] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(rails-passkey-creation-button, rails-passkey-sign-in-button)[data-passkey-error-state="error"] [data-passkey-error="error"],
|
|
||||||
:is(rails-passkey-creation-button, rails-passkey-sign-in-button)[data-passkey-error-state="cancelled"] [data-passkey-error="cancelled"] {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,34 +14,37 @@
|
|||||||
// passkey:error — ceremony failed; detail: { error, cancelled }
|
// passkey:error — ceremony failed; detail: { error, cancelled }
|
||||||
//
|
//
|
||||||
// Attributes (rendered by the Ruby form helpers):
|
// Attributes (rendered by the Ruby form helpers):
|
||||||
// creation-options — JSON WebAuthn creation options (on rails-passkey-creation-button)
|
// options — JSON WebAuthn options (creation or request, on both)
|
||||||
// request-options — JSON WebAuthn request options (on rails-passkey-sign-in-button)
|
// challenge-url — endpoint to refresh the challenge nonce (on both)
|
||||||
// challenge-url — endpoint to refresh the challenge nonce (on both)
|
// mediation — WebAuthn mediation hint, e.g. "conditional" (on rails-passkey-sign-in-button)
|
||||||
// mediation — WebAuthn mediation hint, e.g. "conditional" (on rails-passkey-sign-in-button)
|
|
||||||
|
|
||||||
import { register, authenticate } from "lib/action_pack/webauthn"
|
import { register, authenticate } from "lib/action_pack/webauthn"
|
||||||
|
|
||||||
class PasskeyCreationButton extends HTMLElement {
|
// Base class for passkey web components. Manages the shared ceremony lifecycle:
|
||||||
|
// challenge refresh, button state, error display, and event dispatch.
|
||||||
|
// Subclasses implement `perform()` to run the specific WebAuthn ceremony
|
||||||
|
// and `fillForm()` to populate hidden fields before submission.
|
||||||
|
class PasskeyButton extends HTMLElement {
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.button.addEventListener("click", this.#create)
|
this.button.addEventListener("click", this.#perform)
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this.button.removeEventListener("click", this.#create)
|
this.button.removeEventListener("click", this.#perform)
|
||||||
this.button.disabled = false
|
this.button.disabled = false
|
||||||
delete this.dataset.passkeyErrorState
|
this.#hideErrors()
|
||||||
}
|
}
|
||||||
|
|
||||||
get button() {
|
get button() {
|
||||||
return this.querySelector("[data-passkey='create']")
|
return this.querySelector("[data-passkey]")
|
||||||
}
|
}
|
||||||
|
|
||||||
get form() {
|
get form() {
|
||||||
return this.querySelector("form")
|
return this.querySelector("form")
|
||||||
}
|
}
|
||||||
|
|
||||||
get creationOptions() {
|
get options() {
|
||||||
return JSON.parse(this.getAttribute("creation-options"))
|
return JSON.parse(this.getAttribute("options"))
|
||||||
}
|
}
|
||||||
|
|
||||||
get challengeUrl() {
|
get challengeUrl() {
|
||||||
@@ -49,20 +52,22 @@ class PasskeyCreationButton extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Arrow function to preserve `this` binding for addEventListener/removeEventListener.
|
// Arrow function to preserve `this` binding for addEventListener/removeEventListener.
|
||||||
#create = async () => {
|
#perform = async () => {
|
||||||
this.button.disabled = true
|
this.button.disabled = true
|
||||||
|
this.#hideErrors()
|
||||||
this.button.dispatchEvent(new CustomEvent("passkey:start", { bubbles: true }))
|
this.button.dispatchEvent(new CustomEvent("passkey:start", { bubbles: true }))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!passkeysAvailable()) throw new Error("Passkeys are not supported by this browser")
|
const options = this.options
|
||||||
if (!this.creationOptions) throw new Error("Missing passkey creation options")
|
|
||||||
|
if (!passkeysAvailable()) throw new Error("Passkeys are not supported by this browser")
|
||||||
|
if (!options) throw new Error("Missing passkey options")
|
||||||
|
|
||||||
const options = this.creationOptions
|
|
||||||
await refreshChallenge(options, this.challengeUrl)
|
await refreshChallenge(options, this.challengeUrl)
|
||||||
const passkey = await register(options)
|
const passkey = await this.perform(options)
|
||||||
|
|
||||||
this.button.dispatchEvent(new CustomEvent("passkey:success", { bubbles: true }))
|
this.button.dispatchEvent(new CustomEvent("passkey:success", { bubbles: true }))
|
||||||
fillCreateForm(this.form, passkey)
|
this.fillForm(passkey)
|
||||||
this.form.submit()
|
this.form.submit()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.button.disabled = false
|
this.button.disabled = false
|
||||||
@@ -71,97 +76,76 @@ class PasskeyCreationButton extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#handleError(error) {
|
#handleError(error) {
|
||||||
|
console.error("Passkey ceremony failed", error)
|
||||||
const cancelled = error.name === "AbortError" || error.name === "NotAllowedError"
|
const cancelled = error.name === "AbortError" || error.name === "NotAllowedError"
|
||||||
this.dataset.passkeyErrorState = cancelled ? "cancelled" : "error"
|
this.#showError(cancelled ? "cancelled" : "error")
|
||||||
this.button.dispatchEvent(new CustomEvent("passkey:error", { bubbles: true, detail: { error, cancelled } }))
|
this.button.dispatchEvent(new CustomEvent("passkey:error", { bubbles: true, detail: { error, cancelled } }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#showError(type) {
|
||||||
|
const el = this.querySelector(`[data-passkey-error="${type}"]`)
|
||||||
|
if (el) el.hidden = false
|
||||||
|
}
|
||||||
|
|
||||||
|
#hideErrors() {
|
||||||
|
for (const el of this.querySelectorAll("[data-passkey-error]")) el.hidden = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PasskeySignInButton extends HTMLElement {
|
class PasskeyCreationButton extends PasskeyButton {
|
||||||
|
async perform(options) {
|
||||||
|
return await register(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
fillForm(passkey) {
|
||||||
|
fillCreateForm(this.form, passkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasskeySignInButton extends PasskeyButton {
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.button.addEventListener("click", this.#signIn)
|
super.connectedCallback()
|
||||||
|
|
||||||
if (this.mediation === "conditional") this.#attemptConditionalMediation()
|
if (this.mediation === "conditional") this.#attemptConditionalMediation()
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
this.button.removeEventListener("click", this.#signIn)
|
|
||||||
this.button.disabled = false
|
|
||||||
delete this.dataset.passkeyErrorState
|
|
||||||
}
|
|
||||||
|
|
||||||
get button() {
|
|
||||||
return this.querySelector("[data-passkey='sign_in']")
|
|
||||||
}
|
|
||||||
|
|
||||||
get form() {
|
|
||||||
return this.querySelector("form")
|
|
||||||
}
|
|
||||||
|
|
||||||
get requestOptions() {
|
|
||||||
return JSON.parse(this.getAttribute("request-options"))
|
|
||||||
}
|
|
||||||
|
|
||||||
get challengeUrl() {
|
|
||||||
return this.getAttribute("challenge-url")
|
|
||||||
}
|
|
||||||
|
|
||||||
get mediation() {
|
get mediation() {
|
||||||
return this.getAttribute("mediation")
|
return this.getAttribute("mediation")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arrow function to preserve `this` binding for addEventListener/removeEventListener.
|
async perform(options, { mediation } = {}) {
|
||||||
#signIn = async () => {
|
return await authenticate(options, { mediation })
|
||||||
this.button.disabled = true
|
}
|
||||||
this.button.dispatchEvent(new CustomEvent("passkey:start", { bubbles: true }))
|
|
||||||
|
|
||||||
try {
|
fillForm(passkey) {
|
||||||
if (!passkeysAvailable()) throw new Error("Passkeys are not supported by this browser")
|
fillSignInForm(this.form, passkey)
|
||||||
if (!this.requestOptions) throw new Error("Missing passkey request options")
|
|
||||||
|
|
||||||
const options = this.requestOptions
|
|
||||||
await refreshChallenge(options, this.challengeUrl)
|
|
||||||
const passkey = await authenticate(options)
|
|
||||||
|
|
||||||
this.button.dispatchEvent(new CustomEvent("passkey:success", { bubbles: true }))
|
|
||||||
fillSignInForm(this.form, passkey)
|
|
||||||
this.form.submit()
|
|
||||||
} catch (error) {
|
|
||||||
this.button.disabled = false
|
|
||||||
this.#handleError(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async #attemptConditionalMediation() {
|
async #attemptConditionalMediation() {
|
||||||
if (await this.#conditionalMediationAvailable()) {
|
if (await this.#conditionalMediationAvailable()) {
|
||||||
const options = this.requestOptions
|
const options = this.options
|
||||||
|
|
||||||
this.form.dispatchEvent(new CustomEvent("passkey:start", { bubbles: true }))
|
this.form.dispatchEvent(new CustomEvent("passkey:start", { bubbles: true }))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await refreshChallenge(options, this.challengeUrl)
|
await refreshChallenge(options, this.challengeUrl)
|
||||||
const passkey = await authenticate(options, { mediation: this.mediation })
|
const passkey = await this.perform(options, { mediation: this.mediation })
|
||||||
|
|
||||||
this.form.dispatchEvent(new CustomEvent("passkey:success", { bubbles: true }))
|
this.form.dispatchEvent(new CustomEvent("passkey:success", { bubbles: true }))
|
||||||
fillSignInForm(this.form, passkey)
|
this.fillForm(passkey)
|
||||||
this.form.submit()
|
this.form.submit()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.#handleError(error)
|
console.error("Passkey conditional mediation failed", error)
|
||||||
|
const cancelled = error.name === "AbortError" || error.name === "NotAllowedError"
|
||||||
|
this.button.dispatchEvent(new CustomEvent("passkey:error", { bubbles: true, detail: { error, cancelled } }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #conditionalMediationAvailable() {
|
async #conditionalMediationAvailable() {
|
||||||
return this.requestOptions &&
|
return this.options &&
|
||||||
passkeysAvailable() &&
|
passkeysAvailable() &&
|
||||||
await window.PublicKeyCredential.isConditionalMediationAvailable?.()
|
await window.PublicKeyCredential.isConditionalMediationAvailable?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
#handleError(error) {
|
|
||||||
const cancelled = error.name === "AbortError" || error.name === "NotAllowedError"
|
|
||||||
this.dataset.passkeyErrorState = cancelled ? "cancelled" : "error"
|
|
||||||
this.button.dispatchEvent(new CustomEvent("passkey:error", { bubbles: true, detail: { error, cancelled } }))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("rails-passkey-creation-button", PasskeyCreationButton)
|
customElements.define("rails-passkey-creation-button", PasskeyCreationButton)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<%= passkey_creation_button my_passkeys_path, options: @creation_options, class: "btn btn--link center txt-medium" do %>
|
<%= passkey_creation_button my_passkeys_path, options: @creation_options, error: { class: "txt-negative" }, cancellation: { class: "txt-subtle" }, class: "btn btn--link center txt-medium" do %>
|
||||||
<%= icon_tag "add" %>
|
<%= icon_tag "add" %>
|
||||||
<span>Register a passkey</span>
|
<span>Register a passkey</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
# - +passkey_sign_in_button+ — render a <rails-passkey-sign-in-button> web component with
|
# - +passkey_sign_in_button+ — render a <rails-passkey-sign-in-button> web component with
|
||||||
# a form, hidden fields, and error messages for the authentication ceremony.
|
# a form, hidden fields, and error messages for the authentication ceremony.
|
||||||
module ActionPack::Passkey::FormHelper
|
module ActionPack::Passkey::FormHelper
|
||||||
|
CREATION_ERROR_MESSAGE = "Something went wrong while registering your passkey."
|
||||||
|
CREATION_CANCELLED_MESSAGE = "Passkey registration was cancelled. Try again when you are ready."
|
||||||
|
SIGN_IN_ERROR_MESSAGE = "Something went wrong while signing in with your passkey."
|
||||||
|
SIGN_IN_CANCELLED_MESSAGE = "Passkey sign in was cancelled. Try again when you are ready."
|
||||||
|
|
||||||
# Renders a +<rails-passkey-creation-button>+ web component containing a form with hidden
|
# Renders a +<rails-passkey-creation-button>+ web component containing a form with hidden
|
||||||
# fields for the passkey registration ceremony and error messages. The form POSTs to +url+ and
|
# fields for the passkey registration ceremony and error messages. The form POSTs to +url+ and
|
||||||
# includes hidden fields for +client_data_json+, +attestation_object+, and +transports+ —
|
# includes hidden fields for +client_data_json+, +attestation_object+, and +transports+ —
|
||||||
@@ -16,30 +21,28 @@ module ActionPack::Passkey::FormHelper
|
|||||||
# Options:
|
# Options:
|
||||||
# - +options+: WebAuthn creation options (JSON-serializable hash)
|
# - +options+: WebAuthn creation options (JSON-serializable hash)
|
||||||
# - +challenge_url+: endpoint to refresh the challenge nonce
|
# - +challenge_url+: endpoint to refresh the challenge nonce
|
||||||
# - +param+: the form parameter namespace (default: +:passkey+)
|
# - +form+: additional HTML attributes for the +<form>+ tag. Supports a +:param+ key
|
||||||
# - +error_message+: message shown on ceremony failure
|
# to set the form parameter namespace (default: +:passkey+)
|
||||||
# - +cancelled_message+: message shown when ceremony is cancelled
|
# - +error+: HTML attributes for the error message +<div>+. Supports a +:message+ key
|
||||||
# - +form+: additional HTML attributes for the +<form>+ tag
|
# to override the default error text
|
||||||
|
# - +cancellation+: HTML attributes for the cancellation message +<div>+. Supports a
|
||||||
|
# +:message+ key to override the default cancellation text
|
||||||
# - All other options are passed to the +<button>+ tag
|
# - All other options are passed to the +<button>+ tag
|
||||||
def passkey_creation_button(name = nil, url = nil, options:, challenge_url: nil, param: :passkey, error_message: "Something went wrong while registering your passkey.", cancelled_message: "Passkey registration was cancelled. Try again when you are ready.", form: {}, **button_options, &block)
|
def passkey_creation_button(name = nil, url = nil, **options, &block)
|
||||||
url, name = name, block ? capture(&block) : nil if block_given?
|
url, name = name, block ? capture(&block) : nil if block_given?
|
||||||
form_options = form.reverse_merge(method: :post, action: url, class: "button_to")
|
component_options, form_options, button_options, error_options = extract_passkey_component_options(url, options)
|
||||||
|
error_options[:error][:message] ||= CREATION_ERROR_MESSAGE
|
||||||
|
error_options[:cancellation][:message] ||= CREATION_CANCELLED_MESSAGE
|
||||||
|
param = form_options.delete(:param)
|
||||||
|
|
||||||
component_options = {
|
content_tag("rails-passkey-creation-button", **component_options.transform_keys { |key| key.to_s.dasherize }) do
|
||||||
"creation-options": options.to_json,
|
tag.form(**form_options) do
|
||||||
"challenge-url": challenge_url || default_passkey_challenge_url
|
|
||||||
}
|
|
||||||
|
|
||||||
tag.send(:"rails-passkey-creation-button", **component_options) do
|
|
||||||
form_tag = tag.form(**form_options) do
|
|
||||||
hidden_field_tag(:authenticity_token, form_authenticity_token) +
|
hidden_field_tag(:authenticity_token, form_authenticity_token) +
|
||||||
hidden_field_tag("#{param}[client_data_json]", nil, id: nil, data: { passkey_field: "client_data_json" }) +
|
hidden_field_tag("#{param}[client_data_json]", nil, id: nil, data: { passkey_field: "client_data_json" }) +
|
||||||
hidden_field_tag("#{param}[attestation_object]", nil, id: nil, data: { passkey_field: "attestation_object" }) +
|
hidden_field_tag("#{param}[attestation_object]", nil, id: nil, data: { passkey_field: "attestation_object" }) +
|
||||||
hidden_field_tag("#{param}[transports][]", nil, id: nil, data: { passkey_field: "transports" }) +
|
hidden_field_tag("#{param}[transports][]", nil, id: nil, data: { passkey_field: "transports" }) +
|
||||||
tag.button(name, type: :button, data: { passkey: "create" }, **button_options)
|
tag.button(name, type: :button, data: { passkey: "create" }, **button_options)
|
||||||
end
|
end + passkey_error_messages(**error_options)
|
||||||
|
|
||||||
form_tag + passkey_error_messages(error_message, cancelled_message)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -51,37 +54,50 @@ module ActionPack::Passkey::FormHelper
|
|||||||
# Options:
|
# Options:
|
||||||
# - +options+: WebAuthn request options (JSON-serializable hash)
|
# - +options+: WebAuthn request options (JSON-serializable hash)
|
||||||
# - +challenge_url+: endpoint to refresh the challenge nonce
|
# - +challenge_url+: endpoint to refresh the challenge nonce
|
||||||
# - +param+: the form parameter namespace (default: +:passkey+)
|
|
||||||
# - +mediation+: WebAuthn mediation hint (e.g. +"conditional"+ for autofill-assisted sign in)
|
# - +mediation+: WebAuthn mediation hint (e.g. +"conditional"+ for autofill-assisted sign in)
|
||||||
# - +error_message+: message shown on ceremony failure
|
# - +form+: additional HTML attributes for the +<form>+ tag. Supports a +:param+ key
|
||||||
# - +cancelled_message+: message shown when ceremony is cancelled
|
# to set the form parameter namespace (default: +:passkey+)
|
||||||
# - +form+: additional HTML attributes for the +<form>+ tag
|
# - +error+: HTML attributes for the error message +<div>+. Supports a +:message+ key
|
||||||
|
# to override the default error text
|
||||||
|
# - +cancellation+: HTML attributes for the cancellation message +<div>+. Supports a
|
||||||
|
# +:message+ key to override the default cancellation text
|
||||||
# - All other options are passed to the +<button>+ tag
|
# - All other options are passed to the +<button>+ tag
|
||||||
def passkey_sign_in_button(name = nil, url = nil, options:, challenge_url: nil, param: :passkey, mediation: nil, error_message: "Something went wrong while signing in with your passkey.", cancelled_message: "Passkey sign in was cancelled. Try again when you are ready.", form: {}, **button_options, &block)
|
def passkey_sign_in_button(name = nil, url = nil, **options, &block)
|
||||||
url, name = name, block ? capture(&block) : nil if block_given?
|
url, name = name, block ? capture(&block) : nil if block_given?
|
||||||
form_options = form.reverse_merge(method: :post, action: url, class: "button_to")
|
component_options, form_options, button_options, error_options = extract_passkey_component_options(url, options)
|
||||||
|
error_options[:error][:message] ||= SIGN_IN_ERROR_MESSAGE
|
||||||
|
error_options[:cancellation][:message] ||= SIGN_IN_CANCELLED_MESSAGE
|
||||||
|
param = form_options.delete(:param)
|
||||||
|
|
||||||
component_options = {
|
content_tag("rails-passkey-sign-in-button", **component_options.transform_keys { |key| key.to_s.dasherize }) do
|
||||||
"request-options": options.to_json,
|
tag.form(**form_options) do
|
||||||
"challenge-url": challenge_url || default_passkey_challenge_url
|
|
||||||
}
|
|
||||||
component_options[:mediation] = mediation if mediation
|
|
||||||
|
|
||||||
tag.send(:"rails-passkey-sign-in-button", **component_options) do
|
|
||||||
form_tag = tag.form(**form_options) do
|
|
||||||
hidden_field_tag(:authenticity_token, form_authenticity_token) +
|
hidden_field_tag(:authenticity_token, form_authenticity_token) +
|
||||||
hidden_field_tag("#{param}[id]", nil, id: nil, data: { passkey_field: "id" }) +
|
hidden_field_tag("#{param}[id]", nil, id: nil, data: { passkey_field: "id" }) +
|
||||||
hidden_field_tag("#{param}[client_data_json]", nil, id: nil, data: { passkey_field: "client_data_json" }) +
|
hidden_field_tag("#{param}[client_data_json]", nil, id: nil, data: { passkey_field: "client_data_json" }) +
|
||||||
hidden_field_tag("#{param}[authenticator_data]", nil, id: nil, data: { passkey_field: "authenticator_data" }) +
|
hidden_field_tag("#{param}[authenticator_data]", nil, id: nil, data: { passkey_field: "authenticator_data" }) +
|
||||||
hidden_field_tag("#{param}[signature]", nil, id: nil, data: { passkey_field: "signature" }) +
|
hidden_field_tag("#{param}[signature]", nil, id: nil, data: { passkey_field: "signature" }) +
|
||||||
tag.button(name, type: :button, data: { passkey: "sign_in" }, **button_options)
|
tag.button(name, type: :button, data: { passkey: "sign_in" }, **button_options)
|
||||||
end
|
end + passkey_error_messages(**error_options)
|
||||||
|
|
||||||
form_tag + passkey_error_messages(error_message, cancelled_message)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def extract_passkey_component_options(url, options)
|
||||||
|
passkey_options = options.fetch(:options, {})
|
||||||
|
|
||||||
|
component_options = options
|
||||||
|
.slice(:challenge_url, :mediation)
|
||||||
|
.reverse_merge(challenge_url: default_passkey_challenge_url, options: passkey_options.to_json(except: :challenge))
|
||||||
|
form_options = options
|
||||||
|
.fetch(:form, {})
|
||||||
|
.reverse_merge(method: :post, action: url, class: "button_to", param: :passkey)
|
||||||
|
error_options = options.slice(:error, :cancellation).reverse_merge(error: {}, cancellation: {})
|
||||||
|
|
||||||
|
button_options = options.except(:options, :form, *component_options.keys, *error_options.keys)
|
||||||
|
|
||||||
|
[ component_options, form_options, button_options, error_options ]
|
||||||
|
end
|
||||||
|
|
||||||
def default_passkey_challenge_url
|
def default_passkey_challenge_url
|
||||||
if challenge_url = Rails.configuration.action_pack.passkey.challenge_url
|
if challenge_url = Rails.configuration.action_pack.passkey.challenge_url
|
||||||
instance_exec(&challenge_url)
|
instance_exec(&challenge_url)
|
||||||
@@ -90,8 +106,17 @@ module ActionPack::Passkey::FormHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def passkey_error_messages(error_message, cancelled_message)
|
def passkey_error_messages(error: {}, cancellation: {})
|
||||||
tag.p(error_message, data: { passkey_error: "error" }, class: "txt-negative") +
|
error_message = error[:message]
|
||||||
tag.p(cancelled_message, data: { passkey_error: "cancelled" }, class: "txt-subtle")
|
error_attributes = error.except(:message)
|
||||||
|
error_attributes[:data] ||= {}
|
||||||
|
error_attributes[:data][:passkey_error] = "error"
|
||||||
|
|
||||||
|
cancellation_message = cancellation[:message]
|
||||||
|
cancellation_attributes = cancellation.except(:message)
|
||||||
|
cancellation_attributes[:data] ||= {}
|
||||||
|
cancellation_attributes[:data][:passkey_error] = "cancelled"
|
||||||
|
|
||||||
|
tag.div(error_message, hidden: true, **error_attributes) + tag.div(cancellation_message, hidden: true, **cancellation_attributes)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class ActionPack::WebAuthn::PublicKeyCredential::CreationOptions < ActionPack::W
|
|||||||
|
|
||||||
# Returns a Hash suitable for JSON serialization and passing to the
|
# Returns a Hash suitable for JSON serialization and passing to the
|
||||||
# WebAuthn JavaScript API.
|
# WebAuthn JavaScript API.
|
||||||
def as_json(*)
|
def as_json(options = {})
|
||||||
json = {
|
json = {
|
||||||
challenge: challenge,
|
challenge: challenge,
|
||||||
rp: relying_party.as_json,
|
rp: relying_party.as_json,
|
||||||
@@ -96,7 +96,7 @@ class ActionPack::WebAuthn::PublicKeyCredential::CreationOptions < ActionPack::W
|
|||||||
json[:attestation] = attestation.to_s
|
json[:attestation] = attestation.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
json
|
json.as_json(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ class ActionPack::WebAuthn::PublicKeyCredential::RequestOptions < ActionPack::We
|
|||||||
|
|
||||||
# Returns a Hash suitable for JSON serialization and passing to the
|
# Returns a Hash suitable for JSON serialization and passing to the
|
||||||
# WebAuthn JavaScript API.
|
# WebAuthn JavaScript API.
|
||||||
def as_json(*)
|
def as_json(options = {})
|
||||||
{
|
{
|
||||||
challenge: challenge,
|
challenge: challenge,
|
||||||
rpId: relying_party.id,
|
rpId: relying_party.id,
|
||||||
allowCredentials: credentials.map { |credential| allow_credential_json(credential) },
|
allowCredentials: credentials.map { |credential| allow_credential_json(credential) },
|
||||||
userVerification: user_verification.to_s
|
userVerification: user_verification.to_s
|
||||||
}
|
}.as_json(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
Reference in New Issue
Block a user