590 lines
21 KiB
Rust
590 lines
21 KiB
Rust
use std::net::ToSocketAddrs;
|
|
|
|
use gloo_net::http::Request;
|
|
use serde::{Deserialize, Serialize};
|
|
use wasm_bindgen_futures::spawn_local;
|
|
use yew::prelude::*;
|
|
use yew_router::prelude::use_navigator;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct User {
|
|
pub id: i16,
|
|
pub last_name: String,
|
|
pub first_name: String,
|
|
pub username: String,
|
|
pub is_admin: bool,
|
|
pub pwd: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct UserCreateScheme {
|
|
pub first_name: String,
|
|
pub last_name: String,
|
|
pub username: String,
|
|
pub is_admin: bool,
|
|
pub pwd: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct LoginScheme {
|
|
pub username: String,
|
|
pub pwd: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug)]
|
|
pub struct UserUpdateScheme {
|
|
pub id: i16,
|
|
pub first_name: String,
|
|
pub last_name: String,
|
|
pub username: String,
|
|
pub make_admin: bool,
|
|
pub new_pwd: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct FilteredUser {
|
|
pub id: i16,
|
|
pub first_name: String,
|
|
pub last_name: String,
|
|
pub username: String,
|
|
pub is_admin: bool,
|
|
}
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct UserProps {
|
|
pub id: i16,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct ApiError {
|
|
message: String,
|
|
_status: String,
|
|
}
|
|
|
|
#[component(Register)]
|
|
pub fn register_component() -> Html {
|
|
let first_name = use_state(|| "".to_string());
|
|
let last_name = use_state(|| "".to_string());
|
|
let username = use_state(|| "".to_string());
|
|
let is_admin = use_state(|| false);
|
|
let pwd = use_state(|| "".to_string());
|
|
let status = use_state(|| None::<String>);
|
|
|
|
let onsubmit = {
|
|
let first_name = first_name.clone();
|
|
let last_name = last_name.clone();
|
|
let username = username.clone();
|
|
let is_admin = is_admin.clone();
|
|
let pwd = pwd.clone();
|
|
let status = status.clone();
|
|
|
|
Callback::from(move |e: SubmitEvent| {
|
|
e.prevent_default();
|
|
let first_name = (*first_name).clone();
|
|
let last_name = (*last_name).clone();
|
|
let username = (*username).clone();
|
|
let is_admin = *is_admin;
|
|
let pwd = (*pwd).clone();
|
|
let status = status.clone();
|
|
|
|
spawn_local(async move {
|
|
let payload = UserCreateScheme {
|
|
first_name,
|
|
last_name,
|
|
username,
|
|
is_admin,
|
|
pwd,
|
|
};
|
|
|
|
let request = Request::post("/api/register")
|
|
.json(&payload)
|
|
.expect("Error building request");
|
|
|
|
match request.send().await {
|
|
Ok(response) if response.status() == 200 => status.set(Some("Success".into())),
|
|
Ok(response) => status.set(Some(format!("Error: {}", response.status()))),
|
|
Err(err) => status.set(Some(format!("Network error: {}", err))),
|
|
}
|
|
});
|
|
})
|
|
};
|
|
|
|
let fn_change = {
|
|
let first_name = first_name.clone();
|
|
Callback::from(move |e: InputEvent| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
first_name.set(input.value());
|
|
})
|
|
};
|
|
|
|
let ln_change = {
|
|
let last_name = last_name.clone();
|
|
Callback::from(move |e: InputEvent| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
last_name.set(input.value());
|
|
})
|
|
};
|
|
|
|
let un_change = {
|
|
let username = username.clone();
|
|
Callback::from(move |e: InputEvent| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
username.set(input.value());
|
|
})
|
|
};
|
|
|
|
let admin_change = {
|
|
let is_admin = is_admin.clone();
|
|
Callback::from(move |e: Event| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
is_admin.set(input.checked());
|
|
})
|
|
};
|
|
|
|
let pwd_change = {
|
|
let pwd = pwd.clone();
|
|
Callback::from(move |e: InputEvent| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
pwd.set(input.value());
|
|
})
|
|
};
|
|
|
|
html! {
|
|
<form {onsubmit}>
|
|
<label>{ "Vorname:" }
|
|
<input type="text" value={(*first_name).clone()} oninput={fn_change}/>
|
|
</label>
|
|
<label>{ "Nachname:" }
|
|
<input type="text" value={(*last_name).clone()} oninput={ln_change}/>
|
|
</label>
|
|
<label>{ "Benutzername:" }
|
|
<input type="text" value={(*username).clone()} oninput={un_change}/>
|
|
</label>
|
|
<label>{ "Admin:" }
|
|
<input type="checkbox" checked={*is_admin} onchange={admin_change}/>
|
|
</label>
|
|
<label>{ "Password:" }
|
|
<input type="password" value={(*pwd).clone()} oninput={pwd_change}/>
|
|
</label>
|
|
<button type="submit">{ "Bestätigen" }</button>
|
|
</form>
|
|
}
|
|
}
|
|
|
|
#[component(Login)]
|
|
pub fn login_component() -> Html {
|
|
let username = use_state(|| "".to_string());
|
|
let pwd = use_state(|| "".to_string());
|
|
let loading = use_state(|| false);
|
|
let error = use_state(|| String::new());
|
|
let success = use_state(|| false);
|
|
let navigator = use_navigator().unwrap();
|
|
|
|
let onsubmit = {
|
|
let username = username.clone();
|
|
let pwd = pwd.clone();
|
|
let loading = loading.clone();
|
|
let error = error.clone();
|
|
let success = success.clone();
|
|
let navigator = navigator.clone();
|
|
|
|
Callback::from(move |e: SubmitEvent| {
|
|
e.prevent_default();
|
|
let username = (*username).clone();
|
|
let pwd = (*pwd).clone();
|
|
let loading = loading.clone();
|
|
let error = error.clone();
|
|
let success = success.clone();
|
|
let navigator = navigator.clone();
|
|
|
|
loading.set(true);
|
|
error.set(String::new());
|
|
success.set(false);
|
|
|
|
spawn_local(async move {
|
|
let payload = LoginScheme { username, pwd };
|
|
|
|
let response = Request::post("/api/login")
|
|
.header("Content-Type", "application/json")
|
|
.credentials(web_sys::RequestCredentials::Include)
|
|
.json(&payload)
|
|
.unwrap()
|
|
.send()
|
|
.await;
|
|
|
|
loading.set(false);
|
|
|
|
match response {
|
|
Ok(r) if r.status() == 200 => {
|
|
success.set(true);
|
|
navigator.push(&crate::Route::Home);
|
|
}
|
|
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! {
|
|
<form {onsubmit}>
|
|
<input
|
|
placeholder="username"
|
|
value={(*username).clone()}
|
|
oninput={Callback::from(move |e: InputEvent| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
username.set(input.value());
|
|
})}
|
|
/>
|
|
<input
|
|
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());
|
|
})}
|
|
/>
|
|
<button type="submit" disabled={*loading}>{ if *loading { "Logging in..." } else { "Login" } }</button>
|
|
if !error.is_empty() { <p style="color:red">{(*error).clone()}</p> }
|
|
</form>
|
|
}
|
|
}
|
|
|
|
#[component(AllUsers)]
|
|
pub fn all_users_component() -> Html {
|
|
let users = use_state(|| Vec::<FilteredUser>::new());
|
|
let error = use_state(|| None::<String>);
|
|
let loading = use_state(|| false);
|
|
|
|
{
|
|
let users = users.clone();
|
|
let error = error.clone();
|
|
let loading = loading.clone();
|
|
|
|
use_effect_with((), move |_| {
|
|
loading.set(true);
|
|
spawn_local(async move {
|
|
let url = format!("/api/users");
|
|
match Request::get(&url).send().await {
|
|
Ok(response) if response.status() == 200 => {
|
|
match response.json::<Vec<FilteredUser>>().await {
|
|
Ok(u) => users.set(u),
|
|
Err(err) => error.set(Some(format!("Parse error: {}", err))),
|
|
}
|
|
}
|
|
Ok(response) => {
|
|
if let Ok(text) = response.text().await {
|
|
error.set(Some(text));
|
|
} else {
|
|
error.set(Some(format!("status {}", response.status())));
|
|
}
|
|
}
|
|
Err(err) => error.set(Some(format!("Network error: {}", err))),
|
|
}
|
|
loading.set(false);
|
|
});
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
if *loading {
|
|
html! {<p>{ "Loading" }</p>}
|
|
} else if let Some(e) = &*error {
|
|
html! { <p>{ format!("Error: {}", e) }</p> }
|
|
} else {
|
|
html! {
|
|
<ul>
|
|
{ for users.iter().map(|t| html! {
|
|
<li key={t.id.to_string()}>
|
|
<h3>{ format!("{} {}- #{}", t.first_name, t.last_name, t.id) }</h3>
|
|
</li>
|
|
})}
|
|
</ul>
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component(UserByID)]
|
|
pub fn user_by_id_component(props: &UserProps) -> Html {
|
|
let user = use_state(|| None::<FilteredUser>);
|
|
let error = use_state(|| None::<String>);
|
|
let loading = use_state(|| false);
|
|
let id = props.id;
|
|
|
|
{
|
|
let user = user.clone();
|
|
let error = error.clone();
|
|
let loading = loading.clone();
|
|
|
|
use_effect_with(id, move |id_ref| {
|
|
loading.set(true);
|
|
let user = user.clone();
|
|
let error = error.clone();
|
|
let id = *id_ref;
|
|
spawn_local(async move {
|
|
let url = format!("/api/users/{}", id);
|
|
|
|
match Request::get(&url).send().await {
|
|
Ok(response) => {
|
|
let status = response.status();
|
|
if status == 200 {
|
|
match response.json::<FilteredUser>().await {
|
|
Ok(u) => user.set(Some(u)),
|
|
Err(err) => error.set(Some(format!("Parse error: {}", err))),
|
|
}
|
|
} else {
|
|
match response.json::<ApiError>().await {
|
|
Ok(ae) => error.set(Some(ae.message)),
|
|
Err(_) => {
|
|
if let Ok(text) = response.text().await {
|
|
error.set(Some(text));
|
|
} else {
|
|
error.set(Some(format!("Server error: {}", status)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(err) => error.set(Some(format!("Network error: {}", err))),
|
|
}
|
|
loading.set(false);
|
|
});
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
let first_name = use_state(|| "".to_string());
|
|
let last_name = use_state(|| "".to_string());
|
|
let username = use_state(|| "".to_string());
|
|
let make_admin = use_state(|| false);
|
|
let new_pwd = use_state(|| String::new());
|
|
let saving = use_state(|| false);
|
|
let save_error = use_state(|| None::<String>);
|
|
let save_success = use_state(|| false);
|
|
|
|
{
|
|
let user = user.clone();
|
|
let first_name = first_name.clone();
|
|
let last_name = last_name.clone();
|
|
let username = username.clone();
|
|
let make_admin = make_admin.clone();
|
|
|
|
use_effect_with(user, move |user_ref| {
|
|
if let Some(u) = &**user_ref {
|
|
first_name.set(u.first_name.clone());
|
|
last_name.set(u.last_name.clone());
|
|
username.set(u.username.clone());
|
|
make_admin.set(u.is_admin);
|
|
}
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
let onsubmit = {
|
|
let first_name = first_name.clone();
|
|
let last_name = last_name.clone();
|
|
let username = username.clone();
|
|
let make_admin = make_admin.clone();
|
|
let new_pwd = new_pwd.clone();
|
|
let saving = saving.clone();
|
|
let save_error = save_error.clone();
|
|
let save_success = save_success.clone();
|
|
let user_state = user.clone();
|
|
|
|
Callback::from(move |e: SubmitEvent| {
|
|
e.prevent_default();
|
|
let first_name = (*first_name).clone();
|
|
let last_name = (*last_name).clone();
|
|
let username = (*username).clone();
|
|
let make_admin = *make_admin;
|
|
let new_pwd = (*new_pwd).clone();
|
|
|
|
saving.set(true);
|
|
save_error.set(None);
|
|
save_success.set(false);
|
|
|
|
let saving = saving.clone();
|
|
let save_error = save_error.clone();
|
|
let save_success = save_success.clone();
|
|
let user_state = user_state.clone();
|
|
let id = id;
|
|
|
|
spawn_local(async move {
|
|
let payload = UserUpdateScheme {
|
|
id: id,
|
|
first_name,
|
|
last_name,
|
|
username,
|
|
make_admin,
|
|
new_pwd,
|
|
};
|
|
|
|
let url = format!("/api/users/{}", id);
|
|
let request = Request::patch(&url)
|
|
.header("Content-Type", "application/json")
|
|
.credentials(web_sys::RequestCredentials::Include)
|
|
.json(&payload)
|
|
.expect("Failed to construct Request");
|
|
|
|
match request.send().await {
|
|
Ok(resp) if resp.status() == 200 => {
|
|
if let Ok(updated) = resp.json::<FilteredUser>().await {
|
|
user_state.set(Some(updated));
|
|
}
|
|
save_success.set(true);
|
|
}
|
|
Ok(resp) => {
|
|
let txt = resp.text().await.unwrap_or_else(|_| "Unknown".into());
|
|
save_error.set(Some(format!("HTTP {}: {}", resp.status(), txt)));
|
|
}
|
|
Err(err) => save_error.set(Some(format!("Network error: {}", err))),
|
|
}
|
|
saving.set(false);
|
|
});
|
|
})
|
|
};
|
|
|
|
let deleting = use_state(|| false);
|
|
let delete_error = use_state(|| None::<String>);
|
|
|
|
let ondelete = {
|
|
let deleting = deleting.clone();
|
|
let delete_error = delete_error.clone();
|
|
let user_state = user.clone(); // or ticket
|
|
let id = id;
|
|
|
|
Callback::from(move |e: MouseEvent| {
|
|
e.prevent_default();
|
|
// confirm
|
|
if !web_sys::window()
|
|
.and_then(|w| {
|
|
w.confirm_with_message("Are you sure you want to delete this item?")
|
|
.ok()
|
|
})
|
|
.unwrap_or(false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
deleting.set(true);
|
|
delete_error.set(None);
|
|
|
|
let deleting = deleting.clone();
|
|
let delete_error = delete_error.clone();
|
|
let user_state = user_state.clone();
|
|
|
|
spawn_local(async move {
|
|
let url = format!("/api/users/{}", id); // or /api/tickets/{}
|
|
let req = Request::delete(&url).credentials(web_sys::RequestCredentials::Include);
|
|
|
|
match req.send().await {
|
|
Ok(resp) if resp.status() == 200 || resp.status() == 204 => {
|
|
// remove local state or navigate away
|
|
user_state.set(None); // clears the shown item
|
|
}
|
|
Ok(resp) => {
|
|
let txt = resp.text().await.unwrap_or_else(|_| "Unknown".into());
|
|
delete_error.set(Some(format!("HTTP {}: {}", resp.status(), txt)));
|
|
}
|
|
Err(err) => delete_error.set(Some(format!("Network error: {}", err))),
|
|
}
|
|
deleting.set(false);
|
|
});
|
|
})
|
|
};
|
|
|
|
if *loading {
|
|
html! {<p>{ "Loading" }</p>}
|
|
} else if let Some(e) = &*error {
|
|
html! { <p>{ format!("Error: {}", e) }</p> }
|
|
} else if let Some(u) = &*user {
|
|
html! {
|
|
<div>
|
|
<div>
|
|
<p><strong>{ "Vorname: " }</strong>{ &u.first_name }</p>
|
|
<p><strong>{ "Nachname: " }</strong>{ &u.last_name }</p>
|
|
<p><strong>{ "Benutzername: " }</strong>{ &u.username }</p>
|
|
<p><strong>{ "Ist Admin: " }</strong>{ u.is_admin }</p>
|
|
</div>
|
|
|
|
<h1>{ format!("User #{}", u.id) }</h1>
|
|
<form onsubmit={onsubmit}>
|
|
<div>
|
|
<label>{ "Vorname" }
|
|
<input name="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>
|
|
<label>{ "Nachname" }
|
|
<input name="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>
|
|
<label>{ "Benutzername" }
|
|
<input name="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>
|
|
<label>{ "Ist Admin" }
|
|
<input type="checkbox" name="make_admin" checked={*make_admin} onchange={Callback::from(move |e: Event| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
make_admin.set(input.checked());
|
|
})}/>
|
|
</label>
|
|
</div>
|
|
<div>
|
|
<label>{ "Neues Passwort (leer = unchanged)" }
|
|
<input name="new_pwd" type="password"
|
|
value={(*new_pwd).clone()}
|
|
oninput={Callback::from(move |e: InputEvent| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
new_pwd.set(input.value());
|
|
})}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<button type="submit" disabled={*saving}>{ if *saving { "Speichern..." } else { "Speichern" } }</button>
|
|
if *save_success {
|
|
<p style="color:green">{ "Updated successfully" }</p>
|
|
}
|
|
if let Some(err) = &*save_error {
|
|
<p style="color:red">{ err.clone() }</p>
|
|
}
|
|
</form>
|
|
|
|
<button onclick={ondelete} disabled={*deleting}>
|
|
{if *deleting {"Löschen..."} else {"Löschen"}}
|
|
</button>
|
|
if let Some(err) = &*delete_error {
|
|
<p style="color:red">{ err.clone() }</p>
|
|
}
|
|
</div>
|
|
}
|
|
} else {
|
|
html! { <p>{ "No ticket found." }</p> }
|
|
}
|
|
}
|