fix(quotesdb): fix report submit button always disabled, add auth code hint

- Remove captcha-gated disabled state on report submit button — Turnstile
  does not fire a DOM input event so captcha_solved never became true; rate
  limiting is handled at the WAF layer so the gate was incorrect
- Token is still read from the hidden Turnstile input at submit time if present
- Add explanatory hint below "Enter Auth Code" heading in AuthModal explaining
  the code was provided or generated when the quote was created
- Add .auth-modal__hint CSS class for the hint text

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
quotesdb
Elijah Voigt 3 months ago
parent 1728141517
commit 66cbe67100

@ -54,6 +54,9 @@ pub fn auth_modal(props: &AuthModalProps) -> Html {
<div class="auth-modal__overlay">
<div class="auth-modal">
<h2 class="auth-modal__title">{ "Enter Auth Code" }</h2>
<p class="auth-modal__hint">
{ "This is the authorization code you were given (or that was generated) when you created this quote." }
</p>
<form onsubmit={onsubmit}>
<input
class="auth-modal__input"

@ -35,12 +35,14 @@ pub struct ReportModalProps {
/// Modal dialog for reporting a quote.
///
/// Renders a reason textarea, a Cloudflare Turnstile CAPTCHA widget, and
/// Submit / Cancel buttons. The Submit button is disabled until the Turnstile
/// widget has been solved (i.e., the hidden input carries a non-empty token).
/// Submit / Cancel buttons.
///
/// The Turnstile widget is rendered by injecting a `<div class="cf-turnstile">`
/// with the appropriate `data-sitekey` attribute. Turnstile's global JS (loaded
/// in `index.html`) picks up that div automatically.
/// in `index.html`) picks up that div automatically. The token is read from the
/// hidden `cf-turnstile-response` input at submit time and forwarded to the
/// caller; it may be empty if Turnstile has not yet solved (e.g., no network).
/// Rate limiting is enforced at the WAF layer rather than the application layer.
///
/// # Example
///
@ -55,8 +57,6 @@ pub struct ReportModalProps {
#[function_component(ReportModal)]
pub fn report_modal(props: &ReportModalProps) -> Html {
let reason = use_state(String::new);
// Whether the Turnstile CAPTCHA has been solved (token is non-empty).
let captcha_solved = use_state(|| false);
// --- Handle reason textarea input ---
let on_reason_input = {
@ -71,50 +71,6 @@ pub fn report_modal(props: &ReportModalProps) -> Html {
})
};
// --- Poll the hidden Turnstile input for a token on the "input" event ---
// Turnstile fires a synthetic "input" event on the hidden field when solved.
// We wire this up via use_effect so we can attach the listener to the DOM.
{
let captcha_solved = captcha_solved.clone();
use_effect(move || {
let window = web_sys::window().expect("no global window");
let document = window.document().expect("no document");
// Closure to check the hidden input value.
let captcha_solved_clone = captcha_solved.clone();
let check_fn = wasm_bindgen::closure::Closure::<dyn Fn()>::new(move || {
let document = web_sys::window()
.and_then(|w| w.document())
.expect("document");
if let Some(input_el) = document
.query_selector("input[name='cf-turnstile-response']")
.ok()
.flatten()
{
let input: HtmlInputElement = input_el.unchecked_into();
captcha_solved_clone.set(!input.value().is_empty());
}
});
// Attach the listener to the document so it catches the event
// regardless of where Turnstile fires it.
let _ = document
.add_event_listener_with_callback("input", check_fn.as_ref().unchecked_ref());
// Also run once immediately in case the widget already solved
// (e.g., page revisit).
check_fn
.as_ref()
.unchecked_ref::<js_sys::Function>()
.call0(&wasm_bindgen::JsValue::NULL)
.ok();
// Keep the closure alive until the component unmounts.
check_fn.forget();
|| ()
});
}
// --- Form submit handler ---
let on_submit = {
let reason = reason.clone();
@ -122,7 +78,7 @@ pub fn report_modal(props: &ReportModalProps) -> Html {
Callback::from(move |e: SubmitEvent| {
e.prevent_default();
// Read the Turnstile token from the hidden input.
// Read the Turnstile token from the hidden input if available.
let token = web_sys::window()
.and_then(|w| w.document())
.and_then(|doc| {
@ -133,10 +89,6 @@ pub fn report_modal(props: &ReportModalProps) -> Html {
.map(|el| el.unchecked_into::<HtmlInputElement>().value())
.unwrap_or_default();
if token.is_empty() {
return; // Should not happen — button is disabled until solved.
}
let reason_val = {
let s = (*reason).clone();
if s.is_empty() {
@ -151,7 +103,6 @@ pub fn report_modal(props: &ReportModalProps) -> Html {
};
let reason_len = reason.len();
let submit_disabled = !*captcha_solved;
let on_cancel = props.on_cancel.clone();
@ -193,7 +144,6 @@ pub fn report_modal(props: &ReportModalProps) -> Html {
<button
type="submit"
class="btn btn--danger"
disabled={submit_disabled}
>
{ "Submit Report" }
</button>

@ -480,6 +480,12 @@ code {
margin-bottom: 1rem;
}
.auth-modal__hint {
font-size: 0.85rem;
color: var(--color-muted);
margin-bottom: 1rem;
}
.auth-modal__input {
margin-bottom: 1rem;
}

Loading…
Cancel
Save