7 Commits
Author SHA1 Message Date
schn33fuchs d029c5a347 Some improvements 2026-05-16 19:57:17 +02:00
schn33fuchs b1869595ac Fix
Weekday bars now at the bottom of everything
2026-05-16 19:57:04 +02:00
schn33fuchs 0d4a6c4711 favicon 2026-05-16 19:56:47 +02:00
schn33fuchs 726a581a9e Component Fix
easy fix carried over removed now
2026-05-16 19:56:25 +02:00
schn33fuchs e99026e3d6 Styles
Its more beautifull now
2026-05-16 19:54:13 +02:00
schn33fuchs b25e045b99 Styles better 2026-05-16 19:51:08 +02:00
schn33fuchs 99c5aa613c .gitignore 2026-05-13 16:59:32 +02:00
11 changed files with 62 additions and 169 deletions
+2 -2
View File
@@ -57,8 +57,8 @@ pub fn encode_token(header: &Header, id: String, key: &EncodingKey) -> String {
/// - `key`: The `DecodingKey` used to verify the JWT's signature. /// - `key`: The `DecodingKey` used to verify the JWT's signature.
/// ///
/// # Returns /// # Returns
/// - `200 OK`: If the token is successfully decoded and verified, returns the extracted `Claims`. /// - `Ok(Claims)`: If the token is successfully decoded and verified, returns the extracted `Claims`.
/// - `401 UNAUTHORIZED`: If the token is invalid, expired, or cannot be decoded, /// - `Err((StatusCode, Json<Error>))`: If the token is invalid, expired, or cannot be decoded,
/// returns an `UNAUTHORIZED` status code along with a JSON error message. /// returns an `UNAUTHORIZED` status code along with a JSON error message.
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 mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256); let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
+9 -8
View File
@@ -26,13 +26,13 @@ use crate::{AppState, cookie::jwt::decode_token, handlers::auth::filter_user, mo
/// ///
/// # Arguments /// # Arguments
/// - `cookies`: The `CookieJar` from the request, used to extract the `token` cookie. /// - `cookies`: The `CookieJar` from the request, used to extract the `token` cookie.
/// - `request`: The incoming HTTP request, which will have user data injected into its extensions. /// - `State(data)`: Application state containing `AppState` for database access and `token_secret`.
/// - `mut request`: The incoming HTTP request, which will have user data injected into its extensions.
/// - `next`: The next middleware or handler in the chain. /// - `next`: The next middleware or handler in the chain.
/// ///
/// # Returns /// # Returns
/// - `200 OK`: If validation succeeds, the request proceeds to the next handler. /// - `Ok(impl IntoResponse)`: If validation succeeds, the request proceeds to the next handler.
/// - `401 UNAUTHORIZED`: If validating the user fails. /// - `Err((StatusCode, Json<serde_json::Value>))`: An error response if validation fails.
/// - `500 INTERNAL SERVER ERROR`: If the database query fails
pub async fn validate_token( pub async fn validate_token(
cookies: CookieJar, cookies: CookieJar,
State(data): State<Arc<AppState>>, State(data): State<Arc<AppState>>,
@@ -119,13 +119,14 @@ pub async fn validate_token(
/// ///
/// # Arguments /// # Arguments
/// - `cookies`: The `CookieJar` from the request. /// - `cookies`: The `CookieJar` from the request.
/// - `request`: The incoming HTTP request, which will have admin user data injected. /// - `State(data)`: Application state containing `AppState`.
/// - `mut request`: The incoming HTTP request, which will have admin user data injected.
/// - `next`: The next middleware or handler in the chain. /// - `next`: The next middleware or handler in the chain.
/// ///
/// # Returns /// # Returns
/// - `200 OK`: If validation and admin check succeed, the request proceeds. /// - `Ok(impl IntoResponse)`: If validation and admin check succeed, the request proceeds.
/// - `401 UNAUTHORIZED`: An error response if validation fails or the user is not an admin. /// - `Err((StatusCode, Json<serde_json::Value>))`: An error response if validation fails
/// - `500 INTERNAL SERVER ERROR`: If the databse query fails /// or the user is not an admin.
pub async fn validate_admin( pub async fn validate_admin(
cookies: CookieJar, cookies: CookieJar,
State(data): State<Arc<AppState>>, State(data): State<Arc<AppState>>,
+26 -14
View File
@@ -26,7 +26,7 @@ use crate::{
/// before being stored. Only administrators can create new users. /// before being stored. Only administrators can create new users.
/// ///
/// # Arguments /// # Arguments
/// - `request`: Json with [UserCreateScheme] as it's format /// - `request`: User creation details including first/last name, username, admin flag, and password
/// ///
/// # Returns /// # Returns
/// - `200 OK` on successful user creation /// - `200 OK` on successful user creation
@@ -110,10 +110,10 @@ pub async fn create_user(
/// The token is valid for 1 hour. /// The token is valid for 1 hour.
/// ///
/// # Arguments /// # Arguments
/// - `request`: Login credentials in Json format using the [LoginScheme] /// - `request`: Login credentials (username, password)
/// ///
/// # Returns /// # Returns
/// - `200 OK` with JSON containing token and [FilteredUser] info /// - `200 OK` with JSON containing token and filtered user info
/// - `400 Bad Request` if username not found or password invalid /// - `400 Bad Request` if username not found or password invalid
/// - `500 Internal Server Error` if database query fails /// - `500 Internal Server Error` if database query fails
/// ///
@@ -231,7 +231,7 @@ pub async fn logout() -> Result<impl IntoResponse, (StatusCode, Json<serde_json:
/// Useful for frontends to display logged-in user info or verify authentication. /// Useful for frontends to display logged-in user info or verify authentication.
/// ///
/// # Returns /// # Returns
/// - `200 OK` with the [FilteredUser] in Json format /// - `200 OK` with user data (excluding password)
/// - Automatically returns `401 Unauthorized` if not authenticated (middleware) /// - Automatically returns `401 Unauthorized` if not authenticated (middleware)
/// ///
/// # Example Response /// # Example Response
@@ -306,7 +306,7 @@ pub async fn delete_user(
/// Password hashes are not included in the response. /// Password hashes are not included in the response.
/// ///
/// # Returns /// # Returns
/// - `200 OK` with array of [FilteredUser] objects /// - `200 OK` with array of FilteredUser objects
/// - `500 Internal Server Error` if database query fails /// - `500 Internal Server Error` if database query fails
/// ///
/// # Example Response /// # Example Response
@@ -352,11 +352,15 @@ pub async fn get_users(
/// Retrieves a single user's details by their ID. /// Retrieves a single user's details by their ID.
/// ///
/// This endpoint allows fetching a specific user's information. It returns a `FilteredUser`
/// object, ensuring sensitive data like password hashes are not exposed.
///
/// # Arguments /// # Arguments
/// - `id`: The ID of the user to retrieve, extracted from the URL path. /// - `Path(id)`: The ID of the user to retrieve, extracted from the URL path.
/// - `State(data)`: Application state containing `AppState` for database access.
/// ///
/// # Returns /// # Returns
/// - `200 OK` with a [FilteredUser] JSON object if the user is found. /// - `200 OK` with a `FilteredUser` JSON object if the user is found.
/// - `404 Not Found` if a user with the given ID does not exist. /// - `404 Not Found` if a user with the given ID does not exist.
/// - `500 Internal Server Error` if a database query error occurs. /// - `500 Internal Server Error` if a database query error occurs.
/// ///
@@ -392,12 +396,17 @@ pub async fn get_user_by_id(
/// Updates an existing user's information. /// Updates an existing user's information.
/// ///
/// This endpoint allows administrators to modify a user's `first_name`, `last_name`,
/// `username`, `is_admin` status, and optionally their password. If `new_pwd` in the
/// request body is an empty string, the user's password remains unchanged.
///
/// # Arguments /// # Arguments
/// - `id`: The ID of the user to update, extracted from the URL path. /// - `Path(id)`: The ID of the user to update, extracted from the URL path.
/// - `body`: [UserUpdateScheme] containing the fields to update. /// - `State(data)`: Application state containing `AppState` for database access.
/// - `Json(body)`: `UserUpdateScheme` containing the fields to update.
/// ///
/// # Returns /// # Returns
/// - `200 OK` with the [FilteredUser] object of the updated user. /// - `200 OK` with the `FilteredUser` object of the updated user.
/// - `404 Not Found` if a user with the given ID does not exist. /// - `404 Not Found` if a user with the given ID does not exist.
/// - `500 Internal Server Error` if a database query or password hashing error occurs. /// - `500 Internal Server Error` if a database query or password hashing error occurs.
/// ///
@@ -461,8 +470,11 @@ pub async fn update_user(
/// Checks if any administrator user exists in the system. /// Checks if any administrator user exists in the system.
/// ///
/// This endpoint is used during initialization to determine if the setup page should be displayed.
/// It counts all users with `is_admin = true` in the database.
///
/// # Returns /// # Returns
/// - `200 OK` On successful database query /// - `200 OK` with JSON: `{"has_admin": bool}` - Whether at least one admin exists
/// - `500 Internal Server Error` if database query fails /// - `500 Internal Server Error` if database query fails
/// ///
/// # Example Response /// # Example Response
@@ -495,7 +507,7 @@ pub async fn check_admin_exists(
/// with proper authorization. /// with proper authorization.
/// ///
/// # Arguments /// # Arguments
/// - `request`: [UserCreateScheme] as a Json value /// - `request`: User creation details (first_name, last_name, username, password)
/// ///
/// # Returns /// # Returns
/// - `200 OK` with success message if admin account created /// - `200 OK` with success message if admin account created
@@ -582,10 +594,10 @@ pub async fn setup_initial_admin(
/// serializing User objects. /// serializing User objects.
/// ///
/// # Arguments /// # Arguments
/// - `user`: Reference to the internal [User] struct containing password hash /// - `user`: Reference to the internal User struct containing password hash
/// ///
/// # Returns /// # Returns
/// [FilteredUser] with only safe-to-share information: /// FilteredUser with only safe-to-share information:
/// - `id`: User ID /// - `id`: User ID
/// - `first_name`, `last_name`: User name /// - `first_name`, `last_name`: User name
/// - `username`: Login username /// - `username`: Login username
+8 -7
View File
@@ -21,7 +21,7 @@ use crate::{
/// ///
/// # Arguments /// # Arguments
/// - `user`: Authenticated user (extracted from JWT token) /// - `user`: Authenticated user (extracted from JWT token)
/// - `body`: Json with the [TicketCreateScheme] as it's format /// - `body`: Ticket details (category, subject, description, room)
/// ///
/// # Returns /// # Returns
/// - `200 OK` on successful creation /// - `200 OK` on successful creation
@@ -95,16 +95,17 @@ pub async fn delete_ticket(
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
/// Retrieves all tickets. /// Retrieves all non-archived tickets.
/// ///
/// Returns a list of all tickets with user information denormalized for easier rendering. /// Returns a list of all active tickets with user information denormalized for easier rendering.
/// Tickets are ordered by creation date (newest first). /// Tickets are ordered by creation date (newest first).
/// ///
/// # Filtering /// # Filtering
/// - Excludes tickets with status "Archived"
/// - Uses LEFT JOIN to include creator information /// - Uses LEFT JOIN to include creator information
/// ///
/// # Returns /// # Returns
/// - `200 OK` with array of [TicketResponse] objects /// - `200 OK` with array of TicketResponse objects
/// - `500 Internal Server Error` if database query fails /// - `500 Internal Server Error` if database query fails
/// ///
/// # Example Response /// # Example Response
@@ -173,7 +174,7 @@ pub async fn get_tickets(
/// - `id`: Ticket ID to retrieve /// - `id`: Ticket ID to retrieve
/// ///
/// # Returns /// # Returns
/// - `200 OK` with [TicketResponse] object /// - `200 OK` with TicketResponse object
/// - `404 Not Found` if ticket doesn't exist /// - `404 Not Found` if ticket doesn't exist
/// - `500 Internal Server Error` if database error occurs /// - `500 Internal Server Error` if database error occurs
/// ///
@@ -246,10 +247,10 @@ pub async fn get_ticket_by_id(
/// ///
/// # Arguments /// # Arguments
/// - `id`: Ticket ID to update /// - `id`: Ticket ID to update
/// - `body`: Update payload as a Json array with [TicketUpdateScheme] as it's format /// - `body`: Update payload containing new status
/// ///
/// # Returns /// # Returns
/// - `200 OK` with updated [TicketResponse] /// - `200 OK` with updated TicketResponse
/// - `500 Internal Server Error` if ticket not found or database error /// - `500 Internal Server Error` if ticket not found or database error
/// ///
/// # Typical Status Flow /// # Typical Status Flow
+4 -38
View File
@@ -9,53 +9,19 @@ mod models;
/// Axum router configuration with all routes and middleware /// Axum router configuration with all routes and middleware
mod router; mod router;
use std::sync::{ use std::sync::Arc;
Arc,
atomic::{AtomicI64, Ordering},
};
use axum::{ use axum::http::{
Json, HeaderValue, Method,
http::{ header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
HeaderValue, Method, StatusCode,
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
},
}; };
use dotenv::dotenv; use dotenv::dotenv;
use router::create_router; use router::create_router;
use serde::Serialize;
use sqlx::{PgPool, postgres::PgPoolOptions}; use sqlx::{PgPool, postgres::PgPoolOptions};
use tower_http::cors::CorsLayer; use tower_http::cors::CorsLayer;
use crate::env::Env; use crate::env::Env;
/// A variable to count the problems fixed by a very easy solution
static EASY_FIX_COUNT: AtomicI64 = AtomicI64::new(0);
/// The Response struct for the [EASY_FIX_COUNT]
#[derive(Serialize)]
struct CounterResp {
value: i64,
}
/// Gets the [EASY_FIX_COUNT] value
///
/// # Returns
/// - `200 OK` with the value on success
async fn get_count() -> Json<CounterResp> {
let v = EASY_FIX_COUNT.load(Ordering::SeqCst);
Json(CounterResp { value: v })
}
/// Incremets the [EASY_FIX_COUNT] value by one
///
/// # Returns
/// - `200 OK` with the new value on success
async fn increment() -> Result<Json<CounterResp>, StatusCode> {
let new = EASY_FIX_COUNT.fetch_add(1, Ordering::SeqCst) + 1;
Ok(Json(CounterResp { value: new }))
}
/// Shared application state passed to all route handlers. /// Shared application state passed to all route handlers.
/// ///
/// Contains the database connection pool and environment configuration. /// Contains the database connection pool and environment configuration.
+2 -5
View File
@@ -8,15 +8,13 @@ use axum::{
use crate::{ use crate::{
AppState, AppState,
cookie::validation::{validate_admin, validate_token}, cookie::validation::{validate_admin, validate_token},
get_count,
handlers::{ handlers::{
auth::{ auth::{
check_admin_exists, create_user, delete_user, get_current_user, get_user_by_id, check_admin_exists, create_user, delete_user, get_current_user, get_user_by_id, get_users, login, logout,
get_users, login, logout, setup_initial_admin, update_user, setup_initial_admin, update_user,
}, },
ticket::{create_ticket, delete_ticket, edit_ticket, get_ticket_by_id, get_tickets}, ticket::{create_ticket, delete_ticket, edit_ticket, get_ticket_by_id, get_tickets},
}, },
increment,
}; };
/// Creates the complete router with all API endpoints. /// Creates the complete router with all API endpoints.
@@ -75,7 +73,6 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/api/tickets/create", post(create_ticket)) .route("/api/tickets/create", post(create_ticket))
.route("/api/logout", get(logout)) .route("/api/logout", get(logout))
.route("/api/users/current", get(get_current_user)) .route("/api/users/current", get(get_current_user))
.route("/api/count", get(get_count).post(increment))
.layer(middleware::from_fn_with_state( .layer(middleware::from_fn_with_state(
state.clone(), state.clone(),
validate_token, validate_token,
+1 -1
View File
@@ -1,6 +1,6 @@
[serve] [serve]
# The address to serve on LAN. # The address to serve on LAN.
addresses = ["127.0.0.1"]#, "192.168.178.56"] #,"10.150.9.116"] addresses = ["127.0.0.1"] #,"10.150.9.116"]
# The address to serve on WAN. # The address to serve on WAN.
# address = "0.0.0.0" # address = "0.0.0.0"
# The port to serve on. # The port to serve on.
+5 -5
View File
@@ -91,7 +91,7 @@ fn sidebar_shell(props: &SidebarShellProps) -> Html {
} }
} }
/// Props for the [AdminCheckWrapper] component. /// Props for the AdminCheckWrapper component.
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
pub struct AdminCheckWrapperProps { pub struct AdminCheckWrapperProps {
pub children: Children, pub children: Children,
@@ -166,7 +166,7 @@ fn admin_check_wrapper(props: &AdminCheckWrapperProps) -> Html {
/// The main routing logic for the application. /// The main routing logic for the application.
/// ///
/// This function takes a [Route] enum variant and returns the corresponding HTML /// This function takes a `Route` enum variant and returns the corresponding HTML
/// content to be rendered. It acts as a central dispatcher for the application's /// content to be rendered. It acts as a central dispatcher for the application's
/// navigation. /// navigation.
/// ///
@@ -259,12 +259,12 @@ fn switch(route: Route) -> Html {
/// The root component of the Yew application. /// The root component of the Yew application.
/// ///
/// This component sets up the application's routing using `yew-router`'s /// This component sets up the application's routing using `yew-router`'s
/// `BrowserRouter` and `Switch` components. All other application content /// [`BrowserRouter`] and [`Switch`] components. All other application content
/// is rendered based on the current route. /// is rendered based on the current route.
/// ///
/// # Structure /// # Structure
/// - `BrowserRouter`: Enables client-side routing. /// - [`BrowserRouter`]: Enables client-side routing.
/// - `Switch`: Renders components based on the matched [Route]. /// - [`Switch`]: Renders components based on the matched [`Route`].
#[component(App)] #[component(App)]
pub fn app() -> Html { pub fn app() -> Html {
html! { html! {
+4 -1
View File
@@ -3,7 +3,6 @@ use wasm_bindgen_futures::spawn_local;
use yew::prelude::*; use yew::prelude::*;
use yew_router::prelude::*; use yew_router::prelude::*;
/// A macro for dequoting a Json value returned from the backend
#[macro_export] #[macro_export]
macro_rules! dequote { macro_rules! dequote {
($str:expr) => { ($str:expr) => {
@@ -98,6 +97,10 @@ pub fn not_found_component() -> Html {
/// A component displayed when a user attempts to access a page for which they do not have sufficient permissions. /// A component displayed when a user attempts to access a page for which they do not have sufficient permissions.
/// ///
/// It informs the user about the access restriction and provides instructions to contact
/// a specific person ("Herr Winter") if they believe this is an error.
/// It also includes a link to return to the home page.
///
/// # Example /// # Example
/// ```rust /// ```rust
/// html! { /// html! {
-25
View File
@@ -169,31 +169,6 @@ pub fn submit_ticket_component() -> Html {
let valid_rooms: HashSet<i16> = VALID_ROOMS.iter().copied().collect(); let valid_rooms: HashSet<i16> = VALID_ROOMS.iter().copied().collect();
{
let message = "Bevor sie zum Support weitergeleitet werden prüfen sie ob:
- Ob das Problem durch Neustarten gelößt wird
- Ob sie die richtigen Anmeldedaten genutzt habem
- Alle notwendigen Kabel eingesteckt sind
Wenn es funktioniert hat klicken sie bitte \"Abbrechen\""
.to_string();
use_effect(move || {
if let Some(win) = web_sys::window() {
let _ = if win.confirm_with_message(&message).unwrap() {
} else {
spawn_local(async move {
let _ = Request::post("/api/count")
.credentials(web_sys::RequestCredentials::Include)
.send()
.await;
});
let _ = win.location().set_href("/");
};
}
|| ()
});
}
let onsubmit = { let onsubmit = {
let category = category.clone(); let category = category.clone();
let betreff = betreff.clone(); let betreff = betreff.clone();
+1 -63
View File
@@ -9,12 +9,6 @@ use yew::prelude::*;
use crate::dequote; use crate::dequote;
use crate::pages::ticket::{ActiveUser, Ticket}; use crate::pages::ticket::{ActiveUser, Ticket};
/// The response struct for the [EasyFixCount]
#[derive(Deserialize)]
struct CountResponse {
value: i64,
}
/// A partial representation of a ticket, containing only the fields necessary for statistical analysis. /// A partial representation of a ticket, containing only the fields necessary for statistical analysis.
/// ///
/// This struct is used to efficiently retrieve and process ticket data for diagnostics /// This struct is used to efficiently retrieve and process ticket data for diagnostics
@@ -23,7 +17,6 @@ struct CountResponse {
/// # Fields /// # Fields
/// - `date`: The creation date and time of the ticket in UTC. /// - `date`: The creation date and time of the ticket in UTC.
/// - `room`: The room number associated with the ticket. /// - `room`: The room number associated with the ticket.
#[derive(Debug, Deserialize, Clone, PartialEq)] #[derive(Debug, Deserialize, Clone, PartialEq)]
struct TicketPartial { struct TicketPartial {
date: DateTime<Utc>, date: DateTime<Utc>,
@@ -31,14 +24,6 @@ struct TicketPartial {
user_id: i16, user_id: i16,
} }
/// A partial representation of a user, containing only the fields necessary for statistical analysis.
///
/// This struct is used to retrieve and process user data for diagnostics
///
/// # Fields
/// - `id`: The users id
/// - `first_name`: The users first name
/// - `last_name`: The users last name
#[derive(Debug, Deserialize, Clone, PartialEq)] #[derive(Debug, Deserialize, Clone, PartialEq)]
struct UserPartial { struct UserPartial {
id: i16, id: i16,
@@ -201,7 +186,6 @@ pub fn diagnostics_component() -> Html {
html! { html! {
<div> <div>
<TicketCount/> <TicketCount/>
<EasyFixCount/>
<SubmitStats/> <SubmitStats/>
</div> </div>
} }
@@ -481,7 +465,7 @@ pub fn submit_stats_component() -> Html {
/// A component that displays the total number of tickets per room. /// A component that displays the total number of tickets per room.
/// ///
/// This component takes a list of [`TicketPartial`] items and calculates the /// This component takes a list of `TicketPartial` items and calculates the
/// total number of tickets for each room. It then displays these totals /// total number of tickets for each room. It then displays these totals
/// in a sorted list with a bar chart visualization. /// in a sorted list with a bar chart visualization.
/// ///
@@ -591,49 +575,3 @@ fn user_total_component(props: &UserTotalProps) -> Html {
</div> </div>
} }
} }
/// A component for displaying how many problems were solved without creating a ticket
///
/// # Functionality
/// Fetches the value for the count from `/api/count` and parses it into a [`CountResponse`]
///
/// # Example
/// ``` rust
/// html! {
/// <EasyFixCount/>
/// }
/// ```
#[component(EasyFixCount)]
fn easy_fix_component() -> Html {
let count = use_state(|| 0);
let error = use_state(|| None::<String>);
{
let count = count.clone();
let error = error.clone();
use_effect_with((), move |_| {
spawn_local(async move {
match Request::get("/api/count").send().await {
Ok(resp) if resp.status() == 200 => match resp.json::<CountResponse>().await {
Ok(r) => count.set(r.value),
Err(e) => error.set(Some(format!("parse error: {}", e))),
},
Ok(resp) => error.set(Some(
resp.text()
.await
.unwrap_or_else(|_| format!("status {}", resp.status())),
)),
Err(e) => error.set(Some(format!("Network error: {}", e))),
}
});
|| ()
});
}
html! {
<div class="open-tickets">
<h2 class="left">{ "Probleme ohne Ticket gelößt" }</h2>
<h4 class="ticket_count center">{ *count }</h4>
</div>
}
}