From b87a6ff297c1cb796b97e61c679a12bfb871a00e Mon Sep 17 00:00:00 2001 From: schn33fuchs Date: Sat, 9 May 2026 23:00:15 +0200 Subject: [PATCH] Docs.rs comments Comments for generating the docs with cargo doc --- backend/src/cookie/jwt.rs | 38 +++++ backend/src/cookie/mod.rs | 5 + backend/src/cookie/validation.rs | 38 +++++ backend/src/env.rs | 26 ++++ backend/src/handlers/auth.rs | 228 ++++++++++++++++++++++++++++++ backend/src/handlers/mod.rs | 4 + backend/src/handlers/ticket.rs | 110 +++++++++++++- backend/src/main.rs | 37 +++++ backend/src/models.rs | 123 ++++++++++++++-- backend/src/router.rs | 31 ++++ frontend/src/auth.rs | 41 ++++++ frontend/src/lib.rs | 90 ++++++++++++ frontend/src/pages/basic_pages.rs | 40 ++++++ frontend/src/pages/mod.rs | 7 + frontend/src/pages/setup.rs | 48 +++++++ frontend/src/pages/sidebar.rs | 120 ++++++++++++++++ frontend/src/pages/ticket.rs | 163 +++++++++++++++++++++ frontend/src/pages/user.rs | 164 +++++++++++++++++++++ frontend/src/pages/utilities.rs | 149 +++++++++++++++++++ 19 files changed, 1447 insertions(+), 15 deletions(-) diff --git a/backend/src/cookie/jwt.rs b/backend/src/cookie/jwt.rs index 22302e6..2281777 100644 --- a/backend/src/cookie/jwt.rs +++ b/backend/src/cookie/jwt.rs @@ -4,12 +4,36 @@ use serde::{Deserialize, Serialize}; use crate::models::Claims; +/// Error response for JWT token operations. +/// +/// Returned when token encoding or decoding fails. Used in error responses +/// for invalid or expired tokens. +/// +/// # Fields +/// - `status`: HTTP status text (e.g., "error") +/// - `message`: Human-readable error description #[derive(Debug, Deserialize, Serialize)] pub struct Error { pub status: &'static str, pub message: String, } +/// Encodes user information into a JSON Web Token (JWT). +/// +/// This function creates a new JWT with the provided user ID as the subject, +/// sets the issued-at and expiration times (60 minutes from now), and signs it +/// using the given encoding key. +/// +/// # Arguments +/// - `header`: The JWT header, specifying the algorithm (e.g., HS256). +/// - `id`: The user ID (`String`) to be embedded as the subject (`sub`) claim. +/// - `key`: The `EncodingKey` used to sign the JWT. +/// +/// # Returns +/// A `String` representing the encoded JWT. +/// +/// # Panics +/// Panics if the token encoding fails for any reason (e.g., invalid key). pub fn encode_token(header: &Header, id: String, key: &EncodingKey) -> String { let now = chrono::Utc::now(); let expires = (now + chrono::Duration::minutes(60)).timestamp(); @@ -22,6 +46,20 @@ pub fn encode_token(header: &Header, id: String, key: &EncodingKey) -> String { return token.expect("token return failed"); } +/// Decodes and validates a JSON Web Token (JWT). +/// +/// This function attempts to decode a JWT string, validate its signature and claims +/// using the provided decoding key. It specifically ignores expiration (`validate_exp`) +/// and "not before" (`validate_nbf`) claims during validation. +/// +/// # Arguments +/// - `token`: The JWT string to decode. +/// - `key`: The `DecodingKey` used to verify the JWT's signature. +/// +/// # Returns +/// - `Ok(Claims)`: If the token is successfully decoded and verified, returns the extracted `Claims`. +/// - `Err((StatusCode, Json))`: If the token is invalid, expired, or cannot be decoded, +/// returns an `UNAUTHORIZED` status code along with a JSON error message. pub fn decode_token(token: String, key: &DecodingKey) -> Result)> { let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256); validation.validate_exp = false; diff --git a/backend/src/cookie/mod.rs b/backend/src/cookie/mod.rs index a20d7b2..eaa57da 100644 --- a/backend/src/cookie/mod.rs +++ b/backend/src/cookie/mod.rs @@ -1,2 +1,7 @@ +//! This module aggregates and re-exports all cookie-related functionalities, +//! including JWT handling and validation middleware. +//! +//! It serves as a central point for managing session and authentication cookies +//! within the application. pub mod jwt; pub mod validation; diff --git a/backend/src/cookie/validation.rs b/backend/src/cookie/validation.rs index 9994cd4..53166f4 100644 --- a/backend/src/cookie/validation.rs +++ b/backend/src/cookie/validation.rs @@ -19,6 +19,25 @@ use crate::{ models::{LoginScheme, User}, }; +/// Axum middleware to validate a JWT token present in cookies or Authorization header. +/// +/// This function extracts a JWT from the request (either from the `token` cookie or +/// the `Authorization: Bearer` header), decodes and validates it. If valid, it fetches +/// the corresponding user from the database and inserts a `FilteredUser` into the +/// request extensions for subsequent handlers to use. +/// +/// If the token is missing, invalid, or the user is not found, it returns an +/// appropriate error response (401 Unauthorized). +/// +/// # Arguments +/// - `cookies`: The `CookieJar` from the request, used to extract the `token` cookie. +/// - `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. +/// +/// # Returns +/// - `Ok(impl IntoResponse)`: If validation succeeds, the request proceeds to the next handler. +/// - `Err((StatusCode, Json))`: An error response if validation fails. pub async fn validate_token( cookies: CookieJar, State(data): State>, @@ -94,6 +113,25 @@ pub async fn validate_token( Ok(next.run(request).await) } +/// Axum middleware to validate JWT token and ensure the authenticated user has admin privileges. +/// +/// This middleware first performs all checks of `validate_token`: extracting, decoding, +/// and validating the JWT, and fetching the associated user from the database. +/// Additionally, it verifies that the fetched user has `is_admin` set to `true`. +/// +/// If the user is not authenticated or not an administrator, it returns an +/// appropriate error response (401 Unauthorized or 403 Forbidden). +/// +/// # Arguments +/// - `cookies`: The `CookieJar` from the request. +/// - `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. +/// +/// # Returns +/// - `Ok(impl IntoResponse)`: If validation and admin check succeed, the request proceeds. +/// - `Err((StatusCode, Json))`: An error response if validation fails +/// or the user is not an admin. pub async fn validate_admin( cookies: CookieJar, State(data): State>, diff --git a/backend/src/env.rs b/backend/src/env.rs index 632235d..f385bf8 100644 --- a/backend/src/env.rs +++ b/backend/src/env.rs @@ -1,11 +1,37 @@ +/// Environment configuration for the application. +/// +/// Loads required configuration from environment variables at startup. +/// All variables must be present or the application will panic. +/// +/// # Fields +/// - `db_url`: PostgreSQL database connection URL. +/// - `token_secret`: Secret key used to sign and verify JWT tokens. +/// - `origin`: Frontend origin URL for CORS policy. +/// +/// # Required Environment Variables +/// - `DATABASE_URL`: PostgreSQL connection string (e.g., `postgresql://user:pass@localhost/dbname`) +/// - `TOKEN_SECRET`: Secret key for JWT token signing (use a strong random string in production) +/// - `ORIGIN`: Frontend URL for CORS (e.g., `http://localhost:8080`) #[derive(Debug, Clone)] pub struct Env { + /// PostgreSQL database connection URL pub db_url: String, + /// Secret key used to sign and verify JWT tokens pub token_secret: String, + /// Frontend origin URL for CORS policy pub origin: String, } impl Env { + /// Loads environment configuration from system environment variables. + /// + /// Panics if any required variable is missing. + /// + /// # Example + /// ```ignore + /// let env = Env::load(); + /// // Environment must have DATABASE_URL, TOKEN_SECRET, and ORIGIN set + /// ``` pub fn load() -> Env { let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let token_secret = std::env::var("TOKEN_SECRET").expect("TOKEN_SECRET must be set"); diff --git a/backend/src/handlers/auth.rs b/backend/src/handlers/auth.rs index 063ddd2..2be8fc1 100644 --- a/backend/src/handlers/auth.rs +++ b/backend/src/handlers/auth.rs @@ -21,6 +21,26 @@ use crate::{ models::{FilteredUser, LoginScheme, User, UserCreateScheme, UserUpdateScheme}, }; +/// Registers a new user in the system. +/// +/// Creates a new user account with the provided credentials. The password is hashed using Argon2 +/// before being stored. Only administrators can create new users. +/// +/// # Arguments +/// - `request`: User creation details including first/last name, username, admin flag, and password +/// +/// # Returns +/// - `200 OK` on successful user creation +/// - `400 Bad Request` if username/password missing or user already exists +/// - `500 Internal Server Error` if database insertion fails +/// +/// # Password Hashing +/// Uses Argon2 with a cryptographically secure random salt: +/// ```ignore +/// let argon = Argon2::default(); +/// let salt = SaltString::generate(&mut OsRng); +/// let hashed_pwd = argon.hash_password(password.as_bytes(), &salt)?; +/// ``` pub async fn create_user( State(data): State>, Json(request): Json, @@ -84,6 +104,39 @@ pub async fn create_user( } } +/// Authenticates a user and creates a JWT token for session management. +/// +/// Verifies the provided username and password against stored credentials using Argon2 verification. +/// On successful authentication, generates a JWT token and sets it as an HTTP-only cookie. +/// The token is valid for 1 hour. +/// +/// # Arguments +/// - `request`: Login credentials (username, password) +/// +/// # Returns +/// - `200 OK` with JSON containing token and filtered user info +/// - `400 Bad Request` if username not found or password invalid +/// - `500 Internal Server Error` if database query fails +/// +/// # Security Features +/// - HTTP-only cookie prevents JavaScript access +/// - SameSite=Lax protects against CSRF attacks +/// - Password verification uses Argon2: +/// ```ignore +/// let valid_pwd = Argon2::default() +/// .verify_password(&request.pwd.as_bytes(), &pwd_hash.unwrap()) +/// .is_ok(); +/// ``` +/// - JWT token includes user ID and expiration timestamp +/// +/// # Example Response +/// ```json +/// { +/// "status": "success", +/// "token": "eyJ0eXAiOiJKV1QiLCJhbGc...", +/// "user": {"id": 1, "first_name": "Admin", "last_name": "User", "username": "admin", "is_admin": true} +/// } +/// ``` pub async fn login( State(data): State>, Json(request): Json, @@ -139,6 +192,24 @@ pub async fn login( Ok(response) } +/// Logs out the current user by invalidating their session cookie. +/// +/// Sets the authentication cookie to expire immediately (max_age = -1 hour) which causes +/// the browser to discard it. This effectively logs the user out without requiring server-side +/// session invalidation. +/// +/// # Returns +/// Always returns `200 OK` with success message and an expired cookie header +/// +/// # Security +/// - HTTP-only cookie prevents client-side manipulation +/// - SameSite=Lax protects against CSRF +/// - Setting max_age to negative value causes immediate expiration +/// +/// # Example Response +/// ```json +/// {"status": "success", "message": "successfully logged out"} +/// ``` pub async fn logout() -> Result)> { let cookie = Cookie::build(("token", "")) .path("/") @@ -155,6 +226,27 @@ pub async fn logout() -> Result, ) -> Result)> { @@ -171,6 +263,18 @@ pub async fn get_current_user( Ok(Json(response)) } +/// Deletes a user account from the system. +/// +/// Only admins can delete users. The user account and all associated data is removed. +/// Note: Tickets created by deleted users will have NULL user_id references. +/// +/// # Arguments +/// - `id`: User ID to delete +/// +/// # Returns +/// - `204 No Content` on successful deletion +/// - `404 Not Found` if user doesn't exist +/// - `500 Internal Server Error` if database error occurs pub async fn delete_user( Path(id): Path, State(data): State>, @@ -197,6 +301,34 @@ pub async fn delete_user( Ok(StatusCode::NO_CONTENT) } +/// Retrieves all users in the system. +/// +/// Only admins can call this endpoint. Returns all users sorted alphabetically by last name. +/// Password hashes are not included in the response. +/// +/// # Returns +/// - `200 OK` with array of FilteredUser objects +/// - `500 Internal Server Error` if database query fails +/// +/// # Example Response +/// ```json +/// [ +/// { +/// "id": 1, +/// "first_name": "Admin", +/// "last_name": "User", +/// "username": "admin", +/// "is_admin": true +/// }, +/// { +/// "id": 2, +/// "first_name": "Regular", +/// "last_name": "User", +/// "username": "regularuser", +/// "is_admin": false +/// } +/// ] +/// ``` pub async fn get_users( State(data): State>, ) -> Result)> { @@ -219,6 +351,20 @@ pub async fn get_users( Ok(Json(json_respnse)) } +/// 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 +/// - `Path(id)`: The ID of the user to retrieve, extracted from the URL path. +/// - `State(data)`: Application state containing `AppState` for database access. +/// +/// # Returns +/// - `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. +/// - `500 Internal Server Error` if a database query error occurs. +/// pub async fn get_user_by_id( Path(id): Path, State(data): State>, @@ -249,6 +395,25 @@ pub async fn get_user_by_id( }; } +/// 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 +/// - `Path(id)`: The ID of the user to update, extracted from the URL path. +/// - `State(data)`: Application state containing `AppState` for database access. +/// - `Json(body)`: `UserUpdateScheme` containing the fields to update. +/// +/// # Returns +/// - `200 OK` with the `FilteredUser` object of the updated user. +/// - `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. +/// +/// # Security Note +/// - Passwords are hashed using Argon2 before storage. +/// - This endpoint typically requires admin privileges (enforced by middleware). pub async fn update_user( Path(id): Path, State(data): State>, @@ -304,6 +469,19 @@ pub async fn update_user( Ok(Json(response)) } +/// 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 +/// - `200 OK` with JSON: `{"has_admin": bool}` - Whether at least one admin exists +/// - `500 Internal Server Error` if database query fails +/// +/// # Example Response +/// ```json +/// {"has_admin": false} +/// ``` pub async fn check_admin_exists( State(data): State>, ) -> Result)> { @@ -321,6 +499,30 @@ pub async fn check_admin_exists( Ok(Json(json!({"has_admin": has_admin}))) } +/// Creates the initial administrator account for a fresh system. +/// +/// This function handles the one-time setup of the first admin user. It checks that no admin exists +/// before allowing creation. This endpoint is only functional when the system has no administrators. +/// Once created, subsequent admin registrations must go through the normal `create_user` endpoint +/// with proper authorization. +/// +/// # Arguments +/// - `request`: User creation details (first_name, last_name, username, password) +/// +/// # Returns +/// - `200 OK` with success message if admin account created +/// - `400 Bad Request` if: +/// - Admin already exists +/// - Username or password is empty +/// - `500 Internal Server Error` if database insertion fails +/// +/// # Security Note +/// The password is hashed using Argon2 with a random salt before storage: +/// ```ignore +/// let argon = Argon2::default(); +/// let salt = SaltString::generate(&mut OsRng); +/// let hashed_pwd = argon.hash_password(request.pwd.as_bytes(), &salt)?; +/// ``` pub async fn setup_initial_admin( State(data): State>, Json(request): Json, @@ -382,6 +584,32 @@ pub async fn setup_initial_admin( } } +/// Converts a User with sensitive data into a FilteredUser safe for API responses. +/// +/// This function removes password hashes and other sensitive information before +/// returning user data to clients. Always use this helper instead of directly +/// serializing User objects. +/// +/// # Arguments +/// - `user`: Reference to the internal User struct containing password hash +/// +/// # Returns +/// FilteredUser with only safe-to-share information: +/// - `id`: User ID +/// - `first_name`, `last_name`: User name +/// - `username`: Login username +/// - `is_admin`: Admin privilege flag +/// +/// # Important +/// The password hash is explicitly **NOT** included in the output. This prevents +/// accidental exposure of sensitive authentication data. +/// +/// # Example +/// ```ignore +/// let user = get_user_from_db(1).await?; +/// let safe_user = filter_user(&user); +/// // safe_user can be safely serialized and sent to client +/// ``` pub fn filter_user(user: &User) -> FilteredUser { FilteredUser { id: user.id, diff --git a/backend/src/handlers/mod.rs b/backend/src/handlers/mod.rs index 2b84e86..4a03e5e 100644 --- a/backend/src/handlers/mod.rs +++ b/backend/src/handlers/mod.rs @@ -1,2 +1,6 @@ +//! This module aggregates and re-exports all API endpoint handler functions. +//! +//! It serves as a central point for managing the logic that responds to various +//! HTTP requests, categorizing handlers by their domain (e.g., authentication, tickets). pub mod auth; pub mod ticket; diff --git a/backend/src/handlers/ticket.rs b/backend/src/handlers/ticket.rs index e339009..3ae4830 100644 --- a/backend/src/handlers/ticket.rs +++ b/backend/src/handlers/ticket.rs @@ -14,6 +14,23 @@ use crate::{ models::{FilteredUser, TicketCreateScheme, TicketResponse, TicketUpdateScheme}, }; +/// Creates a new support ticket. +/// +/// Associates the ticket with the authenticated user and sets the current timestamp. +/// Tickets are automatically created with "open" status. +/// +/// # Arguments +/// - `user`: Authenticated user (extracted from JWT token) +/// - `body`: Ticket details (category, subject, description, room) +/// +/// # Returns +/// - `200 OK` on successful creation +/// - `500 Internal Server Error` if database insertion fails +/// +/// # Database Fields Set Automatically +/// - `user_id`: From authenticated user +/// - `status`: Defaults to "open" +/// - `date`: Current UTC timestamp pub async fn create_ticket( Extension(user): Extension, State(data): State>, @@ -41,6 +58,17 @@ pub async fn create_ticket( Ok(Json(response_status)) } +/// Deletes a ticket by ID. +/// +/// Only admins can delete tickets. Marks the ticket as deleted or removes from database. +/// +/// # Arguments +/// - `id`: Ticket ID to delete +/// +/// # Returns +/// - `204 No Content` on successful deletion +/// - `404 Not Found` if ticket doesn't exist +/// - `500 Internal Server Error` if database error occurs pub async fn delete_ticket( Path(id): Path, State(data): State>, @@ -67,10 +95,40 @@ pub async fn delete_ticket( Ok(StatusCode::NO_CONTENT) } +/// Retrieves all non-archived tickets. +/// +/// Returns a list of all active tickets with user information denormalized for easier rendering. +/// Tickets are ordered by creation date (newest first). +/// +/// # Filtering +/// - Excludes tickets with status "Archived" +/// - Uses LEFT JOIN to include creator information +/// +/// # Returns +/// - `200 OK` with array of TicketResponse objects +/// - `500 Internal Server Error` if database query fails +/// +/// # Example Response +/// ```json +/// [ +/// { +/// "id": 1, +/// "category": "maintenance", +/// "betreff": "Broken light", +/// "description": "Ceiling light not working", +/// "room": 101, +/// "status": "open", +/// "date": "2024-01-15T10:30:00Z", +/// "user_id": 5, +/// "user_first_name": "John", +/// "user_last_name": "Doe" +/// } +/// ] +/// ``` pub async fn get_tickets( State(data): State>, ) -> Result)> { - println!("get_tickets called"); + // Query tickets with denormalized user info, excluding archived tickets 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 @@ -86,8 +144,8 @@ pub async fn get_tickets( }); (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)) })?; - println!("Tickets fetched"); + // Transform raw database rows into TicketResponse structs let ticket_response: Vec = tickets .iter() .map(|row| TicketResponse { @@ -105,10 +163,36 @@ pub async fn get_tickets( .collect(); let json_response = serde_json::json!(ticket_response); - println!("Json contructed"); Ok(Json(json_response)) } +/// Retrieves a specific ticket by ID. +/// +/// Includes full ticket details and denormalized user information (creator name). +/// +/// # Arguments +/// - `id`: Ticket ID to retrieve +/// +/// # Returns +/// - `200 OK` with TicketResponse object +/// - `404 Not Found` if ticket doesn't exist +/// - `500 Internal Server Error` if database error occurs +/// +/// # Example Response +/// ```json +/// { +/// "id": 1, +/// "category": "maintenance", +/// "betreff": "Broken light in room 101", +/// "description": "The ceiling light is not working", +/// "room": 101, +/// "status": "open", +/// "date": "2024-01-15T10:30:00Z", +/// "user_id": 5, +/// "user_first_name": "John", +/// "user_last_name": "Doe" +/// } +/// ``` pub async fn get_ticket_by_id( Path(id): Path, State(data): State>, @@ -156,11 +240,30 @@ pub async fn get_ticket_by_id( }; } +/// Updates a ticket's status. +/// +/// Only admins can update ticket status. This is typically used to transition tickets +/// through their lifecycle (open → in_progress → resolved → archived). +/// +/// # Arguments +/// - `id`: Ticket ID to update +/// - `body`: Update payload containing new status +/// +/// # Returns +/// - `200 OK` with updated TicketResponse +/// - `500 Internal Server Error` if ticket not found or database error +/// +/// # Typical Status Flow +/// - `open`: Initial state, waiting for action +/// - `in_progress`: Currently being worked on +/// - `resolved`: Issue fixed +/// - `archived`: Closed/hidden from normal view pub async fn edit_ticket( Path(id): Path, State(data): State>, Json(body): Json, ) -> Result)> { + // Update the ticket status let update_result = sqlx::query(r#"UPDATE tickets SET status = $1 WHERE id = $2"#) .bind(body.status.to_owned()) .bind(id) @@ -181,6 +284,7 @@ pub async fn edit_ticket( return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))); } + // Fetch and return the updated ticket 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 diff --git a/backend/src/main.rs b/backend/src/main.rs index 4b05d7a..44ccb83 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,9 +1,16 @@ #![allow(unused_imports)] + +/// Cookie and JWT authentication utilities mod cookie; +/// Environment configuration loading mod env; +/// HTTP request handlers for all endpoints mod handlers; +/// Data structures for request/response serialization mod models; +/// Axum router configuration with all routes and middleware mod router; + use std::sync::Arc; use axum::{ @@ -23,16 +30,43 @@ use tower_http::cors::CorsLayer; use crate::env::Env; +/// Shared application state passed to all route handlers. +/// +/// Contains the database connection pool and environment configuration. +/// This is wrapped in Arc for thread-safe sharing across async tasks. +/// +/// # Fields +/// - `db`: PostgreSQL connection pool for database access +/// - `env`: Configuration loaded from environment variables pub struct AppState { db: PgPool, env: Env, } +/// Main application entry point. +/// +/// Initializes the server by: +/// 1. Loading environment variables from `.env` file +/// 2. Establishing database connection pool +/// 3. Configuring CORS policy for cross-origin requests +/// 4. Starting HTTP server on port 8001 +/// +/// # Server Configuration +/// - Binds to `0.0.0.0:8001` (all network interfaces) +/// - Allows: GET, POST, PATCH, DELETE methods +/// - Allows credentials and custom headers +/// - CORS origin configured from environment +/// +/// # Panics +/// - If environment loading fails +/// - If database connection fails #[tokio::main] async fn main() { dotenv().ok(); let env = Env::load(); let database_url = &env.db_url; + + // Establish connection pool to PostgreSQL let pool = match PgPoolOptions::new().connect(&database_url).await { Ok(pool) => { println!("Database connection successful"); @@ -44,18 +78,21 @@ async fn main() { } }; + // Configure CORS to allow requests from frontend let cors = CorsLayer::new() .allow_origin(env.origin.parse::().unwrap()) .allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE]) .allow_credentials(true) .allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]); + // Build router with all endpoints and apply CORS middleware let app = create_router(Arc::new(AppState { db: pool.clone(), env: env.clone(), })) .layer(cors); + // Start listening for incoming connections let listener = tokio::net::TcpListener::bind("0.0.0.0:8001").await.unwrap(); let _ = axum::serve(listener, app).await; } diff --git a/backend/src/models.rs b/backend/src/models.rs index 377d660..a8054d8 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -3,18 +3,36 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; use sqlx::{Decode, prelude::Type}; -// #[derive(Deserialize, Serialize, PartialEq, Debug, sqlx::FromRow)] -// pub struct Ticket { -// pub id: i32, -// pub category: String, -// pub betreff: String, -// pub description: String, -// pub room: i16, -// pub status: String, -// pub date: chrono::DateTime, -// pub user_id: i16, -// } - +/// API response for a ticket with user information. +/// +/// Returned by ticket endpoints. Includes denormalized user data for easier frontend rendering. +/// +/// # Fields +/// - `id`: Unique ticket identifier +/// - `category`: Ticket category/type +/// - `betreff`: Ticket subject line +/// - `description`: Detailed ticket description +/// - `room`: Room number associated with the issue +/// - `status`: Current ticket status (e.g., "open", "in_progress", "resolved") +/// - `date`: When the ticket was created (UTC timestamp) +/// - `user_id`: ID of the user who created the ticket +/// - `user_first_name`, `user_last_name`: User's name (denormalized for convenience) +/// +/// # Example +/// ```json +/// { +/// "id": 1, +/// "category": "maintenance", +/// "betreff": "Broken light in room 101", +/// "description": "The ceiling light is not working", +/// "room": 101, +/// "status": "open", +/// "date": "2024-01-15T10:30:00Z", +/// "user_id": 5, +/// "user_first_name": "John", +/// "user_last_name": "Doe" +/// } +/// ``` #[derive(Deserialize, Serialize, Debug, PartialEq)] pub struct TicketResponse { pub id: i32, @@ -29,6 +47,21 @@ pub struct TicketResponse { pub user_last_name: String, } +/// Complete user record from the database. +/// +/// Contains all user information including the password hash. +/// This should NEVER be sent directly to clients - always use `FilteredUser` instead. +/// +/// # Fields +/// - `id`: Unique user identifier +/// - `first_name`, `last_name`: User's full name +/// - `username`: Login username (must be unique) +/// - `is_admin`: Whether user has admin privileges +/// - `pwd`: Argon2 password hash (NEVER expose to clients) +/// +/// # Security Note +/// The `pwd` field contains the password hash and should never be included in API responses. +/// Use `filter_user()` to convert to `FilteredUser` for responses. #[derive(Deserialize, Serialize, PartialEq, Debug, Clone, sqlx::FromRow)] pub struct User { pub id: i16, @@ -39,6 +72,16 @@ pub struct User { pub pwd: String, } +/// Payload for creating a new ticket. +/// +/// Sent to `/api/tickets/create`. The backend automatically associates it with the +/// authenticated user and sets the creation timestamp. +/// +/// # Fields +/// - `category`: Ticket category/type +/// - `betreff`: Subject line for the ticket +/// - `description`: Detailed problem description +/// - `room`: Room number where the issue is located #[derive(Deserialize, Serialize, Debug)] pub struct TicketCreateScheme { pub category: String, @@ -47,11 +90,29 @@ pub struct TicketCreateScheme { pub room: i16, } +/// Payload for updating a ticket. +/// +/// Sent to `PATCH /api/tickets/{id}`. Currently only allows status updates. +/// Only admins can update tickets. +/// +/// # Fields +/// - `status`: New ticket status (e.g., "open", "in_progress", "resolved") #[derive(Deserialize, Serialize, Debug)] pub struct TicketUpdateScheme { pub status: String, } +/// Payload for updating user information. +/// +/// Sent to `PATCH /api/users/{id}`. Allows updating profile and admin status. +/// Only admins can update users. Empty password field means no password change. +/// +/// # Fields +/// - `id`: User ID to update +/// - `first_name`, `last_name`: Updated user name +/// - `username`: Updated login username +/// - `make_admin`: New admin privilege status +/// - `new_pwd`: New password (empty string = keep existing password) #[derive(Deserialize, Serialize, Debug)] pub struct UserUpdateScheme { pub id: i16, @@ -62,6 +123,17 @@ pub struct UserUpdateScheme { pub new_pwd: String, } +/// Payload for creating a new user account. +/// +/// Used in both admin registration (`/api/register`) and initial setup (`/api/setup-admin`). +/// The password is hashed server-side before storage using Argon2. +/// +/// # Fields +/// - `first_name`: User's first name +/// - `last_name`: User's last name +/// - `username`: Unique username for login +/// - `is_admin`: Whether to grant admin privileges (setup endpoint always sets this to true) +/// - `pwd`: Plain text password (hashed on server) #[derive(Deserialize, Serialize, Debug, sqlx::FromRow)] pub struct UserCreateScheme { pub first_name: String, @@ -71,12 +143,24 @@ pub struct UserCreateScheme { pub pwd: String, } +/// Payload for user login. +/// +/// Sent to `/api/login` endpoint with credentials. The backend verifies the password +/// against the stored Argon2 hash. +/// +/// # Security +/// The password is never stored in plain text - only the Argon2 hash is persisted. #[derive(Deserialize, Serialize, Debug)] pub struct LoginScheme { pub username: String, pub pwd: String, } +/// User information sent to clients, excluding password hashes. +/// +/// This is the safe version of User data that gets returned in API responses. +/// It never includes the password hash or JWT claims. Always use this for responses +/// to prevent leaking sensitive data. #[derive(Debug, Clone, Serialize)] pub struct FilteredUser { pub id: i16, @@ -86,12 +170,27 @@ pub struct FilteredUser { pub is_admin: bool, } +/// JWT token claims embedded in the session token. +/// +/// Contains user identification and token validity information. +/// Generated during login and verified by middleware on protected routes. +/// +/// # Fields +/// - `sub`: Subject - the user ID as a string +/// - `issued`: Unix timestamp when token was created +/// - `expires`: Unix timestamp when token expires (currently 1 hour from creation) +/// +/// # Token Lifetime +/// Tokens are valid for 1 hour. After expiration, user must log in again. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Claims { + /// Subject - typically the user ID #[serde(alias = "subject")] pub sub: String, + /// Issued at time (Unix timestamp) #[serde(rename = "iat", alias = "issued", default)] pub issued: usize, + /// Expiration time (Unix timestamp) #[serde(rename = "exp", alias = "expires", default)] pub expires: usize, } diff --git a/backend/src/router.rs b/backend/src/router.rs index 908e200..bc41ba5 100644 --- a/backend/src/router.rs +++ b/backend/src/router.rs @@ -17,6 +17,37 @@ use crate::{ }, }; +/// Creates the complete router with all API endpoints. +/// +/// The router is organized in layers for proper middleware application: +/// +/// ## Route Layers (from most to least restricted): +/// +/// ### Admin-Only Routes (requires admin privilege + valid token) +/// - `GET /api/tickets/{id}` - Get specific ticket details +/// - `DELETE /api/tickets/{id}` - Delete a ticket +/// - `PATCH /api/tickets/{id}` - Update ticket status +/// - `POST /api/register` - Create a new user +/// - `GET /api/users` - List all users +/// - `GET /api/users/{id}` - Get user details +/// - `DELETE /api/users/{id}` - Delete a user +/// - `PATCH /api/users/{id}` - Update user details +/// +/// ### Protected Routes (requires valid token) +/// - `GET /api/tickets` - List all tickets +/// - `POST /api/tickets/create` - Create a new ticket +/// - `GET /api/logout` - Logout user +/// - `GET /api/users/current` - Get current authenticated user +/// +/// ### Public Routes (no authentication required) +/// - `POST /api/login` - User login +/// - `GET /api/check-admin` - Check if admin exists (for setup detection) +/// - `POST /api/setup-admin` - Create initial admin account (only if no admin exists) +/// +/// # Middleware Stack +/// - Admin routes have `validate_admin` middleware +/// - Protected routes have `validate_token` middleware +/// - Public routes have no authentication requirements pub fn create_router(state: Arc) -> Router { let admin_routes = Router::new() .route( diff --git a/frontend/src/auth.rs b/frontend/src/auth.rs index cfcd547..18720fa 100644 --- a/frontend/src/auth.rs +++ b/frontend/src/auth.rs @@ -3,18 +3,59 @@ use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew_router::prelude::*; +/// Represents the authentication state of the current user. +/// +/// This struct holds information about whether a user is authenticated and if they +/// possess administrator privileges. +/// +/// # Fields +/// - `is_authenticated`: An `Option` indicating if the user is logged in. +/// `None` means the status is still being checked. +/// - `is_admin`: An `Option` indicating if the authenticated user is an administrator. +/// `None` means the admin status is still being checked or is not applicable. #[derive(Clone, Debug, PartialEq)] pub struct AuthState { pub is_authenticated: Option, pub is_admin: Option, } +/// Properties for the [`ProtectedRoute`] component. +/// +/// # Fields +/// - `children`: The child components that this protected route will render if access is granted. +/// - `admin_page`: A boolean flag indicating whether this route requires administrator privileges. +/// If `true`, the user must be authenticated AND be an administrator to access the `children`. #[derive(Properties, PartialEq)] pub struct ProtectedRouteProps { pub children: Children, pub admin_page: bool, } +/// A component that protects routes by enforcing authentication and optional administrator privileges. +/// +/// This component fetches the current user's authentication and admin status from the +/// `/api/users/current` endpoint upon mounting. Based on the `AuthState` and the +/// `admin_page` property, it either renders its children or redirects the user. +/// +/// # Behavior +/// - **Initial Load**: Displays "Loading..." while checking authentication status. +/// - **Not Authenticated**: Redirects to the login page (`crate::Route::Login`). +/// - **Authenticated**: +/// - If `admin_page` is `true`: +/// - If the user is an administrator (`is_admin: Some(true)`), it renders `children`. +/// - If the user is not an administrator (`is_admin: Some(false)`), it redirects to +/// the permission denied page (`crate::Route::PermissionDenied`). +/// - If admin status is still being checked (`is_admin: None`), it displays "Checking permissions...". +/// - If `admin_page` is `false`: It renders `children` directly, as only authentication is required. +/// +/// # Example Usage +/// ```ignore +/// html! { +/// +/// +/// +/// } +/// ``` #[component(ProtectedRoute)] pub fn protected_route(props: &ProtectedRouteProps) -> Html { let auth_state = use_state(|| AuthState { diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index 04252bf..8818e01 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -7,40 +7,75 @@ use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew_router::prelude::*; +/// Defines the application's various routes and their corresponding paths. +/// +/// This enum is used by `yew-router` to map URLs to specific components, +/// enabling navigation within the single-page application. #[derive(Clone, PartialEq, Routable)] enum Route { + /// The application's home page. #[at("/")] Home, + /// Route for submitting a new ticket. #[at("/ticket")] Ticket, + /// Route for viewing a specific ticket by its ID. #[at("/tickets/:id")] TicketById { id: i32 }, + /// Route for viewing all tickets. #[at("/tickets")] AllTickets, + /// Route for user registration. #[at("/register")] Register, + /// Route for user login. #[at("/login")] Login, + /// Route for the initial administrator setup. #[at("/setup")] Setup, + /// Route for viewing all users. #[at("/users")] AllUsers, + /// Route for viewing a specific user by their ID. #[at("/users/:id")] UserByID { id: i16 }, + /// Route for displaying diagnostics information (admin-only). #[at("/diagnostics")] Diagnostics, + /// Route displayed when a user attempts to access a page without sufficient permissions. #[at("/denied")] PermissionDenied, + /// Catch-all route for unmatched paths, leading to a 404 Not Found page. #[not_found] #[at("/404")] NotFound, } +/// Properties for the [`SidebarShell`] component. #[derive(Properties, PartialEq)] pub struct SidebarShellProps { + /// The child components to be rendered within the main content area of the shell. pub children: Children, } +/// A shell component that provides a consistent layout with a sidebar and a main content area. +/// +/// This component is designed to wrap page-specific content, ensuring that the sidebar +/// is always present for navigation. +/// +/// # Components +/// - [`sidebar::Sidebar`]: The navigation sidebar component. +/// - Main content area: Renders the `children` passed to this component. +/// +/// # Example +/// ```rust +/// html! { +/// +///

{"Your page content goes here."}

+///
+/// } +/// ``` #[component(SidebarShell)] fn sidebar_shell(props: &SidebarShellProps) -> Html { html! { @@ -53,11 +88,43 @@ fn sidebar_shell(props: &SidebarShellProps) -> Html { } } +/// Props for the AdminCheckWrapper component. #[derive(Properties, PartialEq)] pub struct AdminCheckWrapperProps { pub children: Children, } +/// Wrapper component that checks if an admin exists before rendering children. +/// +/// This component is used to gate access to pages that should only be accessible before +/// system initialization (e.g., login page). It performs an asynchronous check to the +/// `/api/check-admin` endpoint to determine system state. +/// +/// # Behavior +/// - **Loading**: Displays "Loading..." while checking admin status +/// - **No Admin**: Automatically redirects to `/setup` page to initialize +/// - **Admin Exists**: Renders the wrapped children (e.g., login page) +/// +/// # Example Usage +/// ```ignore +/// +/// +/// +/// ``` +/// +/// # Implementation Detail +/// The check happens in `use_effect_with` on component mount: +/// ```ignore +/// spawn_local(async move { +/// match Request::get("/api/check-admin").send().await { +/// Ok(resp) if resp.status() == 200 => { +/// let has_admin = data["has_admin"].as_bool().unwrap_or(false); +/// admin_exists.set(Some(has_admin)); +/// } +/// _ => admin_exists.set(Some(false)) +/// } +/// }); +/// ``` #[component(AdminCheckWrapper)] fn admin_check_wrapper(props: &AdminCheckWrapperProps) -> Html { let admin_exists = use_state(|| None::); @@ -94,6 +161,20 @@ fn admin_check_wrapper(props: &AdminCheckWrapperProps) -> Html { } } +/// The main routing logic for the application. +/// +/// 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 +/// navigation. +/// +/// Many routes are wrapped in a [`ProtectedRoute`] to enforce authentication +/// and authorization, and in a [`SidebarShell`] to maintain consistent layout. +/// +/// # Arguments +/// - `route`: The [`Route`] enum variant representing the current URL path. +/// +/// # Returns +/// An `Html` component that should be rendered for the given route. fn switch(route: Route) -> Html { match route { Route::Home => html! { @@ -165,6 +246,15 @@ fn switch(route: Route) -> Html { } } +/// The root component of the Yew application. +/// +/// This component sets up the application's routing using `yew-router`'s +/// [`BrowserRouter`] and [`Switch`] components. All other application content +/// is rendered based on the current route. +/// +/// # Structure +/// - [`BrowserRouter`]: Enables client-side routing. +/// - [`Switch`]: Renders components based on the matched [`Route`]. #[component(App)] pub fn app() -> Html { html! { diff --git a/frontend/src/pages/basic_pages.rs b/frontend/src/pages/basic_pages.rs index 9ee7a23..dd171d9 100644 --- a/frontend/src/pages/basic_pages.rs +++ b/frontend/src/pages/basic_pages.rs @@ -3,6 +3,23 @@ use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew_router::prelude::*; +/// The main home page component of the application. +/// +/// This component displays different content based on whether the logged-in user +/// is an administrator. It fetches the user's admin status from the +/// `/api/users/current` endpoint upon initialization. +/// +/// # Behavior +/// - **Loading**: Displays "Loading..." while fetching user data. +/// - **Admin User**: Renders the `TicketCount` utility component. +/// - **Non-Admin User**: Renders the `TicketCount` utility component. +/// +/// # Example +/// ```rust +/// html! { +/// +/// } +/// ``` #[component(Home)] pub fn home_component() -> Html { let is_admin = use_state(|| None::); @@ -44,6 +61,17 @@ pub fn home_component() -> Html { } } +/// A basic component displayed when a requested route does not match any defined paths (404 error). +/// +/// It provides a simple message indicating that the page was not found and includes +/// a link to navigate back to the home page. +/// +/// # Example +/// ```rust +/// html! { +/// +/// } +/// ``` #[component(NotFound)] pub fn not_found_component() -> Html { let message = "404 Not found"; @@ -55,6 +83,18 @@ 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. +/// +/// 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 +/// ```rust +/// html! { +/// +/// } +/// ``` #[component(PermissionDenied)] pub fn denied_component() -> Html { html! { diff --git a/frontend/src/pages/mod.rs b/frontend/src/pages/mod.rs index 6147f33..38dabb7 100644 --- a/frontend/src/pages/mod.rs +++ b/frontend/src/pages/mod.rs @@ -1,3 +1,10 @@ +//! This module aggregates and re-exports all individual page components of the application. +//! +//! Each sub-module within `pages` typically represents a distinct view or section +//! of the user interface, such as authentication, ticket management, or user profiles. +//! +//! By re-exporting them here, other parts of the application can import page +//! components more conveniently using `crate::pages::*`. pub mod basic_pages; pub mod setup; pub mod sidebar; diff --git a/frontend/src/pages/setup.rs b/frontend/src/pages/setup.rs index ba0d70b..2f8c532 100644 --- a/frontend/src/pages/setup.rs +++ b/frontend/src/pages/setup.rs @@ -4,6 +4,17 @@ use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew_router::prelude::*; +/// Payload for creating the initial administrator account. +/// +/// This struct is sent to the `/api/setup-admin` endpoint to create the first admin user +/// when no administrators exist in the system. It carries the necessary information +/// for the new admin's profile and credentials. +/// +/// # Fields +/// - `first_name`: The first name of the administrator. +/// - `last_name`: The last name of the administrator. +/// - `username`: The unique username for the administrator's login. +/// - `pwd`: The password for the administrator's account. This will be hashed on the backend. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct AdminSetupScheme { pub first_name: String, @@ -12,6 +23,43 @@ pub struct AdminSetupScheme { pub pwd: String, } +/// Component for the initial admin account setup page. +/// +/// This page is displayed when a fresh system has no administrator accounts. It provides +/// a form to create the first admin user. Key functionality: +/// +/// - **Admin Check**: On mount, verifies if an admin already exists by calling `/api/check-admin`. +/// If an admin is found, the user is redirected to the login page (`crate::Route::Login`). +/// - **Form Fields**: Collects `first_name`, `last_name`, `username`, `password`, and `confirm_password`. +/// - **Form Validation**: +/// - Ensures password fields are not empty. +/// - Verifies that `password` and `confirm_password` match. +/// - Ensures the `username` field is not empty. +/// - **API Interaction**: On form submission, a POST request is sent to `/api/setup-admin` +/// with the new admin's details. +/// - **Password Hashing**: The backend is responsible for hashing the password using Argon2 +/// before storage; this component only sends the plain text password. +/// - **Auto-redirect**: On successful admin account creation, the user is automatically +/// redirected to the login page (`crate::Route::Login`). +/// - **State Management**: Uses Yew's `use_state` hooks to manage: +/// - Input field values (`first_name`, `last_name`, `username`, `pwd`, `pwd_confirm`). +/// - UI states like `error` messages, `success` status, and `loading` indicators. +/// - `admin_check_done` to prevent rendering the form before the initial admin check completes. +/// +/// # Example Flow +/// 1. User navigates to `/setup`. +/// 2. The component checks `/api/check-admin`. +/// 3. If an admin exists, redirects to `/login`. +/// 4. If no admin exists, the setup form is displayed. +/// 5. User fills out the form and submits. +/// 6. Form data is sent via POST to `/api/setup-admin`. +/// 7. On successful response (HTTP 200), redirects to `/login`. +/// 8. On error, displays an error message to the user. +/// +/// # Security Notes +/// - The initial admin check prevents re-creating an admin if one already exists. +/// - Password confirmation helps prevent user typos for critical credentials. +/// - Backend validation further ensures non-empty and secure credentials. #[component(InitialAdminSetup)] pub fn initial_admin_setup() -> Html { let first_name = use_state(|| "".to_string()); diff --git a/frontend/src/pages/sidebar.rs b/frontend/src/pages/sidebar.rs index 49f7f2a..b51087a 100644 --- a/frontend/src/pages/sidebar.rs +++ b/frontend/src/pages/sidebar.rs @@ -9,6 +9,15 @@ use yew_router::prelude::*; const STORAGE_KEY: &str = "sidebar_state"; +/// Represents the expansion state of collapsible menus within the sidebar. +/// +/// This struct is used to persist the open/closed state of sidebar submenus, +/// improving user experience by remembering their last interaction. +/// The state is stored in and retrieved from `LocalStorage`. +/// +/// # Fields +/// - `ticket_open`: A boolean indicating if the "Tickets" submenu is expanded (`true`) or collapsed (`false`). +/// - `users_open`: A boolean indicating if the "Users" submenu is expanded (`true`) or collapsed (`false`). #[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct SidebarExpandState { pub ticket_open: bool, @@ -16,6 +25,7 @@ pub struct SidebarExpandState { } impl Default for SidebarExpandState { + /// Provides the default expansion state, where all submenus are collapsed. fn default() -> Self { Self { ticket_open: false, @@ -24,6 +34,17 @@ impl Default for SidebarExpandState { } } +/// Represents the shared state for the sidebar's expandable menus. +/// +/// This context struct is provided via Yew's `ContextProvider` and allows child components +/// within the sidebar to read and modify the expansion state of the "Tickets" and "Users" menus. +/// +/// # Fields +/// - `expand`: The current [`SidebarExpandState`] holding whether each menu is open or closed. +/// - `set_tickets_open`: A `Callback` to explicitly set the open state of the "Tickets" menu. +/// - `toggle_tickets`: A `Callback<()>` to toggle the open state of the "Tickets" menu. +/// - `set_users_open`: A `Callback` to explicitly set the open state of the "Users" menu. +/// - `toggle_users`: A `Callback<()>` to toggle the open state of the "Users" menu. #[derive(Clone, PartialEq)] pub struct SidebarState { pub expand: SidebarExpandState, @@ -34,6 +55,10 @@ pub struct SidebarState { } impl SidebarState { + /// Creates a new `SidebarState` instance. + /// + /// This constructor is typically used within the [`SidebarStateProvider`] to + /// bundle the current expansion state and its associated callbacks for context sharing. fn new( expand: SidebarExpandState, set_tickets_open: Callback, @@ -51,11 +76,40 @@ impl SidebarState { } } +/// Properties for components that provide sidebar state. +/// +/// This struct is typically used by context providers that wrap child components +/// and supply them with shared sidebar-related state or functionality. +/// +/// # Fields +/// - `children`: The child components that will have access to the provided sidebar state. #[derive(Properties, PartialEq)] pub struct SidebarProps { pub children: Children, } +/// A Yew context provider component that manages and supplies the sidebar's expansion state. +/// +/// This component is responsible for: +/// 1. Loading the initial `SidebarExpandState` from browser `LocalStorage` (or using defaults). +/// 2. Providing a `SidebarState` context (`Rc`) to its children, which includes +/// the current expansion state and callbacks to modify it. +/// 3. Persisting any changes to the `SidebarExpandState` back to `LocalStorage`. +/// +/// Child components (like [`TicketMenu`] and [`UsersMenu`]) can consume this context +/// to react to and control the sidebar's collapsible sections. +/// +/// # LocalStorage Key +/// The state is stored under the key `STORAGE_KEY` ("sidebar_state"). +/// +/// # Example Usage +/// ```rust +/// html! { +/// +/// // Sidebar and its sub-components will have access to the state +/// +/// } +/// ``` #[component(SidebarStateProvider)] pub fn sidebar_state_provider(props: &SidebarProps) -> Html { let default = LocalStorage::get(STORAGE_KEY).unwrap_or_else(|_| SidebarExpandState::default()); @@ -126,6 +180,27 @@ pub fn sidebar_state_provider(props: &SidebarProps) -> Html { } } +/// A collapsible menu component for "Tickets" within the sidebar. +/// +/// This component consumes the [`SidebarState`] context to manage its expanded/collapsed state. +/// It displays a button to toggle its visibility and, when expanded, reveals links +/// for "Submit Ticket" and "View Tickets". +/// +/// # Context +/// Requires [`SidebarStateProvider`] as an ancestor to provide the necessary context. +/// +/// # Functionality +/// - **Toggle Button**: Clicking the button toggles the `ticket_open` state in the `SidebarState`. +/// - **Submenu Links**: +/// - [`crate::Route::Ticket`]: Link to submit a new ticket. +/// - [`crate::Route::AllTickets`]: Link to view all tickets. +/// +/// # Example +/// ```rust +/// html! { +/// +/// } +/// ``` #[component(TicketMenu)] pub fn ticket_menu() -> Html { let ctx = @@ -169,6 +244,27 @@ pub fn ticket_menu() -> Html { } } +/// A collapsible menu component for "Users" within the sidebar. +/// +/// This component consumes the [`SidebarState`] context to manage its expanded/collapsed state. +/// It displays a button to toggle its visibility and, when expanded, reveals links +/// for "Create User" and "View Users". This menu is typically only visible to administrators. +/// +/// # Context +/// Requires [`SidebarStateProvider`] as an ancestor to provide the necessary context. +/// +/// # Functionality +/// - **Toggle Button**: Clicking the button toggles the `users_open` state in the `SidebarState`. +/// - **Submenu Links**: +/// - [`crate::Route::Register`]: Link to create a new user account. +/// - [`crate::Route::AllUsers`]: Link to view all registered users. +/// +/// # Example +/// ```rust +/// html! { +/// +/// } +/// ``` #[component(UsersMenu)] pub fn users_menu() -> Html { let ctx = @@ -212,6 +308,30 @@ pub fn users_menu() -> Html { } } +/// The main sidebar component of the application. +/// +/// This component dynamically renders its content based on the user's authentication +/// and administrative status. It fetches the current user's details via `/api/users/current` +/// to determine what menu items to display. +/// +/// # Structure +/// - Wraps its content in a [`SidebarStateProvider`] to allow nested menus to manage their state. +/// - Contains a navigation (`