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
This commit is contained in:
@@ -12,23 +12,28 @@ pub struct Error {
|
||||
|
||||
pub fn encode_token(header: &Header, id: String, key: &EncodingKey) -> String {
|
||||
let now = chrono::Utc::now();
|
||||
let issued = now.timestamp() as usize;
|
||||
let expires = (now + chrono::Duration::minutes(60)).timestamp() as usize;
|
||||
let expires = (now + chrono::Duration::minutes(60)).timestamp();
|
||||
let claims: Claims = Claims {
|
||||
subject: id,
|
||||
issued: issued,
|
||||
expires: expires,
|
||||
sub: id,
|
||||
issued: now.timestamp() as usize,
|
||||
expires: expires as usize,
|
||||
};
|
||||
let token = encode(header, &claims, key);
|
||||
return token.expect("token return failed");
|
||||
}
|
||||
|
||||
pub fn decode_token(token: String, key: &DecodingKey) -> Result<Claims, (StatusCode, Json<Error>)> {
|
||||
let claims = decode::<Claims>(&token, key, &Validation::default())
|
||||
.map_err(|_| {
|
||||
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||
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 {
|
||||
status: "error",
|
||||
message: "Invalid Token".to_string(),
|
||||
message,
|
||||
};
|
||||
(StatusCode::UNAUTHORIZED, Json(error))
|
||||
})?
|
||||
|
||||
@@ -15,6 +15,7 @@ use serde_json::json;
|
||||
use crate::{
|
||||
AppState,
|
||||
cookie::jwt::decode_token,
|
||||
handlers::auth::filter_user,
|
||||
models::{LoginScheme, User},
|
||||
};
|
||||
|
||||
@@ -53,9 +54,15 @@ pub async fn validate_token(
|
||||
token,
|
||||
&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!({
|
||||
"status": "error",
|
||||
"message": "Invalid user id"
|
||||
@@ -83,6 +90,6 @@ pub async fn validate_token(
|
||||
(StatusCode::UNAUTHORIZED, Json(error))
|
||||
})?;
|
||||
|
||||
request.extensions_mut().insert(user);
|
||||
request.extensions_mut().insert(filter_user(&user));
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
|
||||
@@ -156,13 +156,14 @@ pub async fn logout() -> Result<impl IntoResponse, (StatusCode, Json<serde_json:
|
||||
}
|
||||
|
||||
pub async fn get_current_user(
|
||||
Extension(state): Extension<User>,
|
||||
Extension(user): Extension<FilteredUser>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||
let response = json!({
|
||||
"status": "success",
|
||||
"data": json!({
|
||||
"first_name": filter_user(&state).first_name,
|
||||
"last_name": filter_user(&state).last_name
|
||||
"id": user.id,
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name
|
||||
})
|
||||
});
|
||||
|
||||
@@ -302,7 +303,7 @@ pub async fn update_user(
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
fn filter_user(user: &User) -> FilteredUser {
|
||||
pub fn filter_user(user: &User) -> FilteredUser {
|
||||
FilteredUser {
|
||||
id: user.id,
|
||||
first_name: user.first_name.clone(),
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
Extension, Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use serde_json::json;
|
||||
use sqlx::query;
|
||||
use sqlx::{query, Row};
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
models::{Ticket, TicketCreateScheme, TicketResponse, TicketUpdateScheme},
|
||||
models::{FilteredUser, Ticket, TicketCreateScheme, TicketResponse, TicketUpdateScheme},
|
||||
};
|
||||
|
||||
pub async fn create_ticket(
|
||||
Extension(user): Extension<FilteredUser>,
|
||||
State(data): State<Arc<AppState>>,
|
||||
Json(body): Json<TicketCreateScheme>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||
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.description.to_string())
|
||||
.bind(body.betreff.to_string())
|
||||
.bind(body.room)
|
||||
.bind(user.id)
|
||||
.execute(&data.db)
|
||||
.await;
|
||||
|
||||
@@ -69,8 +71,11 @@ pub async fn get_tickets(
|
||||
State(data): State<Arc<AppState>>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||
println!("get_tickets called");
|
||||
let tickets = sqlx::query_as::<_, Ticket>(
|
||||
r#"SELECT * FROM tickets WHERE status <> 'Archived' ORDER BY date DESC"#,
|
||||
let tickets = sqlx::query(
|
||||
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)
|
||||
.await
|
||||
@@ -83,10 +88,21 @@ pub async fn get_tickets(
|
||||
})?;
|
||||
println!("Tickets fetched");
|
||||
|
||||
let ticket_response = tickets
|
||||
let ticket_response: Vec<TicketResponse> = tickets
|
||||
.iter()
|
||||
.map(|ticket| filter_record(&ticket))
|
||||
.collect::<Vec<TicketResponse>>();
|
||||
.map(|row| 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);
|
||||
println!("Json contructed");
|
||||
@@ -97,15 +113,32 @@ pub async fn get_ticket_by_id(
|
||||
Path(id): Path<i32>,
|
||||
State(data): State<Arc<AppState>>,
|
||||
) -> 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(
|
||||
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.id = $1"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(&data.db)
|
||||
.await;
|
||||
|
||||
match query {
|
||||
Ok(ticket) => {
|
||||
let ticket_response = serde_json::json!(filter_record(&ticket));
|
||||
return Ok(Json(ticket_response));
|
||||
Ok(row) => {
|
||||
let ticket_response = 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"),
|
||||
};
|
||||
let response = serde_json::json!(ticket_response);
|
||||
return Ok(Json(response));
|
||||
}
|
||||
Err(sqlx::Error::RowNotFound) => {
|
||||
let error_response = serde_json::json!({
|
||||
@@ -148,7 +181,12 @@ pub async fn edit_ticket(
|
||||
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(
|
||||
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.id = $1"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(&data.db)
|
||||
.await
|
||||
@@ -159,23 +197,24 @@ pub async fn edit_ticket(
|
||||
)
|
||||
})?;
|
||||
|
||||
let ticket_response = serde_json::json!({
|
||||
"ticket": filter_record(&updated_ticket),
|
||||
let ticket_response = TicketResponse {
|
||||
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"
|
||||
});
|
||||
|
||||
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 date: chrono::NaiveDateTime,
|
||||
pub user_id: i16,
|
||||
pub user_first_name: String,
|
||||
pub user_last_name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone, sqlx::FromRow)]
|
||||
@@ -75,7 +77,7 @@ pub struct LoginScheme {
|
||||
pub pwd: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FilteredUser {
|
||||
pub id: i16,
|
||||
pub first_name: String,
|
||||
@@ -86,7 +88,10 @@ pub struct FilteredUser {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Claims {
|
||||
pub subject: String,
|
||||
#[serde(alias = "subject")]
|
||||
pub sub: String,
|
||||
#[serde(rename = "iat", alias = "issued", default)]
|
||||
pub issued: usize,
|
||||
#[serde(rename = "exp", alias = "expires", default)]
|
||||
pub expires: usize,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
Router, middleware,
|
||||
routing::{get, post},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
cookie::validation::validate_token,
|
||||
handlers::{
|
||||
auth::{
|
||||
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 {
|
||||
Router::new()
|
||||
let protected_routes = Router::new()
|
||||
.route("/api/tickets", get(get_tickets))
|
||||
.route("/api/tickets/create", post(create_ticket))
|
||||
.route(
|
||||
@@ -27,7 +28,6 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
.patch(edit_ticket),
|
||||
)
|
||||
.route("/api/register", post(create_user))
|
||||
.route("/api/login", post(login))
|
||||
.route("/api/logout", get(logout))
|
||||
.route("/api/users", get(get_users))
|
||||
.route("/api/users/current", get(get_current_user))
|
||||
@@ -35,5 +35,13 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
"/api/users/{id}",
|
||||
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)
|
||||
}
|
||||
|
||||
58
frontend/src/auth.rs
Normal file
58
frontend/src/auth.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
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: bool,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct ProtectedRouteProps {
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
#[component(ProtectedRoute)]
|
||||
pub fn protected_route(props: &ProtectedRouteProps) -> Html {
|
||||
let is_authenticated = use_state(|| None::<bool>);
|
||||
|
||||
{
|
||||
let is_authenticated = is_authenticated.clone();
|
||||
use_effect_with((), move |_| {
|
||||
let is_authenticated = is_authenticated.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 {
|
||||
is_authenticated.set(Some(true));
|
||||
} else {
|
||||
is_authenticated.set(Some(false));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
web_sys::console::log_1(&format!("Auth check error: {:?}", err).into());
|
||||
is_authenticated.set(Some(false));
|
||||
}
|
||||
}
|
||||
});
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
match *is_authenticated {
|
||||
None => html! { <div>{"Loading..."}</div> },
|
||||
Some(true) => props.children.clone().into(),
|
||||
Some(false) => {
|
||||
html! {
|
||||
<Redirect<crate::Route> to={crate::Route::Login} />
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
mod pages;
|
||||
mod auth;
|
||||
use crate::pages::*;
|
||||
use crate::auth::ProtectedRoute;
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
@@ -28,15 +30,43 @@ enum Route {
|
||||
|
||||
fn switch(route: Route) -> Html {
|
||||
match route {
|
||||
Route::Home => html! { <basic_pages::Home/>},
|
||||
Route::Home => html! {
|
||||
<ProtectedRoute>
|
||||
<basic_pages::Home/>
|
||||
</ProtectedRoute>
|
||||
},
|
||||
Route::NotFound => html! { <basic_pages::NotFound/> },
|
||||
Route::Ticket => html! { <ticket::SubmitTicket/> },
|
||||
Route::TicketById { id } => html! { <ticket::TicketByID id={id}/> },
|
||||
Route::AllTickets => html! { <ticket::AllTickets/> },
|
||||
Route::Register => html! { <user::Register/> },
|
||||
Route::Ticket => html! {
|
||||
<ProtectedRoute>
|
||||
<ticket::SubmitTicket/>
|
||||
</ProtectedRoute>
|
||||
},
|
||||
Route::TicketById { id } => html! {
|
||||
<ProtectedRoute>
|
||||
<ticket::TicketByID {id}/>
|
||||
</ProtectedRoute>
|
||||
},
|
||||
Route::AllTickets => html! {
|
||||
<ProtectedRoute>
|
||||
<ticket::AllTickets/>
|
||||
</ProtectedRoute>
|
||||
},
|
||||
Route::Register => html! {
|
||||
<ProtectedRoute>
|
||||
<user::Register/>
|
||||
</ProtectedRoute>
|
||||
},
|
||||
Route::Login => html! { <user::Login/> },
|
||||
Route::AllUsers => html! {<user::AllUsers/>},
|
||||
Route::UserByID { id } => html! { <user::UserByID id={id}/> },
|
||||
Route::AllUsers => html! {
|
||||
<ProtectedRoute>
|
||||
<user::AllUsers/>
|
||||
</ProtectedRoute>
|
||||
},
|
||||
Route::UserByID { id } => html! {
|
||||
<ProtectedRoute>
|
||||
<user::UserByID {id}/>
|
||||
</ProtectedRoute>
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user