This commit is contained in:
2026-05-25 17:43:07 +02:00
parent d1576ae8fa
commit e2cfb61caa
11 changed files with 56 additions and 85 deletions

View File

@@ -43,7 +43,7 @@ pub fn encode_token(header: &Header, id: String, key: &EncodingKey) -> String {
expires: expires as usize, expires: expires as usize,
}; };
let token = encode(header, &claims, key); let token = encode(header, &claims, key);
return token.expect("token return failed"); token.expect("token return failed")
} }
/// Decodes and validates a JSON Web Token (JWT). /// Decodes and validates a JSON Web Token (JWT).
@@ -77,5 +77,5 @@ pub fn decode_token(token: String, key: &DecodingKey) -> Result<Claims, (StatusC
(StatusCode::UNAUTHORIZED, Json(error)) (StatusCode::UNAUTHORIZED, Json(error))
})? })?
.claims; .claims;
return Ok(claims); Ok(claims)
} }

View File

@@ -48,13 +48,7 @@ pub async fn validate_token(
.headers() .headers()
.get(header::AUTHORIZATION) .get(header::AUTHORIZATION)
.and_then(|header| header.to_str().ok()) .and_then(|header| header.to_str().ok())
.and_then(|value| { .and_then(|value| value.strip_prefix("Bearer ").map(|s| s.to_owned()))
if value.starts_with("Bearer ") {
Some(value[7..].to_owned())
} else {
None
}
})
}); });
let token = token.ok_or_else(|| { let token = token.ok_or_else(|| {
@@ -77,7 +71,7 @@ pub async fn validate_token(
(status, Json(error)) (status, Json(error))
})?; })?;
let uuid = (&claims.sub).parse::<i16>().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"
@@ -143,13 +137,7 @@ pub async fn validate_admin(
.headers() .headers()
.get(header::AUTHORIZATION) .get(header::AUTHORIZATION)
.and_then(|header| header.to_str().ok()) .and_then(|header| header.to_str().ok())
.and_then(|value| { .and_then(|value| value.strip_prefix("Bearer ").map(|s| s.to_owned()))
if value.starts_with("Bearer ") {
Some(value[7..].to_owned())
} else {
None
}
})
}); });
let token = token.ok_or_else(|| { let token = token.ok_or_else(|| {
@@ -172,7 +160,7 @@ pub async fn validate_admin(
(status, Json(error)) (status, Json(error))
})?; })?;
let uuid = (&claims.sub).parse::<i16>().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"

View File

@@ -60,7 +60,7 @@ pub async fn create_user(
) )
})?; })?;
if let Some(_) = exist_check { if exist_check.is_some() {
return Err(( return Err((
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
Json(json!({"status": "error", "message": "user already exists"})), Json(json!({"status": "error", "message": "user already exists"})),
@@ -90,10 +90,10 @@ pub async fn create_user(
})?; })?;
if user.rows_affected() < 1 { if user.rows_affected() < 1 {
return Err(( Err((
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"status": "error", "message": "Error creating user"})), Json(json!({"status": "error", "message": "Error creating user"})),
)); ))
} else { } else {
Ok(Json(json!({"status": "success", "result": "User created"}))) Ok(Json(json!({"status": "success", "result": "User created"})))
} }
@@ -151,7 +151,7 @@ pub async fn login(
let pwd_hash = PasswordHash::new(&user.pwd); let pwd_hash = PasswordHash::new(&user.pwd);
let valid_pwd = Argon2::default() let valid_pwd = Argon2::default()
.verify_password(&request.pwd.as_bytes(), &pwd_hash.unwrap()) .verify_password(request.pwd.as_bytes(), &pwd_hash.unwrap())
.is_ok(); .is_ok();
if !valid_pwd { if !valid_pwd {
@@ -340,7 +340,7 @@ pub async fn get_users(
let response = users let response = users
.iter() .iter()
.map(|user| filter_user(&user)) .map(filter_user)
.collect::<Vec<FilteredUser>>(); .collect::<Vec<FilteredUser>>();
let json_respnse = json!(response); let json_respnse = json!(response);
Ok(Json(json_respnse)) Ok(Json(json_respnse))
@@ -372,22 +372,22 @@ pub async fn get_user_by_id(
match query { match query {
Ok(user) => { Ok(user) => {
let response = serde_json::json!(filter_user(&user)); let response = serde_json::json!(filter_user(&user));
return Ok(Json(response)); Ok(Json(response))
} }
Err(sqlx::Error::RowNotFound) => { Err(sqlx::Error::RowNotFound) => {
let error_response = serde_json::json!({ let error_response = serde_json::json!({
"status": "fail", "status": "fail",
"message": format!("User with ID {} not found", id) "message": format!("User with ID {} not found", id)
}); });
return Err((StatusCode::NOT_FOUND, Json(error_response))); Err((StatusCode::NOT_FOUND, Json(error_response)))
} }
Err(e) => { Err(e) => {
return Err(( Err((
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"status": "error", "message": format!("{:?}", e)})), Json(json!({"status": "error", "message": format!("{:?}", e)})),
)); ))
} }
}; }
} }
/// Updates an existing user's information. /// Updates an existing user's information.
@@ -409,7 +409,7 @@ pub async fn get_user_by_id(
/// # Security Note /// # Security Note
/// - Passwords are hashed using Argon2 before storage. /// - Passwords are hashed using Argon2 before storage.
/// - This endpoint requires admin privileges (enforced by middleware via /// - This endpoint requires admin privileges (enforced by middleware via
/// [`validate_admin`](crate::cookie::validation::validate_admin)). /// [`validate_admin`](crate::cookie::validation::validate_admin)).
pub async fn update_user( pub async fn update_user(
Path(id): Path<i32>, Path(id): Path<i32>,
State(data): State<Arc<AppState>>, State(data): State<Arc<AppState>>,
@@ -572,10 +572,10 @@ pub async fn setup_initial_admin(
})?; })?;
if user.rows_affected() < 1 { if user.rows_affected() < 1 {
return Err(( Err((
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"status": "error", "message": "Error creating admin user"})), Json(json!({"status": "error", "message": "Error creating admin user"})),
)); ))
} else { } else {
Ok(Json( Ok(Json(
json!({"status": "success", "result": "Admin user created"}), json!({"status": "success", "result": "Admin user created"}),
@@ -616,6 +616,6 @@ pub fn filter_user(user: &User) -> FilteredUser {
first_name: user.first_name.clone(), first_name: user.first_name.clone(),
last_name: user.last_name.clone(), last_name: user.last_name.clone(),
username: user.username.clone(), username: user.username.clone(),
is_admin: user.is_admin.clone(), is_admin: user.is_admin,
} }
} }

View File

@@ -230,22 +230,22 @@ pub async fn get_ticket_by_id(
user_last_name: row.get("last_name"), user_last_name: row.get("last_name"),
}; };
let response = serde_json::json!(ticket_response); let response = serde_json::json!(ticket_response);
return Ok(Json(response)); Ok(Json(response))
} }
Err(sqlx::Error::RowNotFound) => { Err(sqlx::Error::RowNotFound) => {
let error_response = serde_json::json!({ let error_response = serde_json::json!({
"status": "fail", "status": "fail",
"message": format!("Ticket with ID {} not found", id) "message": format!("Ticket with ID {} not found", id)
}); });
return Err((StatusCode::NOT_FOUND, Json(error_response))); Err((StatusCode::NOT_FOUND, Json(error_response)))
} }
Err(e) => { Err(e) => {
return Err(( Err((
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"status": "error", "message": format!("{:?}", e)})), Json(json!({"status": "error", "message": format!("{:?}", e)})),
)); ))
} }
}; }
} }
/// Updates a ticket's status. /// Updates a ticket's status.

View File

@@ -64,7 +64,7 @@ async fn main() {
let database_url = &env.db_url; let database_url = &env.db_url;
// Establish connection pool to PostgreSQL // Establish connection pool to PostgreSQL
let pool = match PgPoolOptions::new().connect(&database_url).await { let pool = match PgPoolOptions::new().connect(database_url).await {
Ok(pool) => { Ok(pool) => {
println!("Database connection successful"); println!("Database connection successful");
pool pool

View File

@@ -8,7 +8,7 @@ services:
environment: environment:
- POSTGRES_PASSWORD=tickets - POSTGRES_PASSWORD=tickets
volumes: volumes:
- pg_data:/var/lib/postregsql/pg_data - pg_data:/var/lib/postgresql/pg_data
volumes: volumes:
pg_data: pg_data:

View File

@@ -67,7 +67,7 @@ pub fn initial_admin_setup() -> Html {
let username = use_state(|| "".to_string()); let username = use_state(|| "".to_string());
let pwd = use_state(|| "".to_string()); let pwd = use_state(|| "".to_string());
let pwd_confirm = use_state(|| "".to_string()); let pwd_confirm = use_state(|| "".to_string());
let error = use_state(|| String::new()); let error = use_state(String::new);
let success = use_state(|| false); let success = use_state(|| false);
let loading = use_state(|| false); let loading = use_state(|| false);
let admin_check_done = use_state(|| false); let admin_check_done = use_state(|| false);

View File

@@ -133,7 +133,7 @@ pub fn sidebar_state_provider(props: &SidebarProps) -> Html {
Callback::from(move |v: bool| { Callback::from(move |v: bool| {
state.set(SidebarExpandState { state.set(SidebarExpandState {
ticket_open: v, ticket_open: v,
users_open: (*state).users_open, users_open: state.users_open,
}) })
}) })
}; };
@@ -141,10 +141,10 @@ pub fn sidebar_state_provider(props: &SidebarProps) -> Html {
let toggle_tickets = { let toggle_tickets = {
let state = state.clone(); let state = state.clone();
Callback::from(move |_| { Callback::from(move |_| {
let current = (*state).ticket_open; let current = state.ticket_open;
state.set(SidebarExpandState { state.set(SidebarExpandState {
ticket_open: !current, ticket_open: !current,
users_open: (*state).users_open, users_open: state.users_open,
}); });
}) })
}; };
@@ -153,7 +153,7 @@ pub fn sidebar_state_provider(props: &SidebarProps) -> Html {
let state = state.clone(); let state = state.clone();
Callback::from(move |v: bool| { Callback::from(move |v: bool| {
state.set(SidebarExpandState { state.set(SidebarExpandState {
ticket_open: (*state).ticket_open, ticket_open: state.ticket_open,
users_open: v, users_open: v,
}) })
}) })
@@ -162,9 +162,9 @@ pub fn sidebar_state_provider(props: &SidebarProps) -> Html {
let toggle_users = { let toggle_users = {
let state = state.clone(); let state = state.clone();
Callback::from(move |_| { Callback::from(move |_| {
let current = (*state).users_open; let current = state.users_open;
state.set(SidebarExpandState { state.set(SidebarExpandState {
ticket_open: (*state).ticket_open, ticket_open: state.ticket_open,
users_open: !current, users_open: !current,
}); });
}) })

View File

@@ -265,10 +265,7 @@ pub fn submit_ticket_component() -> Html {
Err(_) => None, Err(_) => None,
} }
} else { } else {
match raw_trim.parse::<i16>() { raw_trim.parse::<i16>().ok()
Ok(n) => Some(n),
Err(_) => None,
}
} }
}; };
@@ -421,7 +418,6 @@ pub fn ticket_by_id_component(props: &TicketProps) -> Html {
} }
let onsubmit = { let onsubmit = {
let status = status.clone(); let status = status.clone();
let id = id.clone();
let error = error.clone(); let error = error.clone();
Callback::from(move |e: SubmitEvent| { Callback::from(move |e: SubmitEvent| {
@@ -437,7 +433,6 @@ pub fn ticket_by_id_component(props: &TicketProps) -> Html {
.unwrap_or_else(|| (*status).clone()); .unwrap_or_else(|| (*status).clone());
status.set(new_status.clone()); status.set(new_status.clone());
let id = id.clone();
let error = error.clone(); let error = error.clone();
spawn_local(async move { spawn_local(async move {
@@ -464,7 +459,6 @@ pub fn ticket_by_id_component(props: &TicketProps) -> Html {
let deleting = deleting.clone(); let deleting = deleting.clone();
let delete_error = delete_error.clone(); let delete_error = delete_error.clone();
let ticket_state = ticket.clone(); let ticket_state = ticket.clone();
let id = id;
Callback::from(move |e: MouseEvent| { Callback::from(move |e: MouseEvent| {
e.prevent_default(); e.prevent_default();
@@ -589,7 +583,7 @@ pub fn ticket_by_id_component(props: &TicketProps) -> Html {
/// ``` /// ```
#[component(AllTickets)] #[component(AllTickets)]
pub fn all_tickets_component() -> Html { 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 { let user = use_state(|| ActiveUser {
@@ -605,7 +599,7 @@ pub fn all_tickets_component() -> Html {
use_effect_with((), move |_| { use_effect_with((), move |_| {
loading.set(true); loading.set(true);
spawn_local(async move { spawn_local(async move {
let url = format!("/api/tickets"); let url = "/api/tickets".to_string();
match Request::get(&url).send().await { match Request::get(&url).send().await {
Ok(response) if response.status() == 200 => { Ok(response) if response.status() == 200 => {
match response.json::<Vec<Ticket>>().await { match response.json::<Vec<Ticket>>().await {
@@ -637,9 +631,8 @@ pub fn all_tickets_component() -> Html {
.credentials(web_sys::RequestCredentials::Include) .credentials(web_sys::RequestCredentials::Include)
.send() .send()
.await .await
{ && response.status() == 200
if response.status() == 200 { && let Ok(json) = response.json::<serde_json::Value>().await {
if let Ok(json) = response.json::<serde_json::Value>().await {
let id = json let id = json
.get("data") .get("data")
.and_then(|d| d.get("id")) .and_then(|d| d.get("id"))
@@ -652,8 +645,6 @@ pub fn all_tickets_component() -> Html {
.unwrap_or(false); .unwrap_or(false);
user.set(ActiveUser { id, is_admin }); user.set(ActiveUser { id, is_admin });
} }
}
}
}); });
|| () || ()
}); });
@@ -729,7 +720,7 @@ pub fn all_tickets_component() -> Html {
/// ``` /// ```
#[component(ArchivedTickets)] #[component(ArchivedTickets)]
pub fn archived_tickets_component() -> Html { pub fn archived_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 { let user = use_state(|| ActiveUser {
@@ -745,7 +736,7 @@ pub fn archived_tickets_component() -> Html {
use_effect_with((), move |_| { use_effect_with((), move |_| {
loading.set(true); loading.set(true);
spawn_local(async move { spawn_local(async move {
let url = format!("/api/tickets"); let url = "/api/tickets".to_string();
match Request::get(&url).send().await { match Request::get(&url).send().await {
Ok(response) if response.status() == 200 => { Ok(response) if response.status() == 200 => {
match response.json::<Vec<Ticket>>().await { match response.json::<Vec<Ticket>>().await {
@@ -777,9 +768,8 @@ pub fn archived_tickets_component() -> Html {
.credentials(web_sys::RequestCredentials::Include) .credentials(web_sys::RequestCredentials::Include)
.send() .send()
.await .await
{ && response.status() == 200
if response.status() == 200 { && let Ok(json) = response.json::<serde_json::Value>().await {
if let Ok(json) = response.json::<serde_json::Value>().await {
let id = json let id = json
.get("data") .get("data")
.and_then(|d| d.get("id")) .and_then(|d| d.get("id"))
@@ -792,8 +782,6 @@ pub fn archived_tickets_component() -> Html {
.unwrap_or(false); .unwrap_or(false);
user.set(ActiveUser { id, is_admin }); user.set(ActiveUser { id, is_admin });
} }
}
}
}); });
|| () || ()
}); });

View File

@@ -287,7 +287,7 @@ pub fn login_component() -> Html {
let username = use_state(|| "".to_string()); let username = use_state(|| "".to_string());
let pwd = use_state(|| "".to_string()); let pwd = use_state(|| "".to_string());
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 navigator = use_navigator().unwrap();
@@ -401,7 +401,7 @@ pub fn login_component() -> Html {
/// ``` /// ```
#[component(AllUsers)] #[component(AllUsers)]
pub fn all_users_component() -> Html { pub fn all_users_component() -> Html {
let users = use_state(|| Vec::<FilteredUser>::new()); let users = use_state(Vec::<FilteredUser>::new);
let error = use_state(|| None::<String>); let error = use_state(|| None::<String>);
let loading = use_state(|| false); let loading = use_state(|| false);
@@ -413,7 +413,7 @@ pub fn all_users_component() -> Html {
use_effect_with((), move |_| { use_effect_with((), move |_| {
loading.set(true); loading.set(true);
spawn_local(async move { spawn_local(async move {
let url = format!("/api/users"); let url = "/api/users".to_string();
match Request::get(&url).send().await { match Request::get(&url).send().await {
Ok(response) if response.status() == 200 => { Ok(response) if response.status() == 200 => {
match response.json::<Vec<FilteredUser>>().await { match response.json::<Vec<FilteredUser>>().await {
@@ -562,7 +562,7 @@ pub fn user_by_id_component(props: &UserProps) -> Html {
let last_name = use_state(|| "".to_string()); let last_name = use_state(|| "".to_string());
let username = use_state(|| "".to_string()); let username = use_state(|| "".to_string());
let make_admin = use_state(|| false); let make_admin = use_state(|| false);
let new_pwd = use_state(|| String::new()); let new_pwd = use_state(String::new);
let saving = use_state(|| false); let saving = use_state(|| false);
let save_error = use_state(|| None::<String>); let save_error = use_state(|| None::<String>);
let save_success = use_state(|| false); let save_success = use_state(|| false);
@@ -612,11 +612,10 @@ pub fn user_by_id_component(props: &UserProps) -> Html {
let save_error = save_error.clone(); let save_error = save_error.clone();
let save_success = save_success.clone(); let save_success = save_success.clone();
let user_state = user_state.clone(); let user_state = user_state.clone();
let id = id;
spawn_local(async move { spawn_local(async move {
let payload = UserUpdateScheme { let payload = UserUpdateScheme {
id: id, id,
first_name, first_name,
last_name, last_name,
username, username,
@@ -656,7 +655,6 @@ pub fn user_by_id_component(props: &UserProps) -> Html {
let deleting = deleting.clone(); let deleting = deleting.clone();
let delete_error = delete_error.clone(); let delete_error = delete_error.clone();
let user_state = user.clone(); // or ticket let user_state = user.clone(); // or ticket
let id = id;
Callback::from(move |e: MouseEvent| { Callback::from(move |e: MouseEvent| {
e.prevent_default(); e.prevent_default();

View File

@@ -156,7 +156,7 @@ fn day_counts(partials: &[TicketPartial]) -> [usize; 7] {
chrono::Weekday::Sun => 6, chrono::Weekday::Sun => 6,
}; };
occ[idx] += 1; occ[idx] += 1;
current = current + chrono::Duration::days(1); current += chrono::Duration::days(1);
} }
occ occ
@@ -236,7 +236,7 @@ pub fn diagnostics_component() -> Html {
/// ``` /// ```
#[component(TicketCount)] #[component(TicketCount)]
pub fn ticket_count_component() -> Html { pub fn ticket_count_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 { let user = use_state(|| ActiveUser {
@@ -252,7 +252,7 @@ pub fn ticket_count_component() -> Html {
use_effect_with((), move |_| { use_effect_with((), move |_| {
loading.set(true); loading.set(true);
spawn_local(async move { spawn_local(async move {
let url = format!("/api/tickets"); let url = "/api/tickets".to_string();
match Request::get(&url).send().await { match Request::get(&url).send().await {
Ok(response) if response.status() == 200 => { Ok(response) if response.status() == 200 => {
match response.json::<Vec<Ticket>>().await { match response.json::<Vec<Ticket>>().await {
@@ -284,9 +284,8 @@ pub fn ticket_count_component() -> Html {
.credentials(web_sys::RequestCredentials::Include) .credentials(web_sys::RequestCredentials::Include)
.send() .send()
.await .await
{ && response.status() == 200
if response.status() == 200 { && let Ok(json) = response.json::<serde_json::Value>().await {
if let Ok(json) = response.json::<serde_json::Value>().await {
let id = json let id = json
.get("data") .get("data")
.and_then(|d| d.get("id")) .and_then(|d| d.get("id"))
@@ -299,8 +298,6 @@ pub fn ticket_count_component() -> Html {
.unwrap_or(false); .unwrap_or(false);
user.set(ActiveUser { id, is_admin }); user.set(ActiveUser { id, is_admin });
} }
}
}
}); });
|| () || ()
}); });
@@ -316,7 +313,7 @@ pub fn ticket_count_component() -> Html {
.iter() .iter()
.filter(|t| { .filter(|t| {
status_conditions(t) status_conditions(t)
&& (user.is_admin || user.id.map_or(false, |uid| t.user_id == uid)) && (user.is_admin || (user.id == Some(t.user_id)))
}) })
.count(); .count();
html! { html! {
@@ -357,8 +354,8 @@ pub fn ticket_count_component() -> Html {
/// ``` /// ```
#[component(SubmitStats)] #[component(SubmitStats)]
pub fn submit_stats_component() -> Html { pub fn submit_stats_component() -> Html {
let tickets = use_state(|| Vec::<TicketPartial>::new()); let tickets = use_state(Vec::<TicketPartial>::new);
let users = use_state(|| Vec::<UserPartial>::new()); let users = use_state(Vec::<UserPartial>::new);
let error = use_state(|| None::<String>); let error = use_state(|| None::<String>);
let loading = use_state(|| false); let loading = use_state(|| false);