5 Commits
Author SHA1 Message Date
schn33fuchs b672fe9768 Deleting users now possible 2026-04-29 21:52:03 +02:00
schn33fuchs fd9e429760 Deleting tickets now possible 2026-04-29 21:48:57 +02:00
schn33fuchs a6f818505b Implemented updating users
at /users/{id}
2026-04-29 21:28:36 +02:00
schn33fuchs 63af32c4af Show user by ID implemented
At /users/{id},
Also minor bugfixes and improvements
2026-04-29 20:54:14 +02:00
schn33fuchs 6dfa21a005 Login and Showing of all Users implemented
At /login and /users
2026-04-29 20:41:31 +02:00
4 changed files with 503 additions and 6 deletions
+2 -2
View File
@@ -16,8 +16,8 @@ serde = { version = "1.0.228", features = ["derive"] }
wasm-bindgen = "0.2.118" wasm-bindgen = "0.2.118"
wasm-bindgen-futures = "0.4.68" wasm-bindgen-futures = "0.4.68"
web-sys = { version = "0.3.95", features = [ web-sys = { version = "0.3.95", features = [
"Window","Document","Request","Response","Headers", "HtmlSelectElement", "Window","Document","Request","Response","Headers", "HtmlSelectElement", "RequestCredentials",
"SubmitEvent","InputEvent","HtmlInputElement","Event", "HtmlFormElement" "SubmitEvent","InputEvent","HtmlInputElement","Event", "HtmlFormElement", "MouseEvent"
] } ] }
gloo-net = "0.7.0" gloo-net = "0.7.0"
yew-router = "0.20.0" yew-router = "0.20.0"
+10 -1
View File
@@ -15,6 +15,12 @@ enum Route {
AllTickets, AllTickets,
#[at("/register")] #[at("/register")]
Register, Register,
#[at("/login")]
Login,
#[at("/users")]
AllUsers,
#[at("/users/:id")]
UserByID { id: i16 },
#[not_found] #[not_found]
#[at("/404")] #[at("/404")]
NotFound, NotFound,
@@ -25,9 +31,12 @@ fn switch(route: Route) -> Html {
Route::Home => html! { <basic_pages::Home/>}, Route::Home => html! { <basic_pages::Home/>},
Route::NotFound => html! { <basic_pages::NotFound/> }, Route::NotFound => html! { <basic_pages::NotFound/> },
Route::Ticket => html! { <ticket::SubmitTicket/> }, 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::AllTickets => html! { <ticket::AllTickets/> },
Route::Register => html! { <user::Register/> }, Route::Register => html! { <user::Register/> },
Route::Login => html! { <user::Login/> },
Route::AllUsers => html! {<user::AllUsers/>},
Route::UserByID { id } => html! { <user::UserByID id={id}/> },
} }
} }
+56 -3
View File
@@ -36,7 +36,7 @@ pub struct TicketProps {
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ApiError { struct ApiError {
message: String, message: String,
_status: String, _status: String,
} }
@@ -156,8 +156,8 @@ pub fn submit_ticket_component() -> Html {
} }
} }
#[component(ShowTicketByID)] #[component(TicketByID)]
pub fn show_ticket_by_id_component(props: &TicketProps) -> Html { pub fn ticket_by_id_component(props: &TicketProps) -> Html {
let ticket = use_state(|| None::<Ticket>); let ticket = use_state(|| None::<Ticket>);
let error = use_state(|| None::<String>); let error = use_state(|| None::<String>);
let loading = use_state(|| false); 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 { if *loading {
html! {<p>{ "Loading" }</p>} html! {<p>{ "Loading" }</p>}
} else if let Some(e) = &*error { } else if let Some(e) = &*error {
@@ -274,6 +320,13 @@ pub fn show_ticket_by_id_component(props: &TicketProps) -> Html {
</label> </label>
<button type="submit">{ "Aktualisieren" }</button> <button type="submit">{ "Aktualisieren" }</button>
</form> </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> </div>
} }
} else { } else {
+435
View File
@@ -1,3 +1,5 @@
use std::net::ToSocketAddrs;
use gloo_net::http::Request; use gloo_net::http::Request;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::spawn_local;
@@ -28,6 +30,16 @@ pub struct LoginScheme {
pub pwd: 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)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FilteredUser { pub struct FilteredUser {
pub id: i16, pub id: i16,
@@ -37,6 +49,17 @@ pub struct FilteredUser {
pub is_admin: bool, pub is_admin: bool,
} }
#[derive(Properties, PartialEq)]
pub struct UserProps {
pub id: i16,
}
#[derive(Deserialize, Debug)]
struct ApiError {
message: String,
_status: String,
}
#[component(Register)] #[component(Register)]
pub fn register_component() -> Html { pub fn register_component() -> Html {
let first_name = use_state(|| "".to_string()); let first_name = use_state(|| "".to_string());
@@ -146,3 +169,415 @@ pub fn register_component() -> Html {
</form> </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> }
}
}