frontend/pages/
setup.rs

1use gloo_net::http::Request;
2use serde::{Deserialize, Serialize};
3use wasm_bindgen_futures::spawn_local;
4use yew::prelude::*;
5use yew_router::prelude::*;
6
7/// Payload for creating the initial administrator account.
8///
9/// This struct is sent to the `/api/setup-admin` endpoint to create the first admin user
10/// when no administrators exist in the system. It carries the necessary information
11/// for the new admin's profile and credentials.
12///
13/// # Fields
14/// - `first_name`: The first name of the administrator.
15/// - `last_name`: The last name of the administrator.
16/// - `username`: The unique username for the administrator's login.
17/// - `pwd`: The password for the administrator's account. This will be hashed on the backend.
18#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
19pub struct AdminSetupScheme {
20    pub first_name: String,
21    pub last_name: String,
22    pub username: String,
23    pub pwd: String,
24}
25
26/// Component for the initial admin account setup page.
27///
28/// This page is displayed when a fresh system has no administrator accounts. It provides
29/// a form to create the first admin user. Key functionality:
30///
31/// - **Admin Check**: On mount, verifies if an admin already exists by calling `/api/check-admin`.
32///   If an admin is found, the user is redirected to the login page ([`crate::Route::Login`]).
33/// - **Form Fields**: Collects `first_name`, `last_name`, `username`, `password`, and `confirm_password`.
34/// - **Form Validation**:
35///   - Ensures password fields are not empty.
36///   - Verifies that `password` and `confirm_password` match.
37///   - Ensures the `username` field is not empty.
38/// - **API Interaction**: On form submission, a POST request is sent to `/api/setup-admin`
39///   with the new admin's details.
40/// - **Password Hashing**: The backend is responsible for hashing the password using Argon2
41///   before storage; this component only sends the plain text password.
42/// - **Auto-redirect**: On successful admin account creation, the user is automatically
43///   redirected to the login page (`crate::Route::Login`).
44/// - **State Management**: Uses Yew's `use_state` hooks to manage:
45///   - Input field values (`first_name`, `last_name`, `username`, `pwd`, `pwd_confirm`).
46///   - UI states like `error` messages, `success` status, and `loading` indicators.
47///   - `admin_check_done` to prevent rendering the form before the initial admin check completes.
48///
49/// # Example Flow
50/// 1. User navigates to `/setup`.
51/// 2. The component checks `/api/check-admin`.
52/// 3. If an admin exists, redirects to `/login`.
53/// 4. If no admin exists, the setup form is displayed.
54/// 5. User fills out the form and submits.
55/// 6. Form data is sent via POST to `/api/setup-admin`.
56/// 7. On successful response (HTTP 200), redirects to `/login`.
57/// 8. On error, displays an error message to the user.
58///
59/// # Security Notes
60/// - The initial admin check prevents re-creating an admin if one already exists.
61/// - Password confirmation helps prevent user typos for critical credentials.
62/// - Backend validation further ensures non-empty and secure credentials.
63#[component(InitialAdminSetup)]
64pub fn initial_admin_setup() -> Html {
65    let first_name = use_state(|| "".to_string());
66    let last_name = use_state(|| "".to_string());
67    let username = use_state(|| "".to_string());
68    let pwd = use_state(|| "".to_string());
69    let pwd_confirm = use_state(|| "".to_string());
70    let error = use_state(|| String::new());
71    let success = use_state(|| false);
72    let loading = use_state(|| false);
73    let admin_check_done = use_state(|| false);
74    let navigator = use_navigator().unwrap();
75
76    {
77        let admin_check_done = admin_check_done.clone();
78        let navigator = navigator.clone();
79        use_effect_with((), move |_| {
80            let admin_check_done = admin_check_done.clone();
81            let navigator = navigator.clone();
82            spawn_local(async move {
83                match Request::get("/api/check-admin").send().await {
84                    Ok(resp) if resp.status() == 200 => {
85                        if let Ok(data) = resp.json::<serde_json::Value>().await {
86                            let has_admin = data["has_admin"].as_bool().unwrap_or(false);
87                            if has_admin {
88                                navigator.push(&crate::Route::Login);
89                            } else {
90                                admin_check_done.set(true);
91                            }
92                        } else {
93                            admin_check_done.set(true);
94                        }
95                    }
96                    _ => {
97                        admin_check_done.set(true);
98                    }
99                }
100            });
101            || ()
102        });
103    }
104
105    if !*admin_check_done {
106        return html! { <div>{ "Wird überprüft..." }</div> };
107    }
108
109    let onsubmit = {
110        let first_name = first_name.clone();
111        let last_name = last_name.clone();
112        let username = username.clone();
113        let pwd = pwd.clone();
114        let pwd_confirm = pwd_confirm.clone();
115        let error = error.clone();
116        let success = success.clone();
117        let loading = loading.clone();
118        let navigator = navigator.clone();
119
120        Callback::from(move |e: SubmitEvent| {
121            e.prevent_default();
122
123            if (*pwd).is_empty() || (*pwd_confirm).is_empty() {
124                error.set("Passwortfelder dürfen nicht leer sein".to_string());
125                return;
126            }
127
128            if *pwd != *pwd_confirm {
129                error.set("Passwörter stimmen nicht überein".to_string());
130                return;
131            }
132
133            if (*username).is_empty() {
134                error.set("Benutzername darf nicht leer sein".to_string());
135                return;
136            }
137
138            let first_name_val = (*first_name).clone();
139            let last_name_val = (*last_name).clone();
140            let username_val = (*username).clone();
141            let pwd_val = (*pwd).clone();
142
143            loading.set(true);
144            error.set(String::new());
145            success.set(false);
146
147            let error = error.clone();
148            let success = success.clone();
149            let loading = loading.clone();
150            let navigator = navigator.clone();
151
152            spawn_local(async move {
153                let payload = AdminSetupScheme {
154                    first_name: first_name_val,
155                    last_name: last_name_val,
156                    username: username_val,
157                    pwd: pwd_val,
158                };
159
160                let response = Request::post("/api/setup-admin")
161                    .header("Content-Type", "application/json")
162                    .json(&payload)
163                    .unwrap()
164                    .send()
165                    .await;
166
167                loading.set(false);
168
169                match response {
170                    Ok(r) if r.status() == 200 => {
171                        success.set(true);
172                        navigator.push(&crate::Route::Login);
173                    }
174                    Ok(r) => {
175                        let text = r.text().await.unwrap_or_else(|_| "Unbekannt".into());
176                        error.set(text);
177                    }
178                    Err(err) => error.set(format!("Netzwerkfehler: {}", err)),
179                }
180            });
181        })
182    };
183
184    html! {
185        <div class="setup-container rundbg">
186            <div class="setup-box">
187                <h1>{ "Admin-Einrichtung" }</h1>
188                <p>{ "Erstellen Sie Ihr erstes Administratorkonto" }</p>
189
190                <form {onsubmit} class="setup-form">
191                    <div class="form-group">
192                        <label for="first_name">{ "Vorname:" }
193                            <input
194                                id="first_name"
195                                type="text"
196                                placeholder="Vorname"
197                                value={(*first_name).clone()}
198                                oninput={Callback::from(move |e: InputEvent| {
199                                    let input: web_sys::HtmlInputElement = e.target_unchecked_into();
200                                    first_name.set(input.value());
201                                })}
202                            />
203                        </label>
204                    </div>
205
206                    <div class="form-group">
207                        <label for="last_name">{ "Nachname:" }
208                            <input
209                                id="last_name"
210                                type="text"
211                                placeholder="Nachname"
212                                value={(*last_name).clone()}
213                                oninput={Callback::from(move |e: InputEvent| {
214                                    let input: web_sys::HtmlInputElement = e.target_unchecked_into();
215                                    last_name.set(input.value());
216                                })}
217                            />
218                        </label>
219                    </div>
220
221                    <div class="form-group">
222                        <label for="username">{ "Benutzername:" }
223                            <input
224                                id="username"
225                                type="text"
226                                placeholder="Benutzername"
227                                value={(*username).clone()}
228                                oninput={Callback::from(move |e: InputEvent| {
229                                    let input: web_sys::HtmlInputElement = e.target_unchecked_into();
230                                    username.set(input.value());
231                                })}
232                            />
233                        </label>
234                    </div>
235
236                    <div class="form-group">
237                        <label for="password">{ "Passwort:" }
238                            <input
239                                id="password"
240                                type="password"
241                                placeholder="Passwort"
242                                value={(*pwd).clone()}
243                                oninput={Callback::from(move |e: InputEvent| {
244                                    let input: web_sys::HtmlInputElement = e.target_unchecked_into();
245                                    pwd.set(input.value());
246                                })}
247                            />
248                        </label>
249                    </div>
250
251                    <div class="form-group">
252                        <label for="pwd_confirm">{ "Passwort bestätigen:" }
253                            <input
254                                id="pwd_confirm"
255                                type="password"
256                                placeholder="Passwort bestätigen"
257                                value={(*pwd_confirm).clone()}
258                                oninput={Callback::from(move |e: InputEvent| {
259                                    let input: web_sys::HtmlInputElement = e.target_unchecked_into();
260                                    pwd_confirm.set(input.value());
261                                })}
262                            />
263                        </label>
264                    </div>
265
266                    <button type="submit" disabled={*loading} class="submit-btn">
267                        { if *loading { "Wird erstellt..." } else { "Administratorkonto erstellen" } }
268                    </button>
269
270                    if !error.is_empty() {
271                        <p class="error-message" style="color:red">{ (*error).clone() }</p>
272                    }
273
274                    if *success {
275                        <p class="success-message" style="color:green">{ "Administratorkonto erfolgreich erstellt! Wird weitergeleitet zum Login..." }</p>
276                    }
277                </form>
278            </div>
279        </div>
280    }
281}