282 lines
12 KiB
Rust
282 lines
12 KiB
Rust
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::<serde_json::Value>().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! { <div>{ "Checking..." }</div> };
|
|
}
|
|
|
|
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! {
|
|
<div class="setup-container">
|
|
<div class="setup-box">
|
|
<h1>{ "Initial Admin Setup" }</h1>
|
|
<p>{ "Create your first administrator account" }</p>
|
|
|
|
<form {onsubmit} class="setup-form">
|
|
<div class="form-group">
|
|
<label for="first_name">{ "First Name:" }
|
|
<input
|
|
id="first_name"
|
|
type="text"
|
|
placeholder="First name"
|
|
value={(*first_name).clone()}
|
|
oninput={Callback::from(move |e: InputEvent| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
first_name.set(input.value());
|
|
})}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="last_name">{ "Last Name:" }
|
|
<input
|
|
id="last_name"
|
|
type="text"
|
|
placeholder="Last name"
|
|
value={(*last_name).clone()}
|
|
oninput={Callback::from(move |e: InputEvent| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
last_name.set(input.value());
|
|
})}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="username">{ "Username:" }
|
|
<input
|
|
id="username"
|
|
type="text"
|
|
placeholder="Username"
|
|
value={(*username).clone()}
|
|
oninput={Callback::from(move |e: InputEvent| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
username.set(input.value());
|
|
})}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">{ "Password:" }
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
placeholder="Password"
|
|
value={(*pwd).clone()}
|
|
oninput={Callback::from(move |e: InputEvent| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
pwd.set(input.value());
|
|
})}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="pwd_confirm">{ "Confirm Password:" }
|
|
<input
|
|
id="pwd_confirm"
|
|
type="password"
|
|
placeholder="Confirm password"
|
|
value={(*pwd_confirm).clone()}
|
|
oninput={Callback::from(move |e: InputEvent| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
pwd_confirm.set(input.value());
|
|
})}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<button type="submit" disabled={*loading} class="submit-btn">
|
|
{ if *loading { "Creating..." } else { "Create Admin Account" } }
|
|
</button>
|
|
|
|
if !error.is_empty() {
|
|
<p class="error-message" style="color:red">{ (*error).clone() }</p>
|
|
}
|
|
|
|
if *success {
|
|
<p class="success-message" style="color:green">{ "Admin account created successfully! Redirecting to login..." }</p>
|
|
}
|
|
</form>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|