1use gloo_net::http::Request;
2use serde::{Deserialize, Serialize};
3use wasm_bindgen_futures::spawn_local;
4use yew::prelude::*;
5use yew_router::prelude::*;
6
7#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
29pub struct UserCreateScheme {
30 pub first_name: String,
31 pub last_name: String,
32 pub username: String,
33 pub is_admin: bool,
34 pub pwd: String,
35}
36
37#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
45pub struct LoginScheme {
46 pub username: String,
47 pub pwd: String,
48}
49
50#[derive(Deserialize, Serialize, Debug)]
63pub struct UserUpdateScheme {
64 pub id: i16,
65 pub first_name: String,
66 pub last_name: String,
67 pub username: String,
68 pub make_admin: bool,
69 pub new_pwd: String,
70}
71
72#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
85pub struct FilteredUser {
86 pub id: i16,
87 pub first_name: String,
88 pub last_name: String,
89 pub username: String,
90 pub is_admin: bool,
91}
92
93#[derive(Properties, PartialEq)]
101pub struct UserProps {
102 pub id: i16,
103}
104
105#[derive(Deserialize, Debug)]
114struct ApiError {
115 message: String,
116 _status: String,
117}
118
119#[component(Register)]
145pub fn register_component() -> Html {
146 let first_name = use_state(|| "".to_string());
147 let last_name = use_state(|| "".to_string());
148 let username = use_state(|| "".to_string());
149 let is_admin = use_state(|| false);
150 let pwd = use_state(|| "".to_string());
151 let status = use_state(|| None::<String>);
152
153 let onsubmit = {
154 let first_name = first_name.clone();
155 let last_name = last_name.clone();
156 let username = username.clone();
157 let is_admin = is_admin.clone();
158 let pwd = pwd.clone();
159 let status = status.clone();
160
161 Callback::from(move |e: SubmitEvent| {
162 e.prevent_default();
163 let first_name = (*first_name).clone();
164 let last_name = (*last_name).clone();
165 let username = (*username).clone();
166 let is_admin = *is_admin;
167 let pwd = (*pwd).clone();
168 let status = status.clone();
169
170 spawn_local(async move {
171 let payload = UserCreateScheme {
172 first_name,
173 last_name,
174 username,
175 is_admin,
176 pwd,
177 };
178
179 let request = Request::post("/api/register")
180 .json(&payload)
181 .expect("Error building request");
182
183 match request.send().await {
184 Ok(response) if response.status() == 200 => status.set(Some("Erfolg".into())),
185 Ok(response) => {
186 let text = response.text().await.unwrap_or_else(|_| "Unbekannt".into());
187 status.set(Some(text));
188 },
189 Err(err) => status.set(Some(format!("Netzwerkfehler: {}", err))),
190 }
191 });
192 })
193 };
194
195 let fn_change = {
196 let first_name = first_name.clone();
197 Callback::from(move |e: InputEvent| {
198 let input: web_sys::HtmlInputElement = e.target_unchecked_into();
199 first_name.set(input.value());
200 })
201 };
202
203 let ln_change = {
204 let last_name = last_name.clone();
205 Callback::from(move |e: InputEvent| {
206 let input: web_sys::HtmlInputElement = e.target_unchecked_into();
207 last_name.set(input.value());
208 })
209 };
210
211 let un_change = {
212 let username = username.clone();
213 Callback::from(move |e: InputEvent| {
214 let input: web_sys::HtmlInputElement = e.target_unchecked_into();
215 username.set(input.value());
216 })
217 };
218
219 let admin_change = {
220 let is_admin = is_admin.clone();
221 Callback::from(move |e: Event| {
222 let input: web_sys::HtmlInputElement = e.target_unchecked_into();
223 is_admin.set(input.checked());
224 })
225 };
226
227 let pwd_change = {
228 let pwd = pwd.clone();
229 Callback::from(move |e: InputEvent| {
230 let input: web_sys::HtmlInputElement = e.target_unchecked_into();
231 pwd.set(input.value());
232 })
233 };
234
235 html! {
236 <form {onsubmit} class="rundbg">
237 <label>{ "Vorname:" }
238 <input type="text" value={(*first_name).clone()} oninput={fn_change}/>
239 </label>
240 <label>{ "Nachname:" }
241 <input type="text" value={(*last_name).clone()} oninput={ln_change}/>
242 </label>
243 <label>{ "Benutzername:" }
244 <input type="text" value={(*username).clone()} oninput={un_change}/>
245 </label>
246 <label>{ "Admin:" }
247 <input type="checkbox" checked={*is_admin} onchange={admin_change}/>
248 </label>
249 <label>{ "Password:" }
250 <input type="password" value={(*pwd).clone()} oninput={pwd_change}/>
251 </label>
252 <button type="submit">{ "Bestätigen" }</button>
253 </form>
254 }
255}
256
257#[component(Login)]
284pub fn login_component() -> Html {
285 let username = use_state(|| "".to_string());
286 let pwd = use_state(|| "".to_string());
287 let loading = use_state(|| false);
288 let error = use_state(|| String::new());
289 let success = use_state(|| false);
290 let navigator = use_navigator().unwrap();
291
292 let onsubmit = {
293 let username = username.clone();
294 let pwd = pwd.clone();
295 let loading = loading.clone();
296 let error = error.clone();
297 let success = success.clone();
298 let navigator = navigator.clone();
299
300 Callback::from(move |e: SubmitEvent| {
301 e.prevent_default();
302 let username = (*username).clone();
303 let pwd = (*pwd).clone();
304 let loading = loading.clone();
305 let error = error.clone();
306 let success = success.clone();
307 let navigator = navigator.clone();
308
309 loading.set(true);
310 error.set(String::new());
311 success.set(false);
312
313 spawn_local(async move {
314 let payload = LoginScheme { username, pwd };
315
316 let response = Request::post("/api/login")
317 .header("Content-Type", "application/json")
318 .credentials(web_sys::RequestCredentials::Include)
319 .json(&payload)
320 .unwrap()
321 .send()
322 .await;
323
324 loading.set(false);
325
326 match response {
327 Ok(r) if r.status() == 200 => {
328 success.set(true);
329 navigator.push(&crate::Route::Home);
330 }
331 Ok(r) => {
332 let text = r.text().await.unwrap_or_else(|_| "Unbekannt".into());
333 error.set(text);
334 }
335 Err(err) => error.set(format!("Netzwerkfehler: {}", err)),
336 }
337 });
338 })
339 };
340
341 html! {
342 <div>
343 <h1 class="headline">{ "Anmeldung" }</h1>
344 <form {onsubmit} class="rundbg login">
345 <input
346 placeholder="Benutzername"
347 value={(*username).clone()}
348 oninput={Callback::from(move |e: InputEvent| {
349 let input: web_sys::HtmlInputElement = e.target_unchecked_into();
350 username.set(input.value());
351 })}
352 />
353 <input
354 type="password"
355 placeholder="Passwort"
356 value={(*pwd).clone()}
357 oninput={Callback::from(move |e: InputEvent| {
358 let input: web_sys::HtmlInputElement = e.target_unchecked_into();
359 pwd.set(input.value());
360 })}
361 />
362 <button type="submit" disabled={*loading}>{ if *loading { "Anmeldung läuft..." } else { "Anmelden" } }</button>
363 if !error.is_empty() { <p style="color:red">{(*error).clone()}</p> }
364 </form>
365 </div>
366 }
367}
368
369#[component(AllUsers)]
397pub fn all_users_component() -> Html {
398 let users = use_state(|| Vec::<FilteredUser>::new());
399 let error = use_state(|| None::<String>);
400 let loading = use_state(|| false);
401
402 {
403 let users = users.clone();
404 let error = error.clone();
405 let loading = loading.clone();
406
407 use_effect_with((), move |_| {
408 loading.set(true);
409 spawn_local(async move {
410 let url = format!("/api/users");
411 match Request::get(&url).send().await {
412 Ok(response) if response.status() == 200 => {
413 match response.json::<Vec<FilteredUser>>().await {
414 Ok(u) => users.set(u),
415 Err(err) => error.set(Some(format!("Parse error: {}", err))),
416 }
417 }
418 Ok(response) => {
419 if let Ok(text) = response.text().await {
420 error.set(Some(text));
421 } else {
422 error.set(Some(format!("status {}", response.status())));
423 }
424 }
425 Err(err) => error.set(Some(format!("Network error: {}", err))),
426 }
427 loading.set(false);
428 });
429 || ()
430 });
431 }
432
433 if *loading {
434 html! {<p>{ "Wird geladen" }</p>}
435 } else if let Some(e) = &*error {
436 html! { <p>{ format!("Fehler: {}", e) }</p> }
437 } else {
438 html! {
439 <ul class="postits">
440 { for users.iter().map(|t| html! {
441 <li key={t.id.to_string()} class="listbox">
442 <Link<crate::Route> to={crate::Route::UserByID{id: t.id}}><h3>{ format!("{} {}", t.first_name, t.last_name) }</h3></Link<crate::Route>>
443 </li>
444 })}
445 </ul>
446 }
447 }
448}
449
450#[component(UserByID)]
488pub fn user_by_id_component(props: &UserProps) -> Html {
489 let user = use_state(|| None::<FilteredUser>);
490 let error = use_state(|| None::<String>);
491 let loading = use_state(|| false);
492 let id = props.id;
493
494 {
495 let user = user.clone();
496 let error = error.clone();
497 let loading = loading.clone();
498
499 use_effect_with(id, move |id_ref| {
500 loading.set(true);
501 let user = user.clone();
502 let error = error.clone();
503 let id = *id_ref;
504 spawn_local(async move {
505 let url = format!("/api/users/{}", id);
506
507 match Request::get(&url).send().await {
508 Ok(response) => {
509 let status = response.status();
510 if status == 200 {
511 match response.json::<FilteredUser>().await {
512 Ok(u) => user.set(Some(u)),
513 Err(err) => error.set(Some(format!("Parse error: {}", err))),
514 }
515 } else {
516 match response.json::<ApiError>().await {
517 Ok(ae) => error.set(Some(ae.message)),
518 Err(_) => {
519 if let Ok(text) = response.text().await {
520 error.set(Some(text));
521 } else {
522 error.set(Some(format!("Server error: {}", status)));
523 }
524 }
525 }
526 }
527 }
528 Err(err) => error.set(Some(format!("Network error: {}", err))),
529 }
530 loading.set(false);
531 });
532 || ()
533 });
534 }
535
536 let first_name = use_state(|| "".to_string());
537 let last_name = use_state(|| "".to_string());
538 let username = use_state(|| "".to_string());
539 let make_admin = use_state(|| false);
540 let new_pwd = use_state(|| String::new());
541 let saving = use_state(|| false);
542 let save_error = use_state(|| None::<String>);
543 let save_success = use_state(|| false);
544
545 {
546 let user = user.clone();
547 let first_name = first_name.clone();
548 let last_name = last_name.clone();
549 let username = username.clone();
550 let make_admin = make_admin.clone();
551
552 use_effect_with(user, move |user_ref| {
553 if let Some(u) = &**user_ref {
554 first_name.set(u.first_name.clone());
555 last_name.set(u.last_name.clone());
556 username.set(u.username.clone());
557 make_admin.set(u.is_admin);
558 }
559 || ()
560 });
561 }
562
563 let onsubmit = {
564 let first_name = first_name.clone();
565 let last_name = last_name.clone();
566 let username = username.clone();
567 let make_admin = make_admin.clone();
568 let new_pwd = new_pwd.clone();
569 let saving = saving.clone();
570 let save_error = save_error.clone();
571 let save_success = save_success.clone();
572 let user_state = user.clone();
573
574 Callback::from(move |e: SubmitEvent| {
575 e.prevent_default();
576 let first_name = (*first_name).clone();
577 let last_name = (*last_name).clone();
578 let username = (*username).clone();
579 let make_admin = *make_admin;
580 let new_pwd_state = new_pwd.clone();
581 let new_pwd = (*new_pwd).clone();
582
583 saving.set(true);
584 save_error.set(None);
585 save_success.set(false);
586
587 let saving = saving.clone();
588 let save_error = save_error.clone();
589 let save_success = save_success.clone();
590 let user_state = user_state.clone();
591 let id = id;
592
593 spawn_local(async move {
594 let payload = UserUpdateScheme {
595 id: id,
596 first_name,
597 last_name,
598 username,
599 make_admin,
600 new_pwd,
601 };
602
603 let url = format!("/api/users/{}", id);
604 let request = Request::patch(&url)
605 .header("Content-Type", "application/json")
606 .credentials(web_sys::RequestCredentials::Include)
607 .json(&payload)
608 .expect("Failed to construct Request");
609
610 match request.send().await {
611 Ok(resp) if resp.status() == 200 => {
612 if let Ok(updated) = resp.json::<FilteredUser>().await {
613 user_state.set(Some(updated));
614 }
615 new_pwd_state.set(String::new());
616 save_success.set(true);
617 }
618 Ok(resp) => {
619 let txt = resp.text().await.unwrap_or_else(|_| "Unknown".into());
620 save_error.set(Some(format!("HTTP {}: {}", resp.status(), txt)));
621 }
622 Err(err) => save_error.set(Some(format!("Network error: {}", err))),
623 }
624 saving.set(false);
625 });
626 })
627 };
628
629 let deleting = use_state(|| false);
630 let delete_error = use_state(|| None::<String>);
631
632 let ondelete = {
633 let deleting = deleting.clone();
634 let delete_error = delete_error.clone();
635 let user_state = user.clone(); let id = id;
637
638 Callback::from(move |e: MouseEvent| {
639 e.prevent_default();
640 if !web_sys::window()
642 .and_then(|w| {
643 w.confirm_with_message("Möchten Sie diesen Eintrag wirklich löschen?")
644 .ok()
645 })
646 .unwrap_or(false)
647 {
648 return;
649 }
650
651 deleting.set(true);
652 delete_error.set(None);
653
654 let deleting = deleting.clone();
655 let delete_error = delete_error.clone();
656 let user_state = user_state.clone();
657
658 spawn_local(async move {
659 let url = format!("/api/users/{}", id);
660 let req = Request::delete(&url).credentials(web_sys::RequestCredentials::Include);
661
662 match req.send().await {
663 Ok(resp) if resp.status() == 200 || resp.status() == 204 => {
664 user_state.set(None);
665 }
666 Ok(resp) => {
667 let txt = resp.text().await.unwrap_or_else(|_| "Unbekannt".into());
668 delete_error.set(Some(txt));
669 }
670 Err(err) => delete_error.set(Some(format!("Netzwerkfehler: {}", err))),
671 }
672 deleting.set(false);
673 });
674 })
675 };
676
677 if *loading {
678 html! {<p>{ "Wird geladen" }</p>}
679 } else if let Some(e) = &*error {
680 html! { <p>{ format!("Fehler: {}", e) }</p> }
681 } else if let Some(u) = &*user {
682 html! {
683 <div class="rundbg">
684 <div>
685 <p><strong>{ "Vorname: " }</strong>{ &u.first_name }</p>
686 <p><strong>{ "Nachname: " }</strong>{ &u.last_name }</p>
687 <p><strong>{ "Benutzername: " }</strong>{ &u.username }</p>
688 <p><strong>{ "Ist Admin: " }</strong>{ u.is_admin }</p>
689 </div>
690
691 <h1>{ format!("User #{}", u.id) }</h1>
692 <form onsubmit={onsubmit}>
693 <div>
694 <label>{ "Vorname" }
695 <input name="first_name"
696 value={(*first_name).clone()}
697 oninput={Callback::from(move |e: InputEvent| {
698 let input: web_sys::HtmlInputElement = e.target_unchecked_into();
699 first_name.set(input.value());
700 })}
701 />
702 </label>
703 </div>
704 <div>
705 <label>{ "Nachname" }
706 <input name="last_name"
707 value={(*last_name).clone()}
708 oninput={Callback::from(move |e: InputEvent| {
709 let input: web_sys::HtmlInputElement = e.target_unchecked_into();
710 last_name.set(input.value());
711 })}
712 />
713 </label>
714 </div>
715 <div>
716 <label>{ "Benutzername" }
717 <input name="username"
718 value={(*username).clone()}
719 oninput={Callback::from(move |e: InputEvent| {
720 let input: web_sys::HtmlInputElement = e.target_unchecked_into();
721 username.set(input.value());
722 })}
723 />
724 </label>
725 </div>
726 <div>
727 <label>{ "Ist Admin" }
728 <input type="checkbox" name="make_admin" checked={*make_admin} onchange={Callback::from(move |e: Event| {
729 let input: web_sys::HtmlInputElement = e.target_unchecked_into();
730 make_admin.set(input.checked());
731 })}/>
732 </label>
733 </div>
734 <div>
735 <label>{ "Neues Passwort (leer = unchanged)" }
736 <input name="new_pwd" type="password"
737 value={(*new_pwd).clone()}
738 oninput={Callback::from(move |e: InputEvent| {
739 let input: web_sys::HtmlInputElement = e.target_unchecked_into();
740 new_pwd.set(input.value());
741 })}
742 />
743 </label>
744 </div>
745
746 <button type="submit" disabled={*saving}>{ if *saving { "Speichern..." } else { "Speichern" } }</button>
747 if *save_success {
748 <p style="color:green">{ "Erfolgreich aktualisiert" }</p>
749 }
750 if let Some(err) = &*save_error {
751 <p style="color:red">{ err.clone() }</p>
752 }
753 </form>
754
755 <button onclick={ondelete} disabled={*deleting}>
756 {if *deleting {"Löschen..."} else {"Löschen"}}
757 </button>
758
759 <Link<crate::Route> to={crate::Route::AllUsers}>{ "Zurück zur Benutzerübersicht" }</Link<crate::Route>>
760 if let Some(err) = &*delete_error {
761 <p style="color:red">{ err.clone() }</p>
762 }
763 </div>
764 }
765 } else {
766 html! { <p>{ "Kein Benutzer gefunden." }</p> }
767 }
768}