diff --git a/quotesdb/src/bin/ui/pages/admin.rs b/quotesdb/src/bin/ui/pages/admin.rs new file mode 100644 index 0000000..f4cfff1 --- /dev/null +++ b/quotesdb/src/bin/ui/pages/admin.rs @@ -0,0 +1,281 @@ +//! Admin page — manage submissions lock and reset auth codes. +//! +//! Provides a persistent admin auth code input, an auth code reset section, +//! and a submissions lock/unlock section. The lock state is fetched on mount +//! from `GET /api/status`. + +use crate::api::{self, ApiError}; +use wasm_bindgen_futures::spawn_local; +use web_sys::HtmlInputElement; +use yew::prelude::*; + +/// Admin page component. +/// +/// Holds all local admin state and renders three sections: +/// 1. A persistent admin auth code input shared by all actions. +/// 2. A "Reset auth code" section that calls `POST /api/admin/reset-auth-code`. +/// 3. A "Submissions" section that shows current lock state and allows toggling. +/// +/// The submissions lock state is fetched from `GET /api/status` on mount. +#[function_component(AdminPage)] +pub fn admin_page() -> Html { + // Persistent admin auth code used by all admin actions. + let admin_code = use_state(String::new); + // Optional new passphrase for the reset section. + let new_passphrase = use_state(String::new); + // Newly returned auth code after a successful reset. + let reset_result: UseStateHandle> = use_state(|| None); + // Error message for the reset section. + let reset_error: UseStateHandle> = use_state(|| None); + // Current submissions lock state, fetched on mount. + let submissions_locked: UseStateHandle> = use_state(|| None); + // Error message for the lock/unlock section. + let lock_error: UseStateHandle> = use_state(|| None); + // Disables buttons during in-flight requests. + let loading = use_state(|| false); + + // Fetch submission lock state on mount. + { + let submissions_locked = submissions_locked.clone(); + use_effect_with((), move |_| { + spawn_local(async move { + if let Ok(status) = api::get_status().await { + submissions_locked.set(Some(status.submissions_locked)); + } + }); + }); + } + + // --- Reset auth code handler --- + let on_reset = { + let admin_code = admin_code.clone(); + let new_passphrase = new_passphrase.clone(); + let reset_result = reset_result.clone(); + let reset_error = reset_error.clone(); + let loading = loading.clone(); + Callback::from(move |e: MouseEvent| { + e.prevent_default(); + if *loading { + return; + } + let code = (*admin_code).clone(); + let passphrase = (*new_passphrase).clone(); + let reset_result = reset_result.clone(); + let reset_error = reset_error.clone(); + let loading = loading.clone(); + + loading.set(true); + reset_result.set(None); + reset_error.set(None); + + spawn_local(async move { + let new_code_opt = if passphrase.is_empty() { + None + } else { + Some(passphrase.as_str()) + }; + match api::admin_reset_auth_code(&code, new_code_opt, &code).await { + Ok(new_code) => { + reset_result.set(Some(new_code)); + reset_error.set(None); + } + Err(ApiError::Forbidden) => { + reset_error.set(Some("Wrong auth code.".to_string())); + } + Err(e) => { + reset_error.set(Some(format!("Error: {e}"))); + } + } + loading.set(false); + }); + }) + }; + + // --- Lock submissions handler --- + let on_lock = { + let admin_code = admin_code.clone(); + let submissions_locked = submissions_locked.clone(); + let lock_error = lock_error.clone(); + let loading = loading.clone(); + Callback::from(move |e: MouseEvent| { + e.prevent_default(); + if *loading { + return; + } + let code = (*admin_code).clone(); + let submissions_locked = submissions_locked.clone(); + let lock_error = lock_error.clone(); + let loading = loading.clone(); + + loading.set(true); + lock_error.set(None); + + spawn_local(async move { + match api::admin_lock(&code).await { + Ok(_) => { + submissions_locked.set(Some(true)); + lock_error.set(None); + } + Err(ApiError::Forbidden) => { + lock_error.set(Some("Wrong auth code.".to_string())); + } + Err(e) => { + lock_error.set(Some(format!("Error: {e}"))); + } + } + loading.set(false); + }); + }) + }; + + // --- Unlock submissions handler --- + let on_unlock = { + let admin_code = admin_code.clone(); + let submissions_locked = submissions_locked.clone(); + let lock_error = lock_error.clone(); + let loading = loading.clone(); + Callback::from(move |e: MouseEvent| { + e.prevent_default(); + if *loading { + return; + } + let code = (*admin_code).clone(); + let submissions_locked = submissions_locked.clone(); + let lock_error = lock_error.clone(); + let loading = loading.clone(); + + loading.set(true); + lock_error.set(None); + + spawn_local(async move { + match api::admin_unlock(&code).await { + Ok(_) => { + submissions_locked.set(Some(false)); + lock_error.set(None); + } + Err(ApiError::Forbidden) => { + lock_error.set(Some("Wrong auth code.".to_string())); + } + Err(e) => { + lock_error.set(Some(format!("Error: {e}"))); + } + } + loading.set(false); + }); + }) + }; + + html! { +
+

{ "Admin" }

+ +
+ + +
+ +
+ +
+

{ "Reset auth code" }

+
+ + +
+
+ +
+ if let Some(new_code) = (*reset_result).clone() { +

+ { "New code: " } + { new_code } +

+ } + if let Some(err) = (*reset_error).clone() { +

{ err }

+ } +
+ +
+ +
+

{ "Submissions" }

+ { + match *submissions_locked { + None => html! {

{ "Loading status..." }

}, + Some(locked) => html! { + <> +

+ { "Status: " } + if locked { + { "Closed" } + } else { + { "Open" } + } +

+
+ if !locked { + + } else { + + } +
+ + }, + } + } + if let Some(err) = (*lock_error).clone() { +

{ err }

+ } +
+
+ } +} diff --git a/quotesdb/src/bin/ui/pages/mod.rs b/quotesdb/src/bin/ui/pages/mod.rs index 9e2bd45..b48ef69 100644 --- a/quotesdb/src/bin/ui/pages/mod.rs +++ b/quotesdb/src/bin/ui/pages/mod.rs @@ -2,6 +2,7 @@ //! //! Each page corresponds to a route in the [`crate::Route`] enum. +pub mod admin; pub mod author; pub mod browse; pub mod home;