4 Commits
Author SHA1 Message Date
schn33fuchs 4fc1c2eeb8 Non admin only see their tickets 2026-05-01 17:07:20 +02:00
schn33fuchs fdd2e2a757 admin pages
Pages can now be locked behind admin privileges
2026-05-01 16:47:42 +02:00
schn33fuchs 802e5876f7 Ticket now shows who submitted it 2026-05-01 16:46:56 +02:00
schn33fuchs e54be14526 When not logged in redirection to login page
Every page is locked behind a jwt, when it is not supplied neither other
pages not api calls will work
2026-05-01 16:18:15 +02:00
11 changed files with 335 additions and 71 deletions
+13 -8
View File
@@ -12,23 +12,28 @@ pub struct Error {
pub fn encode_token(header: &Header, id: String, key: &EncodingKey) -> String { pub fn encode_token(header: &Header, id: String, key: &EncodingKey) -> String {
let now = chrono::Utc::now(); let now = chrono::Utc::now();
let issued = now.timestamp() as usize; let expires = (now + chrono::Duration::minutes(60)).timestamp();
let expires = (now + chrono::Duration::minutes(60)).timestamp() as usize;
let claims: Claims = Claims { let claims: Claims = Claims {
subject: id, sub: id,
issued: issued, issued: now.timestamp() as usize,
expires: expires, expires: expires as usize,
}; };
let token = encode(header, &claims, key); let token = encode(header, &claims, key);
return token.expect("token return failed"); return token.expect("token return failed");
} }
pub fn decode_token(token: String, key: &DecodingKey) -> Result<Claims, (StatusCode, Json<Error>)> { pub fn decode_token(token: String, key: &DecodingKey) -> Result<Claims, (StatusCode, Json<Error>)> {
let claims = decode::<Claims>(&token, key, &Validation::default()) let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
.map_err(|_| { validation.validate_exp = false;
validation.validate_nbf = false;
validation.leeway = 0;
let claims = decode::<Claims>(&token, key, &validation)
.map_err(|err| {
let message = format!("Invalid Token: {}", err);
let error = Error { let error = Error {
status: "error", status: "error",
message: "Invalid Token".to_string(), message,
}; };
(StatusCode::UNAUTHORIZED, Json(error)) (StatusCode::UNAUTHORIZED, Json(error))
})? })?
+10 -3
View File
@@ -15,6 +15,7 @@ use serde_json::json;
use crate::{ use crate::{
AppState, AppState,
cookie::jwt::decode_token, cookie::jwt::decode_token,
handlers::auth::filter_user,
models::{LoginScheme, User}, models::{LoginScheme, User},
}; };
@@ -53,9 +54,15 @@ pub async fn validate_token(
token, token,
&DecodingKey::from_secret(data.env.token_secret.as_ref()), &DecodingKey::from_secret(data.env.token_secret.as_ref()),
) )
.unwrap(); .map_err(|(status, json_err)| {
let error = json!({
"status": json_err.status,
"message": json_err.message
});
(status, Json(error))
})?;
let uuid = (&claims.subject).parse::<i64>().map_err(|_| { let uuid = (&claims.sub).parse::<i16>().map_err(|_| {
let error = json!({ let error = json!({
"status": "error", "status": "error",
"message": "Invalid user id" "message": "Invalid user id"
@@ -83,6 +90,6 @@ pub async fn validate_token(
(StatusCode::UNAUTHORIZED, Json(error)) (StatusCode::UNAUTHORIZED, Json(error))
})?; })?;
request.extensions_mut().insert(user); request.extensions_mut().insert(filter_user(&user));
Ok(next.run(request).await) Ok(next.run(request).await)
} }
+6 -4
View File
@@ -156,13 +156,15 @@ pub async fn logout() -> Result<impl IntoResponse, (StatusCode, Json<serde_json:
} }
pub async fn get_current_user( pub async fn get_current_user(
Extension(state): Extension<User>, Extension(user): Extension<FilteredUser>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> { ) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let response = json!({ let response = json!({
"status": "success", "status": "success",
"data": json!({ "data": json!({
"first_name": filter_user(&state).first_name, "id": user.id,
"last_name": filter_user(&state).last_name "first_name": user.first_name,
"last_name": user.last_name,
"is_admin": user.is_admin
}) })
}); });
@@ -302,7 +304,7 @@ pub async fn update_user(
Ok(Json(response)) Ok(Json(response))
} }
fn filter_user(user: &User) -> FilteredUser { pub fn filter_user(user: &User) -> FilteredUser {
FilteredUser { FilteredUser {
id: user.id, id: user.id,
first_name: user.first_name.clone(), first_name: user.first_name.clone(),
+80 -41
View File
@@ -1,30 +1,32 @@
use std::sync::Arc; use std::sync::Arc;
use axum::{ use axum::{
Json, Extension, Json,
extract::{Path, State}, extract::{Path, State},
http::StatusCode, http::StatusCode,
response::IntoResponse, response::IntoResponse,
}; };
use serde_json::json; use serde_json::json;
use sqlx::query; use sqlx::{query, Row};
use crate::{ use crate::{
AppState, AppState,
models::{Ticket, TicketCreateScheme, TicketResponse, TicketUpdateScheme}, models::{FilteredUser, Ticket, TicketCreateScheme, TicketResponse, TicketUpdateScheme},
}; };
pub async fn create_ticket( pub async fn create_ticket(
Extension(user): Extension<FilteredUser>,
State(data): State<Arc<AppState>>, State(data): State<Arc<AppState>>,
Json(body): Json<TicketCreateScheme>, Json(body): Json<TicketCreateScheme>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> { ) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let query = query( let query = query(
r#"INSERT INTO tickets (category, description, betreff, room) VALUES ($1, $2, $3, $4)"#, r#"INSERT INTO tickets (category, description, betreff, room, user_id) VALUES ($1, $2, $3, $4, $5)"#,
) )
.bind(body.category.to_string()) .bind(body.category.to_string())
.bind(body.description.to_string()) .bind(body.description.to_string())
.bind(body.betreff.to_string()) .bind(body.betreff.to_string())
.bind(body.room) .bind(body.room)
.bind(user.id)
.execute(&data.db) .execute(&data.db)
.await; .await;
@@ -69,8 +71,11 @@ pub async fn get_tickets(
State(data): State<Arc<AppState>>, State(data): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> { ) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
println!("get_tickets called"); println!("get_tickets called");
let tickets = sqlx::query_as::<_, Ticket>( let tickets = sqlx::query(
r#"SELECT * FROM tickets WHERE status <> 'Archived' ORDER BY date DESC"#, r#"SELECT t.id, t.category, t.betreff, t.description, t.room, t.status, t.date, t.user_id, u.first_name, u.last_name
FROM tickets t
LEFT JOIN users u ON t.user_id = u.id
WHERE t.status <> 'Archived' ORDER BY t.date DESC"#,
) )
.fetch_all(&data.db) .fetch_all(&data.db)
.await .await
@@ -83,10 +88,21 @@ pub async fn get_tickets(
})?; })?;
println!("Tickets fetched"); println!("Tickets fetched");
let ticket_response = tickets let ticket_response: Vec<TicketResponse> = tickets
.iter() .iter()
.map(|ticket| filter_record(&ticket)) .map(|row| TicketResponse {
.collect::<Vec<TicketResponse>>(); id: row.get("id"),
category: row.get("category"),
betreff: row.get("betreff"),
description: row.get("description"),
room: row.get("room"),
status: row.get("status"),
date: row.get("date"),
user_id: row.get("user_id"),
user_first_name: row.get("first_name"),
user_last_name: row.get("last_name"),
})
.collect();
let json_response = serde_json::json!(ticket_response); let json_response = serde_json::json!(ticket_response);
println!("Json contructed"); println!("Json contructed");
@@ -97,15 +113,32 @@ pub async fn get_ticket_by_id(
Path(id): Path<i32>, Path(id): Path<i32>,
State(data): State<Arc<AppState>>, State(data): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> { ) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let query = sqlx::query_as::<_, Ticket>(r#"SELECT * FROM tickets WHERE id = $1"#) let query = sqlx::query(
.bind(id) r#"SELECT t.id, t.category, t.betreff, t.description, t.room, t.status, t.date, t.user_id, u.first_name, u.last_name
.fetch_one(&data.db) FROM tickets t
.await; LEFT JOIN users u ON t.user_id = u.id
WHERE t.id = $1"#,
)
.bind(id)
.fetch_one(&data.db)
.await;
match query { match query {
Ok(ticket) => { Ok(row) => {
let ticket_response = serde_json::json!(filter_record(&ticket)); let ticket_response = TicketResponse {
return Ok(Json(ticket_response)); id: row.get("id"),
category: row.get("category"),
betreff: row.get("betreff"),
description: row.get("description"),
room: row.get("room"),
status: row.get("status"),
date: row.get("date"),
user_id: row.get("user_id"),
user_first_name: row.get("first_name"),
user_last_name: row.get("last_name"),
};
let response = serde_json::json!(ticket_response);
return Ok(Json(response));
} }
Err(sqlx::Error::RowNotFound) => { Err(sqlx::Error::RowNotFound) => {
let error_response = serde_json::json!({ let error_response = serde_json::json!({
@@ -148,34 +181,40 @@ pub async fn edit_ticket(
return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))); return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)));
} }
let updated_ticket = sqlx::query_as::<_, Ticket>(r#"SELECT * FROM tickets WHERE id = $1"#) let updated_ticket = sqlx::query(
.bind(id) r#"SELECT t.id, t.category, t.betreff, t.description, t.room, t.status, t.date, t.user_id, u.first_name, u.last_name
.fetch_one(&data.db) FROM tickets t
.await LEFT JOIN users u ON t.user_id = u.id
.map_err(|e| { WHERE t.id = $1"#,
( )
StatusCode::INTERNAL_SERVER_ERROR, .bind(id)
Json(json!({"status": "error", "message": format!("{:?}", e)})), .fetch_one(&data.db)
) .await
})?; .map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"status": "error", "message": format!("{:?}", e)})),
)
})?;
let ticket_response = serde_json::json!({ let ticket_response = TicketResponse {
"ticket": filter_record(&updated_ticket), id: updated_ticket.get("id"),
category: updated_ticket.get("category"),
betreff: updated_ticket.get("betreff"),
description: updated_ticket.get("description"),
room: updated_ticket.get("room"),
status: updated_ticket.get("status"),
date: updated_ticket.get("date"),
user_id: updated_ticket.get("user_id"),
user_first_name: updated_ticket.get("first_name"),
user_last_name: updated_ticket.get("last_name"),
};
let response = serde_json::json!({
"ticket": ticket_response,
"status": "success" "status": "success"
}); });
Ok(Json(ticket_response)) Ok(Json(response))
} }
fn filter_record(ticket: &Ticket) -> TicketResponse {
TicketResponse {
id: ticket.id.to_owned(),
category: ticket.category.to_owned(),
betreff: ticket.betreff.to_owned(),
description: ticket.description.to_owned(),
room: ticket.room.to_owned(),
status: ticket.status.to_owned(),
date: ticket.date.to_owned(),
user_id: ticket.user_id.to_owned(),
}
}
+7 -2
View File
@@ -25,6 +25,8 @@ pub struct TicketResponse {
pub status: String, pub status: String,
pub date: chrono::NaiveDateTime, pub date: chrono::NaiveDateTime,
pub user_id: i16, pub user_id: i16,
pub user_first_name: String,
pub user_last_name: String,
} }
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone, sqlx::FromRow)] #[derive(Deserialize, Serialize, PartialEq, Debug, Clone, sqlx::FromRow)]
@@ -75,7 +77,7 @@ pub struct LoginScheme {
pub pwd: String, pub pwd: String,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct FilteredUser { pub struct FilteredUser {
pub id: i16, pub id: i16,
pub first_name: String, pub first_name: String,
@@ -86,7 +88,10 @@ pub struct FilteredUser {
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims { pub struct Claims {
pub subject: String, #[serde(alias = "subject")]
pub sub: String,
#[serde(rename = "iat", alias = "issued", default)]
pub issued: usize, pub issued: usize,
#[serde(rename = "exp", alias = "expires", default)]
pub expires: usize, pub expires: usize,
} }
+11 -3
View File
@@ -1,12 +1,13 @@
use std::sync::Arc; use std::sync::Arc;
use axum::{ use axum::{
Router, Router, middleware,
routing::{get, post}, routing::{get, post},
}; };
use crate::{ use crate::{
AppState, AppState,
cookie::validation::validate_token,
handlers::{ handlers::{
auth::{ auth::{
create_user, delete_user, get_current_user, get_user_by_id, get_users, login, logout, create_user, delete_user, get_current_user, get_user_by_id, get_users, login, logout,
@@ -17,7 +18,7 @@ use crate::{
}; };
pub fn create_router(state: Arc<AppState>) -> Router { pub fn create_router(state: Arc<AppState>) -> Router {
Router::new() let protected_routes = Router::new()
.route("/api/tickets", get(get_tickets)) .route("/api/tickets", get(get_tickets))
.route("/api/tickets/create", post(create_ticket)) .route("/api/tickets/create", post(create_ticket))
.route( .route(
@@ -27,7 +28,6 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.patch(edit_ticket), .patch(edit_ticket),
) )
.route("/api/register", post(create_user)) .route("/api/register", post(create_user))
.route("/api/login", post(login))
.route("/api/logout", get(logout)) .route("/api/logout", get(logout))
.route("/api/users", get(get_users)) .route("/api/users", get(get_users))
.route("/api/users/current", get(get_current_user)) .route("/api/users/current", get(get_current_user))
@@ -35,5 +35,13 @@ pub fn create_router(state: Arc<AppState>) -> Router {
"/api/users/{id}", "/api/users/{id}",
get(get_user_by_id).delete(delete_user).patch(update_user), get(get_user_by_id).delete(delete_user).patch(update_user),
) )
.layer(middleware::from_fn_with_state(
state.clone(),
validate_token,
));
Router::new()
.merge(protected_routes)
.route("/api/login", post(login))
.with_state(state) .with_state(state)
} }
+96
View File
@@ -0,0 +1,96 @@
use gloo_net::http::Request;
use wasm_bindgen_futures::spawn_local;
use yew::prelude::*;
use yew_router::prelude::*;
#[derive(Clone, Debug, PartialEq)]
pub struct AuthState {
pub is_authenticated: Option<bool>,
pub is_admin: Option<bool>,
}
#[derive(Properties, PartialEq)]
pub struct ProtectedRouteProps {
pub children: Children,
pub admin_page: bool,
}
#[component(ProtectedRoute)]
pub fn protected_route(props: &ProtectedRouteProps) -> Html {
let auth_state = use_state(|| AuthState {
is_authenticated: None,
is_admin: None,
});
{
let auth_state = auth_state.clone();
use_effect_with((), move |_| {
let auth_state = auth_state.clone();
spawn_local(async move {
match Request::get("/api/users/current")
.credentials(web_sys::RequestCredentials::Include)
.send()
.await
{
Ok(resp) => {
let status = resp.status();
web_sys::console::log_1(&format!("Auth check: status {}", status).into());
if status == 200 {
let user_data: serde_json::Value =
resp.json().await.unwrap_or_default();
let is_admin = user_data["data"]["is_admin"].as_bool();
auth_state.set(AuthState {
is_authenticated: Some(true),
is_admin,
});
} else {
auth_state.set(AuthState {
is_authenticated: Some(false),
is_admin: Some(false),
});
}
}
Err(err) => {
web_sys::console::log_1(&format!("Auth check error: {:?}", err).into());
auth_state.set(AuthState {
is_authenticated: Some(false),
is_admin: Some(false),
});
}
}
});
|| ()
});
}
match (*auth_state) {
AuthState {
is_authenticated: None,
..
} => html! { <div>{ "Loading..." } </div> },
AuthState {
is_authenticated: Some(false),
..
} => html! {
<Redirect<crate::Route> to={crate::Route::Login}/>
},
AuthState {
is_authenticated: Some(true),
is_admin: admin_flag,
} => {
if props.admin_page {
match admin_flag {
Some(true) => props.children.clone().into(),
Some(false) => {
html! { <Redirect<crate::Route> to={crate::Route::PermissionDenied}/> }
}
None => html! { <div>{ "Checking permissions..." }</div> },
}
} else {
props.children.clone().into()
}
}
_ => html! { <div>{ "Checking permissions..." }</div> },
}
}
+40 -7
View File
@@ -1,4 +1,6 @@
mod auth;
mod pages; mod pages;
use crate::auth::ProtectedRoute;
use crate::pages::*; use crate::pages::*;
use yew::prelude::*; use yew::prelude::*;
use yew_router::prelude::*; use yew_router::prelude::*;
@@ -21,6 +23,8 @@ enum Route {
AllUsers, AllUsers,
#[at("/users/:id")] #[at("/users/:id")]
UserByID { id: i16 }, UserByID { id: i16 },
#[at("/denied")]
PermissionDenied,
#[not_found] #[not_found]
#[at("/404")] #[at("/404")]
NotFound, NotFound,
@@ -28,15 +32,44 @@ enum Route {
fn switch(route: Route) -> Html { fn switch(route: Route) -> Html {
match route { match route {
Route::Home => html! { <basic_pages::Home/>}, Route::Home => html! {
<ProtectedRoute admin_page={false}>
<basic_pages::Home/>
</ProtectedRoute>
},
Route::NotFound => html! { <basic_pages::NotFound/> }, Route::NotFound => html! { <basic_pages::NotFound/> },
Route::Ticket => html! { <ticket::SubmitTicket/> }, Route::Ticket => html! {
Route::TicketById { id } => html! { <ticket::TicketByID id={id}/> }, <ProtectedRoute admin_page={false}>
Route::AllTickets => html! { <ticket::AllTickets/> }, <ticket::SubmitTicket/>
Route::Register => html! { <user::Register/> }, </ProtectedRoute>
},
Route::TicketById { id } => html! {
<ProtectedRoute admin_page={true}>
<ticket::TicketByID {id}/>
</ProtectedRoute>
},
Route::AllTickets => html! {
<ProtectedRoute admin_page={false}>
<ticket::AllTickets/>
</ProtectedRoute>
},
Route::Register => html! {
<ProtectedRoute admin_page={true}>
<user::Register/>
</ProtectedRoute>
},
Route::Login => html! { <user::Login/> }, Route::Login => html! { <user::Login/> },
Route::AllUsers => html! {<user::AllUsers/>}, Route::AllUsers => html! {
Route::UserByID { id } => html! { <user::UserByID id={id}/> }, <ProtectedRoute admin_page={true}>
<user::AllUsers/>
</ProtectedRoute>
},
Route::UserByID { id } => html! {
<ProtectedRoute admin_page={true}>
<user::UserByID {id}/>
</ProtectedRoute>
},
Route::PermissionDenied => html! { <basic_pages::PermissionDenied/> },
} }
} }
+10
View File
@@ -28,3 +28,13 @@ pub fn not_found_component() -> Html {
</div> </div>
} }
} }
#[component(PermissionDenied)]
pub fn denied_component() -> Html {
html! {
<div>
<h1>{ "Sie haben nicht die benötigten Rechte um diese Seite aufzurufen" }</h1>
<h3>{ "Wenn sie denken, dass dies ein Fehler ist kontaktieren sie Herrn Winter" }</h3>
</div>
}
}
+54 -1
View File
@@ -28,6 +28,8 @@ pub struct Ticket {
pub status: String, pub status: String,
pub date: chrono::NaiveDateTime, pub date: chrono::NaiveDateTime,
pub user_id: i16, pub user_id: i16,
pub user_first_name: String,
pub user_last_name: String,
} }
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
@@ -35,6 +37,12 @@ pub struct TicketProps {
pub id: i32, pub id: i32,
} }
#[derive(Clone, Debug, PartialEq)]
pub struct ActiveUser {
id: Option<i16>,
is_admin: bool,
}
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct ApiError { struct ApiError {
message: String, message: String,
@@ -73,6 +81,8 @@ pub fn submit_ticket_component() -> Html {
}; };
let request = Request::post("/api/tickets/create") let request = Request::post("/api/tickets/create")
.header("Content-Type", "application/json")
.credentials(web_sys::RequestCredentials::Include)
.json(&payload) .json(&payload)
.expect("Failed to build request"); .expect("Failed to build request");
@@ -308,6 +318,7 @@ pub fn ticket_by_id_component(props: &TicketProps) -> Html {
"Archived" => "Archiviert", "Archived" => "Archiviert",
_ => "Ungültiger Status" _ => "Ungültiger Status"
}}</p> }}</p>
<p><strong>{ "Name: "}</strong>{ format!{"{} {}", t.user_first_name, t.user_last_name } }</p>
<form {onsubmit}> <form {onsubmit}>
<label>{ "Status ändern" } <label>{ "Status ändern" }
@@ -339,6 +350,10 @@ pub fn all_tickets_component() -> Html {
let tickets = use_state(|| Vec::<Ticket>::new()); let tickets = use_state(|| Vec::<Ticket>::new());
let error = use_state(|| None::<String>); let error = use_state(|| None::<String>);
let loading = use_state(|| false); let loading = use_state(|| false);
let user = use_state(|| ActiveUser {
id: None,
is_admin: false,
});
{ {
let tickets = tickets.clone(); let tickets = tickets.clone();
@@ -371,6 +386,37 @@ pub fn all_tickets_component() -> Html {
}); });
} }
{
let user = user.clone();
use_effect_with((), move |_| {
let user = user.clone();
spawn_local(async move {
if let Ok(response) = Request::get("/api/users/current")
.credentials(web_sys::RequestCredentials::Include)
.send()
.await
{
if response.status() == 200 {
if let Ok(json) = response.json::<serde_json::Value>().await {
let id = json
.get("data")
.and_then(|d| d.get("id"))
.and_then(|v| v.as_i64())
.and_then(|n| i16::try_from(n).ok());
let is_admin = json
.get("data")
.and_then(|d| d.get("is_admin"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
user.set(ActiveUser { id, is_admin });
}
}
}
});
|| ()
});
}
if *loading { if *loading {
html! {<p>{ "Loading" }</p>} html! {<p>{ "Loading" }</p>}
} else if let Some(e) = &*error { } else if let Some(e) = &*error {
@@ -378,10 +424,17 @@ pub fn all_tickets_component() -> Html {
} else { } else {
html! { html! {
<ul> <ul>
{ for tickets.iter().map(|t| html! { { for tickets.iter().filter(|t| if user.is_admin { true } else if let Some(uid) = user.id { t.user_id == uid } else { false }).map(|t| html! {
<li key={t.id.to_string()}> <li key={t.id.to_string()}>
<h3>{ format!("{} - #{}", t.betreff, t.id) }</h3> <h3>{ format!("{} - #{}", t.betreff, t.id) }</h3>
<p>{ &t.description }</p> <p>{ &t.description }</p>
<p>{ match t.status.as_str() {
"ToDo" => "Zu tun",
"InProgress" => "In Bearbeitung",
"Completed" => "Erledigt",
"Archived" => "Archiviert",
_ => "Ungültiger Status"
}}</p>
</li> </li>
})} })}
</ul> </ul>
+8 -2
View File
@@ -4,6 +4,7 @@ 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;
use yew::prelude::*; use yew::prelude::*;
use yew_router::prelude::use_navigator;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct User { pub struct User {
@@ -177,6 +178,7 @@ pub fn login_component() -> Html {
let loading = use_state(|| false); let loading = use_state(|| false);
let error = use_state(|| String::new()); let error = use_state(|| String::new());
let success = use_state(|| false); let success = use_state(|| false);
let navigator = use_navigator().unwrap();
let onsubmit = { let onsubmit = {
let username = username.clone(); let username = username.clone();
@@ -184,6 +186,7 @@ pub fn login_component() -> Html {
let loading = loading.clone(); let loading = loading.clone();
let error = error.clone(); let error = error.clone();
let success = success.clone(); let success = success.clone();
let navigator = navigator.clone();
Callback::from(move |e: SubmitEvent| { Callback::from(move |e: SubmitEvent| {
e.prevent_default(); e.prevent_default();
@@ -192,6 +195,7 @@ pub fn login_component() -> Html {
let loading = loading.clone(); let loading = loading.clone();
let error = error.clone(); let error = error.clone();
let success = success.clone(); let success = success.clone();
let navigator = navigator.clone();
loading.set(true); loading.set(true);
error.set(String::new()); error.set(String::new());
@@ -211,7 +215,10 @@ pub fn login_component() -> Html {
loading.set(false); loading.set(false);
match response { match response {
Ok(r) if r.status() == 200 => success.set(true), Ok(r) if r.status() == 200 => {
success.set(true);
navigator.push(&crate::Route::Home);
}
Ok(r) => { Ok(r) => {
let text = r.text().await.unwrap_or_else(|_| "unknown".into()); let text = r.text().await.unwrap_or_else(|_| "unknown".into());
error.set(format!("HTTP {}: {}", r.status(), text)); error.set(format!("HTTP {}: {}", r.status(), text));
@@ -242,7 +249,6 @@ pub fn login_component() -> Html {
})} })}
/> />
<button type="submit" disabled={*loading}>{ if *loading { "Logging in..." } else { "Login" } }</button> <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> } if !error.is_empty() { <p style="color:red">{(*error).clone()}</p> }
</form> </form>
} }