use gloo_net::http::Request; use serde::{Deserialize, Serialize}; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew_router::prelude::*; /// Payload for creating the initial administrator account. /// /// This struct is sent to the `/api/setup-admin` endpoint to create the first admin user /// when no administrators exist in the system. It carries the necessary information /// for the new admin's profile and credentials. /// /// # Fields /// - `first_name`: The first name of the administrator. /// - `last_name`: The last name of the administrator. /// - `username`: The unique username for the administrator's login. /// - `pwd`: The password for the administrator's account. This will be hashed on the backend. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct AdminSetupScheme { pub first_name: String, pub last_name: String, pub username: String, pub pwd: String, } /// Component for the initial admin account setup page. /// /// This page is displayed when a fresh system has no administrator accounts. It provides /// a form to create the first admin user. Key functionality: /// /// - **Admin Check**: On mount, verifies if an admin already exists by calling `/api/check-admin`. /// If an admin is found, the user is redirected to the login page ([`crate::Route::Login`]). /// - **Form Fields**: Collects `first_name`, `last_name`, `username`, `password`, and `confirm_password`. /// - **Form Validation**: /// - Ensures password fields are not empty. /// - Verifies that `password` and `confirm_password` match. /// - Ensures the `username` field is not empty. /// - **API Interaction**: On form submission, a POST request is sent to `/api/setup-admin` /// with the new admin's details. /// - **Password Hashing**: The backend is responsible for hashing the password using Argon2 /// before storage; this component only sends the plain text password. /// - **Auto-redirect**: On successful admin account creation, the user is automatically /// redirected to the login page (`crate::Route::Login`). /// - **State Management**: Uses Yew's `use_state` hooks to manage: /// - Input field values (`first_name`, `last_name`, `username`, `pwd`, `pwd_confirm`). /// - UI states like `error` messages, `success` status, and `loading` indicators. /// - `admin_check_done` to prevent rendering the form before the initial admin check completes. /// /// # Example Flow /// 1. User navigates to `/setup`. /// 2. The component checks `/api/check-admin`. /// 3. If an admin exists, redirects to `/login`. /// 4. If no admin exists, the setup form is displayed. /// 5. User fills out the form and submits. /// 6. Form data is sent via POST to `/api/setup-admin`. /// 7. On successful response (HTTP 200), redirects to `/login`. /// 8. On error, displays an error message to the user. /// /// # Security Notes /// - The initial admin check prevents re-creating an admin if one already exists. /// - Password confirmation helps prevent user typos for critical credentials. /// - Backend validation further ensures non-empty and secure credentials. #[component(InitialAdminSetup)] pub fn initial_admin_setup() -> Html { let first_name = use_state(|| "".to_string()); let last_name = use_state(|| "".to_string()); let username = use_state(|| "".to_string()); let pwd = use_state(|| "".to_string()); let pwd_confirm = use_state(|| "".to_string()); let error = use_state(|| String::new()); let success = use_state(|| false); let loading = use_state(|| false); let admin_check_done = use_state(|| false); let navigator = use_navigator().unwrap(); { let admin_check_done = admin_check_done.clone(); let navigator = navigator.clone(); use_effect_with((), move |_| { let admin_check_done = admin_check_done.clone(); let navigator = navigator.clone(); spawn_local(async move { match Request::get("/api/check-admin").send().await { Ok(resp) if resp.status() == 200 => { if let Ok(data) = resp.json::().await { let has_admin = data["has_admin"].as_bool().unwrap_or(false); if has_admin { navigator.push(&crate::Route::Login); } else { admin_check_done.set(true); } } else { admin_check_done.set(true); } } _ => { admin_check_done.set(true); } } }); || () }); } if !*admin_check_done { return html! {
{ "Checking..." }
}; } let onsubmit = { let first_name = first_name.clone(); let last_name = last_name.clone(); let username = username.clone(); let pwd = pwd.clone(); let pwd_confirm = pwd_confirm.clone(); let error = error.clone(); let success = success.clone(); let loading = loading.clone(); let navigator = navigator.clone(); Callback::from(move |e: SubmitEvent| { e.prevent_default(); if (*pwd).is_empty() || (*pwd_confirm).is_empty() { error.set("Password fields cannot be empty".to_string()); return; } if *pwd != *pwd_confirm { error.set("Passwords do not match".to_string()); return; } if (*username).is_empty() { error.set("Username cannot be empty".to_string()); return; } let first_name_val = (*first_name).clone(); let last_name_val = (*last_name).clone(); let username_val = (*username).clone(); let pwd_val = (*pwd).clone(); loading.set(true); error.set(String::new()); success.set(false); let error = error.clone(); let success = success.clone(); let loading = loading.clone(); let navigator = navigator.clone(); spawn_local(async move { let payload = AdminSetupScheme { first_name: first_name_val, last_name: last_name_val, username: username_val, pwd: pwd_val, }; let response = Request::post("/api/setup-admin") .header("Content-Type", "application/json") .json(&payload) .unwrap() .send() .await; loading.set(false); match response { Ok(r) if r.status() == 200 => { success.set(true); navigator.push(&crate::Route::Login); } Ok(r) => { let text = r.text().await.unwrap_or_else(|_| "unknown".into()); error.set(format!("HTTP {}: {}", r.status(), text)); } Err(err) => error.set(format!("Network error: {}", err)), } }); }) }; html! {

{ "Initial Admin Setup" }

{ "Create your first administrator account" }

if !error.is_empty() {

{ (*error).clone() }

} if *success {

{ "Admin account created successfully! Redirecting to login..." }

}
} }