Frontend initial admin setup

This commit is contained in:
2026-05-09 17:33:24 +02:00
parent 26ac32db21
commit 8ddfe2ba14
3 changed files with 287 additions and 1 deletions

View File

@@ -2,6 +2,8 @@ mod auth;
mod pages;
use crate::auth::ProtectedRoute;
use crate::pages::*;
use gloo_net::http::Request;
use wasm_bindgen_futures::spawn_local;
use yew::prelude::*;
use yew_router::prelude::*;
@@ -19,6 +21,8 @@ enum Route {
Register,
#[at("/login")]
Login,
#[at("/setup")]
Setup,
#[at("/users")]
AllUsers,
#[at("/users/:id")]
@@ -49,6 +53,47 @@ fn sidebar_shell(props: &SidebarShellProps) -> Html {
}
}
#[derive(Properties, PartialEq)]
pub struct AdminCheckWrapperProps {
pub children: Children,
}
#[component(AdminCheckWrapper)]
fn admin_check_wrapper(props: &AdminCheckWrapperProps) -> Html {
let admin_exists = use_state(|| None::<bool>);
let navigator = use_navigator().unwrap();
{
let admin_exists = admin_exists.clone();
use_effect_with((), move |_| {
let admin_exists = admin_exists.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);
admin_exists.set(Some(has_admin));
}
}
_ => {
admin_exists.set(Some(false));
}
}
});
|| ()
});
}
match *admin_exists {
None => html! { <div>{ "Loading..." }</div> },
Some(false) => {
navigator.push(&Route::Setup);
html! { <div>{ "Redirecting to setup..." }</div> }
}
Some(true) => props.children.clone().into(),
}
}
fn switch(route: Route) -> Html {
match route {
Route::Home => html! {
@@ -87,7 +132,14 @@ fn switch(route: Route) -> Html {
</SidebarShell>
</ProtectedRoute>
},
Route::Login => html! { <user::Login/> },
Route::Login => html! {
<AdminCheckWrapper>
<user::Login/>
</AdminCheckWrapper>
},
Route::Setup => html! {
<setup::InitialAdminSetup/>
},
Route::AllUsers => html! {
<ProtectedRoute admin_page={true}>
<SidebarShell>

View File

@@ -1,4 +1,5 @@
pub mod basic_pages;
pub mod setup;
pub mod sidebar;
pub mod ticket;
pub mod user;

233
frontend/src/pages/setup.rs Normal file
View File

@@ -0,0 +1,233 @@
use gloo_net::http::Request;
use serde::{Deserialize, Serialize};
use wasm_bindgen_futures::spawn_local;
use yew::prelude::*;
use yew_router::prelude::*;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AdminSetupScheme {
pub first_name: String,
pub last_name: String,
pub username: String,
pub pwd: String,
}
#[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>
}
}