Compare commits
4
Commits
b672fe9768
...
4fc1c2eeb8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fc1c2eeb8 | ||
|
|
fdd2e2a757 | ||
|
|
802e5876f7 | ||
|
|
e54be14526 |
@@ -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))
|
||||||
})?
|
})?
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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/> },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user