diff --git a/backend/src/cookie/jwt.rs b/backend/src/cookie/jwt.rs index 8f37315..b386fff 100644 --- a/backend/src/cookie/jwt.rs +++ b/backend/src/cookie/jwt.rs @@ -6,8 +6,8 @@ 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. +/// Returned when token encoding or decoding fails via `encode_token` or `decode_token`. +/// Used in error responses for invalid or expired [`Claims`] tokens. /// /// # Fields /// - `status`: HTTP status text (e.g., "error") @@ -22,7 +22,7 @@ pub struct Error { /// /// 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. +/// using the given encoding key. The resulting token is a serialized [`Claims`]. /// /// # Arguments /// - `header`: The JWT header, specifying the algorithm (e.g., HS256). @@ -30,7 +30,7 @@ pub struct Error { /// - `key`: The `EncodingKey` used to sign the JWT. /// /// # Returns -/// A `String` representing the encoded JWT. +/// A `String` representing the encoded JWT containing [`Claims`]. /// /// # Panics /// Panics if the token encoding fails for any reason (e.g., invalid key). @@ -50,14 +50,15 @@ pub fn encode_token(header: &Header, id: String, key: &EncodingKey) -> String { /// /// 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. +/// and "not before" (`validate_nbf`) claims during validation. Returns the extracted [`Claims`] +/// on success. /// /// # 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`. +/// - `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)> { diff --git a/backend/src/cookie/validation.rs b/backend/src/cookie/validation.rs index e1d238c..bfaecd9 100644 --- a/backend/src/cookie/validation.rs +++ b/backend/src/cookie/validation.rs @@ -17,9 +17,10 @@ use crate::{AppState, cookie::jwt::decode_token, handlers::auth::filter_user, mo /// 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. +/// the `Authorization: Bearer` header), decodes and validates it using [`decode_token`](`crate::cookie::jwt::decode_token`)). +/// If valid, it fetches the corresponding [`User`] from the database and inserts a +/// [`FilteredUser`](crate::models::FilteredUser) +/// (converted via [`filter_user`](`crate::handlers::auth::filter_user`)) 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). @@ -110,9 +111,10 @@ pub async fn validate_token( /// 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`. +/// This middleware first performs all checks of [`validate_token`]: extracting, decoding, +/// and validating the JWT via [`decode_token`](`crate::cookie::jwt::decode_token`), and fetching the associated [`User`] from the database. +/// Additionally, it verifies that the fetched user has `is_admin` set to `true`. Returns a [`FilteredUser`](crate::models::FilteredUser) +/// (converted via [`filter_user`](`crate::handlers::auth::filter_user`)) in the request extensions if both authentication and admin status are valid. /// /// If the user is not authenticated or not an administrator, it returns an /// appropriate error response (401 Unauthorized or 403 Forbidden). diff --git a/backend/src/handlers/auth.rs b/backend/src/handlers/auth.rs index 4d251cb..7941a40 100644 --- a/backend/src/handlers/auth.rs +++ b/backend/src/handlers/auth.rs @@ -22,11 +22,12 @@ use crate::{ /// 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. +/// Creates a new [`User`] account with the provided [`UserCreateScheme`] 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 +/// - `State(data)`: Application state containing [`AppState`] for database access +/// - `request`: [`UserCreateScheme`] containing user details including first/last name, username, admin flag, and password /// /// # Returns /// - `200 OK` on successful user creation @@ -34,12 +35,7 @@ use crate::{ /// - `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)?; -/// ``` +/// Uses Argon2 with a cryptographically secure random salt. pub async fn create_user( State(data): State>, Json(request): Json, @@ -106,27 +102,23 @@ 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. +/// On successful authentication, generates and encodes a [`Claims`](crate::models::Claims) token via [`encode_token`](`crate::cookie::jwt::encode_token`) and sets it as an HTTP-only cookie. /// The token is valid for 1 hour. /// /// # Arguments -/// - `request`: Login credentials (username, password) +/// - `State(data)`: Application state containing [`AppState`] for database access +/// - `request`: [`LoginScheme`] containing login credentials (username, password) /// /// # Returns -/// - `200 OK` with JSON containing token and filtered user info +/// - `200 OK` with JSON containing token and filtered [`FilteredUser`] 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 +/// - Password verification uses Argon2 with stored [`User`] hash +/// - JWT token includes user ID and expiration timestamp via [`Claims`](crate::models::Claims) encoded by [`encode_token`](`crate::cookie::jwt::encode_token`) /// /// # Example Response /// ```json @@ -195,7 +187,7 @@ pub async fn login( /// /// 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. +/// session invalidation. The cookie no longer contains a valid [`Claims`](crate::models::Claims) token. /// /// # Returns /// Always returns `200 OK` with success message and an expired cookie header @@ -227,11 +219,11 @@ pub async fn logout() -> Result, State(data): State>, @@ -471,7 +468,10 @@ pub async fn update_user( /// 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. +/// It counts all [`User`] records with `is_admin = true` in the database. +/// +/// # Arguments +/// - `State(data)`: Application state containing [`AppState`] for database access /// /// # Returns /// - `200 OK` with JSON: `{"has_admin": bool}` - Whether at least one admin exists @@ -501,28 +501,24 @@ pub async fn check_admin_exists( /// 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. +/// This function handles the one-time setup of the first admin [`User`]. It checks that no admin exists +/// before allowing creation via database count. 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) +/// - `State(data)`: Application state containing [`AppState`] for database access +/// - `request`: [`UserCreateScheme`] containing 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 +/// - Admin already exists (checked via admin count) /// - 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)?; -/// ``` +/// The password is hashed using Argon2 with a random salt before storage. pub async fn setup_initial_admin( State(data): State>, Json(request): Json, @@ -587,17 +583,18 @@ pub async fn setup_initial_admin( } } -/// Converts a User with sensitive data into a FilteredUser safe for API responses. +/// 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. +/// returning [`User`] data to clients. Always use this helper instead of directly +/// serializing [`User`] objects. +/// Used by all authentication endpoints to ensure passwords are never exposed. /// /// # Arguments -/// - `user`: Reference to the internal User struct containing password hash +/// - `user`: Reference to the internal [`User`] struct containing password hash /// /// # Returns -/// FilteredUser with only safe-to-share information: +/// [`FilteredUser`] with only safe-to-share information: /// - `id`: User ID /// - `first_name`, `last_name`: User name /// - `username`: Login username @@ -610,7 +607,7 @@ pub async fn setup_initial_admin( /// # Example /// ```ignore /// let user = get_user_from_db(1).await?; -/// let safe_user = filter_user(&user); +/// let safe_user = filter_user(&user); // Convert User to FilteredUser /// // safe_user can be safely serialized and sent to client /// ``` pub fn filter_user(user: &User) -> FilteredUser { diff --git a/backend/src/handlers/ticket.rs b/backend/src/handlers/ticket.rs index 8f05cfc..7f2e131 100644 --- a/backend/src/handlers/ticket.rs +++ b/backend/src/handlers/ticket.rs @@ -16,12 +16,14 @@ use crate::{ /// Creates a new support ticket. /// -/// Associates the ticket with the authenticated user and sets the current timestamp. +/// Associates the ticket with the authenticated [`FilteredUser`] and sets the current timestamp. +/// Converts the [`TicketCreateScheme`] request into a database record. /// Tickets are automatically created with "open" status. /// /// # Arguments -/// - `user`: Authenticated user (extracted from JWT token) -/// - `body`: Ticket details (category, subject, description, room) +/// - `Extension(user)`: Authenticated [`FilteredUser`] (extracted from JWT token via middleware) +/// - `State(data)`: Application state containing [`AppState`] for database access +/// - `Json(body)`: [`TicketCreateScheme`] containing ticket details (category, subject, description, room) /// /// # Returns /// - `200 OK` on successful creation @@ -60,10 +62,11 @@ pub async fn create_ticket( /// Deletes a ticket by ID. /// -/// Only admins can delete tickets. Marks the ticket as deleted or removes from database. +/// Only admins can delete tickets (enforced by middleware). Removes the [`TicketResponse`] and associated data from the database. /// /// # Arguments -/// - `id`: Ticket ID to delete +/// - `Path(id)`: Ticket ID to delete, extracted from URL path +/// - `State(data)`: Application state containing [`AppState`] for database access /// /// # Returns /// - `204 No Content` on successful deletion @@ -97,15 +100,18 @@ pub async fn delete_ticket( /// 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). +/// Returns a list of all active [`TicketResponse`] objects with user information denormalized for easier rendering. +/// Tickets are ordered by creation date (newest first). Joins with [`User`](crate::models::User) table to include creator information. +/// +/// # Arguments +/// - `State(data)`: Application state containing [`AppState`] for database access /// /// # Filtering /// - Excludes tickets with status "Archived" -/// - Uses LEFT JOIN to include creator information +/// - Uses LEFT JOIN to include creator information from [`User`](crate::models::User) /// /// # Returns -/// - `200 OK` with array of TicketResponse objects +/// - `200 OK` with array of [`TicketResponse`] objects /// - `500 Internal Server Error` if database query fails /// /// # Example Response @@ -169,12 +175,14 @@ pub async fn get_tickets( /// Retrieves a specific ticket by ID. /// /// Includes full ticket details and denormalized user information (creator name). +/// Returns a [`TicketResponse`] with all metadata by joining with [`User`](crate::models::User) table. /// /// # Arguments -/// - `id`: Ticket ID to retrieve +/// - `Path(id)`: Ticket ID to retrieve, extracted from URL path +/// - `State(data)`: Application state containing [`AppState`] for database access /// /// # Returns -/// - `200 OK` with TicketResponse object +/// - `200 OK` with [`TicketResponse`] object /// - `404 Not Found` if ticket doesn't exist /// - `500 Internal Server Error` if database error occurs /// @@ -242,15 +250,16 @@ 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). +/// Only admins can update ticket status (enforced by middleware). Applies [`TicketUpdateScheme`] to modify the [`TicketResponse`]. +/// 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 +/// - `Path(id)`: Ticket ID to update, extracted from URL path +/// - `State(data)`: Application state containing [`AppState`] for database access +/// - `Json(body)`: [`TicketUpdateScheme`] update payload containing new status /// /// # Returns -/// - `200 OK` with updated TicketResponse +/// - `200 OK` with updated [`TicketResponse`] /// - `500 Internal Server Error` if ticket not found or database error /// /// # Typical Status Flow diff --git a/backend/src/main.rs b/backend/src/main.rs index 04c0dd9..2f8edcf 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -25,11 +25,12 @@ 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. +/// This is wrapped in Arc for thread-safe sharing across async tasks and cloned into each route +/// via `with_state`. /// /// # Fields -/// - `db`: PostgreSQL connection pool for database access -/// - `env`: Configuration loaded from environment variables +/// - `db`: PostgreSQL connection pool for database access (via `sqlx::PgPool`) +/// - `env`: [`Env`] configuration loaded from environment variables pub struct AppState { db: PgPool, env: Env, @@ -39,15 +40,19 @@ pub struct AppState { /// /// Initializes the server by: /// 1. Loading environment variables from `.env` file -/// 2. Establishing database connection pool +/// 2. Establishing database connection pool to PostgreSQL /// 3. Configuring CORS policy for cross-origin requests -/// 4. Starting HTTP server on port 8001 +/// 4. Creating the router with [`create_router`] containing all endpoints +/// 5. 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 +/// - CORS origin configured from [`Env`] +/// +/// # State Setup +/// Creates shared [`AppState`] wrapped in `Arc` and passes to all routes /// /// # Panics /// - If environment loading fails @@ -85,6 +90,7 @@ async fn main() { .layer(cors); // Start listening for incoming connections - let listener = tokio::net::TcpListener::bind("0.0.0.0:8001").await.unwrap(); + let uri = format!("0.0.0.0:{}", env.backend_port); + let listener = tokio::net::TcpListener::bind(&uri).await.unwrap(); let _ = axum::serve(listener, app).await; } diff --git a/backend/src/models.rs b/backend/src/models.rs index 184670d..a6d5111 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; /// API response for a ticket with user information. /// /// Returned by ticket endpoints. Includes denormalized user data for easier frontend rendering. +/// Created via [`TicketCreateScheme`]. /// /// # Fields /// - `id`: Unique ticket identifier @@ -12,7 +13,7 @@ use serde::{Deserialize, Serialize}; /// - `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_id`: ID of the user who created the ticket (references [`User`]) /// - `user_first_name`, `user_last_name`: User's name (denormalized for convenience) /// /// # Example @@ -47,7 +48,7 @@ pub struct TicketResponse { /// 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. +/// This should NEVER be sent directly to clients - always use [`FilteredUser`] instead. /// /// # Fields /// - `id`: Unique user identifier @@ -58,7 +59,7 @@ pub struct TicketResponse { /// /// # 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. +/// Use [`filter_user()`](`crate::handlers::auth::filter_user`) to convert to [`FilteredUser`] for responses. #[derive(Deserialize, Serialize, PartialEq, Debug, Clone, sqlx::FromRow)] pub struct User { pub id: i16, @@ -72,7 +73,7 @@ pub struct User { /// 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. +/// authenticated user and sets the creation timestamp. Converted to [`TicketResponse`] for the response. /// /// # Fields /// - `category`: Ticket category/type @@ -89,7 +90,7 @@ pub struct TicketCreateScheme { /// Payload for updating a ticket. /// -/// Sent to `PATCH /api/tickets/{id}`. Currently only allows status updates. +/// Sent to `PATCH /api/tickets/{id}`. Allows updating the ticket [`TicketResponse::status`]. /// Only admins can update tickets. /// /// # Fields @@ -102,10 +103,10 @@ pub struct TicketUpdateScheme { /// 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. +/// Only admins can update [`User`] records. Empty password field means no password change. /// /// # Fields -/// - `id`: User ID to update +/// - `id`: [`User`] ID to update /// - `first_name`, `last_name`: Updated user name /// - `username`: Updated login username /// - `make_admin`: New admin privilege status @@ -123,7 +124,7 @@ pub struct UserUpdateScheme { /// 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. +/// The password is hashed server-side before storage using Argon2. Converted to [`User`] for storage. /// /// # Fields /// - `first_name`: User's first name @@ -155,7 +156,7 @@ pub struct LoginScheme { /// User information sent to clients, excluding password hashes. /// -/// This is the safe version of User data that gets returned in API responses. +/// 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)] @@ -170,10 +171,10 @@ pub struct FilteredUser { /// 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. +/// Generated during login via `encode_token` and verified via `decode_token`. /// /// # Fields -/// - `sub`: Subject - the user ID as a string +/// - `sub`: Subject - the user ID as a string (references [`User`]) /// - `issued`: Unix timestamp when token was created /// - `expires`: Unix timestamp when token expires (currently 1 hour from creation) /// diff --git a/backend/src/router.rs b/backend/src/router.rs index bc41ba5..0c37299 100644 --- a/backend/src/router.rs +++ b/backend/src/router.rs @@ -19,30 +19,31 @@ use crate::{ /// Creates the complete router with all API endpoints. /// -/// The router is organized in layers for proper middleware application: +/// The router is organized in layers for proper middleware application. Uses [`AppState`] +/// for shared application context across all routes. /// /// ## 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 +/// - `GET /api/tickets/{id}` - Get specific ticket details (via `get_ticket_by_id`) +/// - `DELETE /api/tickets/{id}` - Delete a ticket (via `delete_ticket`) +/// - `PATCH /api/tickets/{id}` - Update ticket status (via `edit_ticket`) +/// - `POST /api/register` - Create a new user (via `create_user`) +/// - `GET /api/users` - List all users (via `get_users`) +/// - `GET /api/users/{id}` - Get user details (via `get_user_by_id`) +/// - `DELETE /api/users/{id}` - Delete a user (via `delete_user`) +/// - `PATCH /api/users/{id}` - Update user details (via `update_user`) /// /// ### 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 +/// - `GET /api/tickets` - List all tickets (via `get_tickets`) +/// - `POST /api/tickets/create` - Create a new ticket (via `create_ticket`) +/// - `GET /api/logout` - Logout user (via `logout`) +/// - `GET /api/users/current` - Get current authenticated user (via `get_current_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) +/// - `POST /api/login` - User login (via `login`) +/// - `GET /api/check-admin` - Check if admin exists (via `check_admin_exists`) +/// - `POST /api/setup-admin` - Create initial admin account (via `setup_initial_admin`) /// /// # Middleware Stack /// - Admin routes have `validate_admin` middleware diff --git a/frontend/src/auth.rs b/frontend/src/auth.rs index 18720fa..77fc817 100644 --- a/frontend/src/auth.rs +++ b/frontend/src/auth.rs @@ -33,14 +33,14 @@ pub struct ProtectedRouteProps { /// 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. +/// This component uses the backend's validation middleware by fetching the current user's authentication +/// and admin status from the `/api/users/current` endpoint (which requires a valid JWT token). +/// 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. +/// - **Initial Load**: Displays "Loading..." while checking authentication status via the backend. /// - **Not Authenticated**: Redirects to the login page (`crate::Route::Login`). -/// - **Authenticated**: +/// - **Authenticated** (valid JWT token from backend): /// - 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 diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index e8e8222..8d411b3 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -10,7 +10,9 @@ 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. +/// enabling navigation within the single-page application. Each route is protected +/// by [`ProtectedRoute`] middleware where appropriate to enforce authentication and authorization. +/// See [`switch`] for the routing logic. #[derive(Clone, PartialEq, Routable)] enum Route { /// The application's home page. @@ -65,10 +67,10 @@ pub struct SidebarShellProps { /// 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. +/// is always present for navigation. Integrates with [`crate::pages::sidebar::Sidebar`] for navigation. /// /// # Components -/// - [`sidebar::Sidebar`]: The navigation sidebar component. +/// - [`crate::pages::sidebar::Sidebar`]: The navigation sidebar component. /// - Main content area: Renders the `children` passed to this component. /// /// # Example @@ -101,11 +103,11 @@ pub struct AdminCheckWrapperProps { /// /// 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. +/// backend's `/api/check-admin` endpoint (via `crate::handlers::auth::check_admin_exists`) to determine system state. /// /// # Behavior -/// - **Loading**: Displays "Loading..." while checking admin status -/// - **No Admin**: Automatically redirects to `/setup` page to initialize +/// - **Loading**: Displays "Loading..." while checking admin status from the backend +/// - **No Admin**: Automatically redirects to [`Route::Setup`] page for initialization /// - **Admin Exists**: Renders the wrapped children (e.g., login page) /// /// # Example Usage @@ -115,19 +117,9 @@ pub struct AdminCheckWrapperProps { /// /// ``` /// -/// # 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)) -/// } -/// }); -/// ``` +/// # Backend Integration +/// The check queries the backend's `/api/check-admin` endpoint which returns `{"has_admin": bool}`. +/// This allows the frontend to determine if initial admin setup is required. #[component(AdminCheckWrapper)] fn admin_check_wrapper(props: &AdminCheckWrapperProps) -> Html { let admin_exists = use_state(|| None::); @@ -166,18 +158,24 @@ 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 +/// 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. +/// navigation and authentication flow. /// -/// Many routes are wrapped in a [`ProtectedRoute`] to enforce authentication -/// and authorization, and in a [`SidebarShell`] to maintain consistent layout. +/// Most routes are wrapped in [`ProtectedRoute`] to enforce authentication +/// and authorization based on the `admin_page` flag, and in [`SidebarShell`] to maintain consistent layout. +/// Login and Setup routes use [`AdminCheckWrapper`] instead to handle pre-authentication states. /// /// # Arguments /// - `route`: The [`Route`] enum variant representing the current URL path. /// /// # Returns /// An `Html` component that should be rendered for the given route. +/// +/// # Route Protection +/// - **Admin-required routes** (`admin_page={true}`): Require both authentication and admin privileges +/// - **Public routes** (`admin_page={false}`): Require only authentication +/// - **Pre-auth routes** (`AdminCheckWrapper`): Used before admin creation or login fn switch(route: Route) -> Html { match route { Route::Home => html! { @@ -259,12 +257,16 @@ 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. +/// `BrowserRouter` and `Switch` components. All other application content +/// is rendered based on the current [`Route`] matched by the `switch` function. +/// +/// Uses [`switch`] as the routing dispatcher to handle all route-specific rendering, +/// which applies appropriate middleware like [`ProtectedRoute`] and [`AdminCheckWrapper`]. /// /// # Structure -/// - [`BrowserRouter`]: Enables client-side routing. -/// - [`Switch`]: Renders components based on the matched [`Route`]. +/// - `BrowserRouter`: Enables client-side routing. +/// - `Switch`: Renders components based on the matched [`Route`]. +/// - `switch` function: Determines which component to render for each 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 fe5850e..90c5c9f 100644 --- a/frontend/src/pages/basic_pages.rs +++ b/frontend/src/pages/basic_pages.rs @@ -4,6 +4,29 @@ use yew::prelude::*; use yew_router::prelude::*; #[macro_export] +/// Removes surrounding double quotes from a string. +/// +/// This macro takes an expression that evaluates to a string and returns a new `String` +/// with any leading or trailing double quotes removed. It's useful for cleaning up +/// string data that might be inadvertently wrapped in quotes, such as JSON string values. +/// +/// # Arguments +/// +/// * `$str`: An expression that can be converted into a string slice (`&str`). +/// +/// # Examples +/// +/// ```rust +/// use your_crate::dequote; // Assuming `dequote` is re-exported or in scope +/// +/// let quoted_string = "\"hello world\""; +/// let dequoted_string = dequote!(quoted_string); +/// assert_eq!(dequoted_string, "hello world"); +/// +/// let already_clean = "no quotes"; +/// let dequoted_clean = dequote!(already_clean); +/// assert_eq!(dequoted_clean, "no quotes"); +/// ``` macro_rules! dequote { ($str:expr) => { $str.trim_matches('"').to_string() diff --git a/frontend/src/pages/setup.rs b/frontend/src/pages/setup.rs index 2f8c532..127c2d5 100644 --- a/frontend/src/pages/setup.rs +++ b/frontend/src/pages/setup.rs @@ -29,7 +29,7 @@ pub struct AdminSetupScheme { /// 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`). +/// 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. diff --git a/frontend/src/pages/sidebar.rs b/frontend/src/pages/sidebar.rs index cff0657..b8c57b4 100644 --- a/frontend/src/pages/sidebar.rs +++ b/frontend/src/pages/sidebar.rs @@ -7,6 +7,11 @@ use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew_router::prelude::*; +/// The key used to store and retrieve the sidebar's expansion state in `LocalStorage`. +/// +/// This constant ensures consistency when accessing the stored state across different +/// parts of the application. The value associated with this key in `LocalStorage` +/// will be a serialized [`SidebarExpandState`] object. const STORAGE_KEY: &str = "sidebar_state"; /// Represents the expansion state of collapsible menus within the sidebar. diff --git a/frontend/src/pages/user.rs b/frontend/src/pages/user.rs index 6a25b63..1855014 100644 --- a/frontend/src/pages/user.rs +++ b/frontend/src/pages/user.rs @@ -102,6 +102,14 @@ pub struct UserProps { pub id: i16, } +/// Represents an error response from the API. +/// +/// This struct is used to deserialize error messages received from the backend API. +/// It typically contains a human-readable message and an internal status code. +/// +/// # Fields +/// - `message`: A `String` containing a description of the error. +/// - `_status`: An internal status code or message, often ignored in frontend display. #[derive(Deserialize, Debug)] struct ApiError { message: String, diff --git a/frontend/src/pages/utilities.rs b/frontend/src/pages/utilities.rs index 968f840..3235097 100644 --- a/frontend/src/pages/utilities.rs +++ b/frontend/src/pages/utilities.rs @@ -24,6 +24,15 @@ struct TicketPartial { user_id: i16, } +/// A partial representation of a user, containing only the fields necessary for statistical analysis. +/// +/// This struct is used to efficiently retrieve and process user data for diagnostics +/// without fetching the full user details. +/// +/// # Fields +/// - `id`: The unique identifier of the user. +/// - `first_name`: The first name of the user. +/// - `last_name`: The last name of the user. #[derive(Debug, Deserialize, Clone, PartialEq)] struct UserPartial { id: i16, @@ -37,12 +46,20 @@ struct UserPartial { /// for calculating and visualizing ticket distribution per room. /// /// # Fields -/// - `tickets`: A vector of `TicketPartial` containing ticket data relevant for room totals. +/// - `tickets`: A vector of [`TicketPartial`] containing ticket data relevant for room totals. #[derive(Properties, PartialEq)] struct RoomTotalsProps { tickets: Vec, } +/// Properties for components that display user-wise ticket totals. +/// +/// This struct passes a list of partial user data and partial ticket data to a component +/// responsible for calculating and visualizing ticket distribution per user. +/// +/// # Fields +/// - `users`: A vector of [`UserPartial`] containing user data relevant for user totals. +/// - `tickets`: A vector of [`TicketPartial`] containing ticket data relevant for user totals. #[derive(Properties, PartialEq)] struct UserTotalProps { users: Vec, @@ -79,7 +96,7 @@ fn weekday_index(dt: &DateTime) -> usize { /// the total count of tickets submitted on that day. /// /// # Arguments -/// - `tickets`: A slice of `TicketPartial` items to count. +/// - `tickets`: A slice of [`TicketPartial`] items to count. /// /// # Returns /// An array `[usize; 7]` with ticket counts for each weekday. @@ -93,12 +110,12 @@ fn count_by_weekday(tickets: &[TicketPartial]) -> [usize; 7] { /// Calculates the occurrences of each weekday within the date range covered by the tickets. /// -/// This function determines the minimum and maximum dates from the provided `TicketPartial` +/// This function determines the minimum and maximum dates from the provided [`TicketPartial`] /// slice and then counts how many times each weekday occurs within that inclusive date range. /// This is useful for normalizing ticket counts against the number of available days for each weekday. /// /// # Arguments -/// - `partials`: A slice of `TicketPartial` items defining the date range. +/// - `partials`: A slice of [`TicketPartial`] items defining the date range. /// /// # Returns /// An array `[usize; 7]` where each element represents the number of times a @@ -147,7 +164,8 @@ fn day_counts(partials: &[TicketPartial]) -> [usize; 7] { /// Converts a numerical room representation back into a human-readable string format. /// -/// This function is the inverse of the room parsing logic in `SubmitTicket` component. +/// This function is the inverse of the room parsing logic in +/// [`SubmitTicket`](`crate::pages::ticket::SubmitTicket`) component. /// It converts negative numbers back to "K" prefixed rooms, numbers >= 1000 back to "D" prefixed rooms, /// and other numbers to their string representation. /// @@ -318,12 +336,12 @@ pub fn ticket_count_component() -> Html { /// /// # State /// Uses `use_state` hooks to manage: -/// - `tickets`: A vector of `TicketPartial` for statistical analysis. +/// - `tickets`: A vector of [`TicketPartial`] for statistical analysis. /// - `error`: Any error message from API calls. /// - `loading`: A boolean indicating if data is being fetched. /// /// # Functionality -/// - Fetches all tickets (as `TicketPartial`) from `/api/tickets`. +/// - Fetches all tickets (as [`TicketPartial`]) from `/api/tickets`. /// - Calculates: /// - `counts`: Number of tickets submitted on each weekday. /// - `occ`: Number of occurrences of each weekday in the ticket date range. @@ -465,7 +483,7 @@ pub fn submit_stats_component() -> Html { /// 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 /// in a sorted list with a bar chart visualization. /// @@ -477,7 +495,7 @@ pub fn submit_stats_component() -> Html { /// - **Sorts Results**: Displays rooms sorted by ticket count in descending order. /// - **Visualizes Data**: Renders a bar chart where the width of each bar is /// proportional to the ticket count for that room, relative to the room with the maximum tickets. -/// - **Room Formatting**: Uses the `parse_room` function to display room numbers +/// - **Room Formatting**: Uses the [`parse_room`] function to display room numbers /// in a human-readable format. /// /// # Example @@ -522,6 +540,30 @@ fn room_total_component(props: &RoomTotalsProps) -> Html { } } +/// A component that displays the total number of tickets per user. +/// +/// This component takes lists of [`UserPartial`] and [`TicketPartial`] items to calculate +/// and display the total number of tickets submitted by each user. +/// The results are presented in a sorted list with a bar chart visualization. +/// +/// # Props +/// - `users`: A `Vec` containing partial user data. +/// - `tickets`: A `Vec` containing partial ticket data for analysis. +/// +/// # Functionality +/// - **Maps User Names**: Creates a map from user IDs to their first and last names for display. +/// - **Calculates Totals**: Aggregates ticket counts for each user. +/// - **Sorts Results**: Displays users sorted by their ticket count in descending order. +/// - **Visualizes Data**: Renders a bar chart where the width of each bar is proportional +/// to the ticket count for that user, relative to the user with the maximum tickets. +/// - **Name Formatting**: Uses the [`dequote!`] macro to clean up user names before display. +/// +/// # Example +/// ```rust +/// html! { +/// +/// } +/// ``` #[component(UserTotal)] fn user_total_component(props: &UserTotalProps) -> Html { let name_map: HashMap = props