Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b672fe9768 | ||
|
|
fd9e429760 | ||
|
|
a6f818505b | ||
|
|
63af32c4af | ||
|
|
6dfa21a005 |
+2
-2
@@ -16,8 +16,8 @@ serde = { version = "1.0.228", features = ["derive"] }
|
||||
wasm-bindgen = "0.2.118"
|
||||
wasm-bindgen-futures = "0.4.68"
|
||||
web-sys = { version = "0.3.95", features = [
|
||||
"Window","Document","Request","Response","Headers", "HtmlSelectElement",
|
||||
"SubmitEvent","InputEvent","HtmlInputElement","Event", "HtmlFormElement"
|
||||
"Window","Document","Request","Response","Headers", "HtmlSelectElement", "RequestCredentials",
|
||||
"SubmitEvent","InputEvent","HtmlInputElement","Event", "HtmlFormElement", "MouseEvent"
|
||||
] }
|
||||
gloo-net = "0.7.0"
|
||||
yew-router = "0.20.0"
|
||||
|
||||
+10
-1
@@ -15,6 +15,12 @@ enum Route {
|
||||
AllTickets,
|
||||
#[at("/register")]
|
||||
Register,
|
||||
#[at("/login")]
|
||||
Login,
|
||||
#[at("/users")]
|
||||
AllUsers,
|
||||
#[at("/users/:id")]
|
||||
UserByID { id: i16 },
|
||||
#[not_found]
|
||||
#[at("/404")]
|
||||
NotFound,
|
||||
@@ -25,9 +31,12 @@ fn switch(route: Route) -> Html {
|
||||
Route::Home => html! { <basic_pages::Home/>},
|
||||
Route::NotFound => html! { <basic_pages::NotFound/> },
|
||||
Route::Ticket => html! { <ticket::SubmitTicket/> },
|
||||
Route::TicketById { id } => html! { <ticket::ShowTicketByID id={id}/> },
|
||||
Route::TicketById { id } => html! { <ticket::TicketByID id={id}/> },
|
||||
Route::AllTickets => html! { <ticket::AllTickets/> },
|
||||
Route::Register => html! { <user::Register/> },
|
||||
Route::Login => html! { <user::Login/> },
|
||||
Route::AllUsers => html! {<user::AllUsers/>},
|
||||
Route::UserByID { id } => html! { <user::UserByID id={id}/> },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ pub struct TicketProps {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ApiError {
|
||||
struct ApiError {
|
||||
message: String,
|
||||
_status: String,
|
||||
}
|
||||
@@ -156,8 +156,8 @@ pub fn submit_ticket_component() -> Html {
|
||||
}
|
||||
}
|
||||
|
||||
#[component(ShowTicketByID)]
|
||||
pub fn show_ticket_by_id_component(props: &TicketProps) -> Html {
|
||||
#[component(TicketByID)]
|
||||
pub fn ticket_by_id_component(props: &TicketProps) -> Html {
|
||||
let ticket = use_state(|| None::<Ticket>);
|
||||
let error = use_state(|| None::<String>);
|
||||
let loading = use_state(|| false);
|
||||
@@ -242,6 +242,52 @@ pub fn show_ticket_by_id_component(props: &TicketProps) -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
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 ticket_state = ticket.clone();
|
||||
let id = id;
|
||||
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
e.prevent_default();
|
||||
if !web_sys::window()
|
||||
.and_then(|w| w.confirm_with_message("Sicher löschen?").ok())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
deleting.set(true);
|
||||
delete_error.set(None);
|
||||
|
||||
let deleting = deleting.clone();
|
||||
let delete_error = delete_error.clone();
|
||||
let ticket_state = ticket_state.clone();
|
||||
|
||||
spawn_local(async move {
|
||||
let url = format!("/api/tickets/{}", id);
|
||||
let request =
|
||||
Request::delete(&url).credentials(web_sys::RequestCredentials::Include);
|
||||
|
||||
match request.send().await {
|
||||
Ok(resp) if resp.status() == 200 || resp.status() == 204 => {
|
||||
// remove local state or navigate away
|
||||
ticket_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 {
|
||||
@@ -274,6 +320,13 @@ pub fn show_ticket_by_id_component(props: &TicketProps) -> Html {
|
||||
</label>
|
||||
<button type="submit">{ "Aktualisieren" }</button>
|
||||
</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 {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
use gloo_net::http::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
@@ -28,6 +30,16 @@ pub struct LoginScheme {
|
||||
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,
|
||||
@@ -37,6 +49,17 @@ pub struct FilteredUser {
|
||||
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());
|
||||
@@ -146,3 +169,415 @@ pub fn register_component() -> Html {
|
||||
</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 onsubmit = {
|
||||
let username = username.clone();
|
||||
let pwd = pwd.clone();
|
||||
let loading = loading.clone();
|
||||
let error = error.clone();
|
||||
let success = success.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();
|
||||
|
||||
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),
|
||||
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 *success { <p>{"Logged in — cookie set by server"}</p> }
|
||||
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> }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user